Temporarily include bndlib 1.47 for testing purposes (not yet on central)

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1185095 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bundleplugin/src/main/java/aQute/lib/base64/Base64.java b/bundleplugin/src/main/java/aQute/lib/base64/Base64.java
new file mode 100644
index 0000000..1210a02
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/base64/Base64.java
@@ -0,0 +1,135 @@
+package aQute.lib.base64;
+
+import java.io.*;
+
+/*
+ * Base 64 converter.
+ * 
+ * TODO Implement string to byte[]
+ */
+public class Base64 {
+	byte[]				data;
+
+	static final String	alphabet	= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+	static byte[]		values		= new byte[128];
+
+	static {
+		for (int i = 0; i < values.length; i++) {
+			values[i] = -1;
+		}
+		// Create reverse index
+		for (int i = 0; i < alphabet.length(); i++) {
+			char c = alphabet.charAt(i);
+			values[c] = (byte) i;
+		}
+	}
+
+	public Base64(byte data[]) {
+		this.data = data;
+	}
+
+	public final static byte[] decodeBase64(String string) {
+		string = string.trim();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+		int register = 0;
+		int i = 0;
+		int pads = 0;
+
+		byte test[] = new byte[3];
+
+		while (i < string.length()) {
+			char c = string.charAt(i);
+			if (c > 0x7F)
+				throw new IllegalArgumentException(
+						"Invalid base64 character in " + string
+								+ ", character value > 128 ");
+			
+			int v = 0;
+			if ( c == '=' ) {
+				pads++;
+			} else {
+				v = values[c];
+				if ( v < 0 )
+					throw new IllegalArgumentException(
+							"Invalid base64 character in " + string + ", " + c );
+			}					
+			register <<= 6;
+			register |= v;
+			test[2] = (byte) (register & 0xFF);
+			test[1] = (byte) ((register >> 8) & 0xFF);
+			test[0] = (byte) ((register >> 16) & 0xFF);
+
+			i++;
+
+			if ((i % 4) == 0) {
+				flush(out, register, pads);
+				register = 0;
+				pads = 0;
+			}
+		}
+		return out.toByteArray();
+	}
+
+	static private void flush(ByteArrayOutputStream out, int register, int pads) {
+		switch (pads) {
+		case 0:
+			out.write(0xFF & (register >> 16));
+			out.write(0xFF & (register >> 8));
+			out.write(0xFF & (register >> 0));
+			break;
+			
+		case 1:
+			out.write(0xFF & (register >> 16));
+			out.write(0xFF & (register >> 8));
+			break;
+			
+		case 2:
+			out.write(0xFF & (register >> 16));
+		}
+	}
+
+	public Base64(String s) {
+		data = decodeBase64(s);
+	}
+
+	public String toString() {
+		return encodeBase64(data);
+	}
+
+	public static String encodeBase64(byte data[]) {
+		StringBuffer sb = new StringBuffer();
+		int buf = 0;
+		int bits = 0;
+		int n = 0;
+
+		while (true) {
+			if (bits >= 6) {
+				bits -= 6;
+				int v = 0x3F & (buf >> bits);
+				sb.append(alphabet.charAt(v));
+			} else {
+				if (n >= data.length)
+					break;
+
+				buf <<= 8;
+				buf |= 0xFF & data[n++];
+				bits += 8;
+			}
+		}
+		if (bits != 0) // must be less than 7
+			sb.append(alphabet.charAt(0x3F & (buf << (6 - bits))));
+
+		int mod = 4 - (sb.length() % 4);
+		if (mod != 4) {
+			for (int i = 0; i < mod; i++)
+				sb.append('=');
+		}
+		return sb.toString();
+	}
+
+	public Object toData() {
+		return data;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/base64/packageinfo b/bundleplugin/src/main/java/aQute/lib/base64/packageinfo
new file mode 100644
index 0000000..7c8de03
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/base64/packageinfo
@@ -0,0 +1 @@
+version 1.0
diff --git a/bundleplugin/src/main/java/aQute/lib/collections/LineCollection.java b/bundleplugin/src/main/java/aQute/lib/collections/LineCollection.java
new file mode 100644
index 0000000..36bfa39
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/collections/LineCollection.java
@@ -0,0 +1,54 @@
+package aQute.lib.collections;
+
+import java.io.*;
+import java.util.*;
+
+public class LineCollection implements Iterator<String>, Closeable {
+	final BufferedReader	reader;
+	String					next;
+
+	public LineCollection(InputStream in) throws IOException {
+		this(new InputStreamReader(in, "UTF8"));
+	}
+
+	public LineCollection(File in) throws IOException {
+		this(new FileReader(in));
+	}
+
+	public LineCollection(Reader reader) throws IOException {
+		this(new BufferedReader(reader));
+	}
+
+	public LineCollection(BufferedReader reader) throws IOException {
+		this.reader = reader;
+		next = reader.readLine();
+	}
+
+	public boolean hasNext() {
+		return next != null;
+	}
+
+	public String next() {
+		if (next == null)
+			throw new IllegalStateException("Iterator has finished");
+		try {
+			String result = next;
+			next = reader.readLine();
+			if (next == null)
+				reader.close();
+			return result;
+		} catch (Exception e) {
+			// ignore
+			return null;
+		}
+	}
+
+	public void remove() {
+		if (next == null)
+			throw new UnsupportedOperationException("Cannot remove");
+	}
+
+	public void close() throws IOException {
+		reader.close();
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/collections/Logic.java b/bundleplugin/src/main/java/aQute/lib/collections/Logic.java
new file mode 100644
index 0000000..75322dd
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/collections/Logic.java
@@ -0,0 +1,22 @@
+package aQute.lib.collections;
+
+import java.util.*;
+
+public class Logic {
+	
+	public static <T> Collection<T> retain( Collection<T> first, Collection<T> ... sets) {
+		Set<T> result = new HashSet<T>(first);
+		for ( Collection<T> set : sets ) {
+			result.retainAll(set);
+		}
+		return result;
+	}
+	
+	public static <T> Collection<T> remove( Collection<T> first, Collection<T> ... sets) {
+		Set<T> result = new HashSet<T>(first);
+		for ( Collection<T> set : sets ) {
+			result.removeAll(set);
+		}
+		return result;
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/collections/MultiMap.java b/bundleplugin/src/main/java/aQute/lib/collections/MultiMap.java
new file mode 100644
index 0000000..7672638
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/collections/MultiMap.java
@@ -0,0 +1,83 @@
+package aQute.lib.collections;
+
+import java.util.*;
+
+public class MultiMap<K,V> extends HashMap<K,Set<V>> {
+	private static final long	serialVersionUID	= 1L;
+	final Set<V> EMPTY = Collections.emptySet();
+	
+	public boolean add( K key, V value ) {
+		Set<V> set = get(key);
+		if ( set == null) {
+			set=new HashSet<V>();
+			put(key,set);
+		}
+		return set.add(value);
+	}
+	
+	public boolean addAll( K key, Collection<V> value ) {
+		Set<V> set = get(key);
+		if ( set == null) {
+			set=new HashSet<V>();
+			put(key,set);
+		}
+		return set.addAll(value);
+	}
+	
+	public boolean remove( K key, V value ) {
+		Set<V> set = get(key);
+		if ( set == null) {
+			return false;
+		}
+		boolean result = set.remove(value);
+		if ( set.isEmpty())
+			remove(key);
+		return result;
+	}
+	
+	public boolean removeAll( K key, Collection<V> value ) {
+		Set<V> set = get(key);
+		if ( set == null) {
+			return false;
+		}
+		boolean result = set.removeAll(value);
+		if ( set.isEmpty())
+			remove(key);
+		return result;
+	}
+	
+	public Iterator<V> iterate(K key) {
+		Set<V> set = get(key);
+		if ( set == null)
+			return EMPTY.iterator();
+		else
+			return set.iterator();
+	}
+	
+	public Iterator<V> all() {
+		return new Iterator<V>() {
+			Iterator<Set<V>> master = values().iterator();
+			Iterator<V> current = null;
+			
+			public boolean hasNext() {
+				if ( current == null || !current.hasNext()) {
+					if ( master.hasNext()) {
+						current = master.next().iterator();
+						return current.hasNext();
+					}
+					return false;
+				}
+				return true;
+			}
+
+			public V next() {
+				return current.next();
+			}
+
+			public void remove() {
+				current.remove();
+			}
+			
+		};
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/collections/packageinfo b/bundleplugin/src/main/java/aQute/lib/collections/packageinfo
new file mode 100644
index 0000000..7c8de03
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/collections/packageinfo
@@ -0,0 +1 @@
+version 1.0
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/FileInstallRepo.java b/bundleplugin/src/main/java/aQute/lib/deployer/FileInstallRepo.java
new file mode 100644
index 0000000..cf05f98
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/FileInstallRepo.java
@@ -0,0 +1,149 @@
+package aQute.lib.deployer;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.jar.*;
+import java.util.regex.*;
+
+import aQute.lib.osgi.*;
+import aQute.libg.reporter.*;
+import aQute.libg.version.*;
+
+public class FileInstallRepo extends FileRepo {
+
+	String group;
+	boolean dirty;
+	Reporter reporter;
+	Pattern              REPO_FILE   = Pattern
+    .compile("([-a-zA-z0-9_\\.]+)-([0-9\\.]+)\\.(jar|lib)");
+	
+    public void setProperties(Map<String, String> map) {
+    	super.setProperties(map);
+    	group = map.get("group");
+    }
+    public void setReporter(Reporter reporter) {
+    	super.setReporter(reporter);
+        this.reporter = reporter;
+    }
+
+    public File put(Jar jar) throws Exception {
+        dirty = true;
+        Manifest manifest = jar.getManifest();
+        if (manifest == null)
+            throw new IllegalArgumentException("No manifest in JAR: " + jar);
+
+        String bsn = manifest.getMainAttributes().getValue(
+                Analyzer.BUNDLE_SYMBOLICNAME);
+        if (bsn == null)
+            throw new IllegalArgumentException("No Bundle SymbolicName set");
+
+        Map<String, Map<String, String>> b = Processor.parseHeader(bsn, null);
+        if (b.size() != 1)
+            throw new IllegalArgumentException("Multiple bsn's specified " + b);
+
+        for (String key : b.keySet()) {
+            bsn = key;
+            if (!Verifier.SYMBOLICNAME.matcher(bsn).matches())
+                throw new IllegalArgumentException(
+                        "Bundle SymbolicName has wrong format: " + bsn);
+        }
+
+        String versionString = manifest.getMainAttributes().getValue(
+                Analyzer.BUNDLE_VERSION);
+        Version version;
+        if (versionString == null)
+            version = new Version();
+        else
+            version = new Version(versionString);
+
+        File dir;
+        if (group == null) {
+        	dir = getRoot();
+        } else {
+        	dir= new File(getRoot(), group);
+        	dir.mkdirs();
+        }
+        String fName = bsn + "-" + version.getMajor() + "."
+                + version.getMinor() + "." + version.getMicro() + ".jar";
+        File file = new File(dir, fName);
+
+        jar.write(file);
+        fireBundleAdded(jar, file);
+
+        file = new File(dir, bsn + "-latest.jar");
+        if (file.isFile() && file.lastModified() < jar.lastModified()) {
+            jar.write(file);
+        }
+        return file;
+    }
+    public boolean refresh() {
+        if ( dirty ) {
+            dirty = false;
+            return true;
+        } else 
+            return false;
+    }
+	@Override
+	public List<String> list(String regex) {
+	       Instruction pattern = null;
+	        if (regex != null)
+	            pattern = Instruction.getPattern(regex);
+
+	        String list[] = getRoot().list();
+	        List<String> result = new ArrayList<String>();
+	        for (String f : list) {
+                Matcher m = REPO_FILE.matcher(f);
+                if (!m.matches()) {
+                	continue;
+                }
+                String s = m.group(1);
+	            if (pattern == null || pattern.matches(s))
+	                result.add(s);
+	        }
+	        return result;
+	}
+	@Override
+	public File[] get(String bsn, String versionRange) throws MalformedURLException {
+	       // If the version is set to project, we assume it is not
+        // for us. A project repo will then get it.
+        if (versionRange != null && versionRange.equals("project"))
+            return null;
+
+        //
+        // The version range we are looking for can
+        // be null (for all) or a version range.
+        //
+        VersionRange range;
+        if (versionRange == null || versionRange.equals("latest")) {
+            range = new VersionRange("0");
+        } else
+            range = new VersionRange(versionRange);
+
+        //
+        // Iterator over all the versions for this BSN.
+        // Create a sorted map over the version as key
+        // and the file as URL as value. Only versions
+        // that match the desired range are included in
+        // this list.
+        //
+        File instances[] = getRoot().listFiles();
+        SortedMap<Version, File> versions = new TreeMap<Version, File>();
+        for (int i = 0; i < instances.length; i++) {
+            Matcher m = REPO_FILE.matcher(instances[i].getName());
+            if (m.matches() && m.group(1).equals(bsn)) {
+                String versionString = m.group(2);
+                Version version;
+                if (versionString.equals("latest"))
+                    version = new Version(Integer.MAX_VALUE);
+                else
+                    version = new Version(versionString);
+
+                if (range.includes(version))
+                    versions.put(version, instances[i]);
+            }
+        }
+        return (File[]) versions.values().toArray(new File[versions.size()]);
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/FileRepo.java b/bundleplugin/src/main/java/aQute/lib/deployer/FileRepo.java
new file mode 100644
index 0000000..0c701aa
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/FileRepo.java
@@ -0,0 +1,311 @@
+package aQute.lib.deployer;
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+import java.util.regex.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.io.*;
+import aQute.lib.osgi.*;
+import aQute.libg.reporter.*;
+import aQute.libg.version.*;
+
+public class FileRepo implements Plugin, RepositoryPlugin, Refreshable, RegistryPlugin {
+	public static String LOCATION = "location";
+	public static String READONLY = "readonly";
+	public static String NAME = "name";
+
+	File[] EMPTY_FILES = new File[0];
+	protected File root;
+	Registry registry;
+	boolean canWrite = true;
+	Pattern REPO_FILE = Pattern
+			.compile("([-a-zA-z0-9_\\.]+)-([0-9\\.]+|latest)\\.(jar|lib)");
+	Reporter reporter;
+	boolean dirty;
+	String name;
+
+	public FileRepo() {}
+	
+	public FileRepo(String name, File location, boolean canWrite) {
+		this.name = name;
+		this.root = location;
+		this.canWrite = canWrite;
+	}
+	
+	protected void init() throws Exception {
+		// for extensions
+	}
+	
+	public void setProperties(Map<String, String> map) {
+		String location = (String) map.get(LOCATION);
+		if (location == null)
+			throw new IllegalArgumentException(
+					"Location must be set on a FileRepo plugin");
+
+		root = new File(location);
+		if (!root.isDirectory())
+			throw new IllegalArgumentException(
+					"Repository is not a valid directory " + root);
+
+		String readonly = (String) map.get(READONLY);
+		if (readonly != null && Boolean.valueOf(readonly).booleanValue())
+			canWrite = false;
+
+		name = (String) map.get(NAME);
+	}
+
+	/**
+	 * Get a list of URLs to bundles that are constrained by the bsn and
+	 * versionRange.
+	 */
+	public File[] get(String bsn, String versionRange)
+			throws Exception {
+		init();
+		
+		// If the version is set to project, we assume it is not
+		// for us. A project repo will then get it.
+		if (versionRange != null && versionRange.equals("project"))
+			return null;
+
+		//
+		// Check if the entry exists
+		//
+		File f = new File(root, bsn);
+		if (!f.isDirectory())
+			return null;
+
+		//
+		// The version range we are looking for can
+		// be null (for all) or a version range.
+		//
+		VersionRange range;
+		if (versionRange == null || versionRange.equals("latest")) {
+			range = new VersionRange("0");
+		} else
+			range = new VersionRange(versionRange);
+
+		//
+		// Iterator over all the versions for this BSN.
+		// Create a sorted map over the version as key
+		// and the file as URL as value. Only versions
+		// that match the desired range are included in
+		// this list.
+		//
+		File instances[] = f.listFiles();
+		SortedMap<Version, File> versions = new TreeMap<Version, File>();
+		for (int i = 0; i < instances.length; i++) {
+			Matcher m = REPO_FILE.matcher(instances[i].getName());
+			if (m.matches() && m.group(1).equals(bsn)) {
+				String versionString = m.group(2);
+				Version version;
+				if (versionString.equals("latest"))
+					version = new Version(Integer.MAX_VALUE);
+				else
+					version = new Version(versionString);
+
+				if (range.includes(version)
+						|| versionString.equals(versionRange))
+					versions.put(version, instances[i]);
+			}
+		}
+
+		File[] files = (File[]) versions.values().toArray(EMPTY_FILES);
+		if ("latest".equals(versionRange) && files.length > 0) {
+			return new File[] { files[files.length - 1] };
+		}
+		return files;
+	}
+
+	public boolean canWrite() {
+		return canWrite;
+	}
+
+	public File put(Jar jar) throws Exception {
+		init();
+		dirty = true;
+
+		Manifest manifest = jar.getManifest();
+		if (manifest == null)
+			throw new IllegalArgumentException("No manifest in JAR: " + jar);
+
+		String bsn = manifest.getMainAttributes().getValue(
+				Analyzer.BUNDLE_SYMBOLICNAME);
+		if (bsn == null)
+			throw new IllegalArgumentException("No Bundle SymbolicName set");
+
+		Map<String, Map<String, String>> b = Processor.parseHeader(bsn, null);
+		if (b.size() != 1)
+			throw new IllegalArgumentException("Multiple bsn's specified " + b);
+
+		for (String key : b.keySet()) {
+			bsn = key;
+			if (!Verifier.SYMBOLICNAME.matcher(bsn).matches())
+				throw new IllegalArgumentException(
+						"Bundle SymbolicName has wrong format: " + bsn);
+		}
+
+		String versionString = manifest.getMainAttributes().getValue(
+				Analyzer.BUNDLE_VERSION);
+		Version version;
+		if (versionString == null)
+			version = new Version();
+		else
+			version = new Version(versionString);
+
+		File dir = new File(root, bsn);
+		dir.mkdirs();
+		String fName = bsn + "-" + version.getMajor() + "."
+				+ version.getMinor() + "." + version.getMicro() + ".jar";
+		File file = new File(dir, fName);
+
+		reporter.trace("Updating " + file.getAbsolutePath());
+		if (!file.exists() || file.lastModified() < jar.lastModified()) {
+			jar.write(file);
+			reporter.progress("Updated " + file.getAbsolutePath());
+			fireBundleAdded(jar, file);
+		} else {
+			reporter.progress("Did not update " + jar
+					+ " because repo has a newer version");
+			reporter.trace("NOT Updating " + fName + " (repo is newer)");
+		}
+
+		File latest = new File(dir, bsn + "-latest.jar");
+		if (latest.exists() && latest.lastModified() < jar.lastModified()) {
+			jar.write(latest);
+			file = latest;
+		}
+		
+		return file;
+	}
+
+	protected void fireBundleAdded(Jar jar, File file) {
+		if (registry == null)
+			return;
+		List<RepositoryListenerPlugin> listeners = registry.getPlugins(RepositoryListenerPlugin.class);
+		for (RepositoryListenerPlugin listener : listeners) {
+			try {
+				listener.bundleAdded(this, jar, file);
+			} catch (Exception e) {
+				if (reporter != null)
+					reporter.warning("Repository listener threw an unexpected exception: %s", e);
+			}
+		}
+	}
+
+	public void setLocation(String string) {
+		root = new File(string);
+		if (!root.isDirectory())
+			throw new IllegalArgumentException("Invalid repository directory");
+	}
+
+	public void setReporter(Reporter reporter) {
+		this.reporter = reporter;
+	}
+
+	public List<String> list(String regex) throws Exception {
+		init();
+		Instruction pattern = null;
+		if (regex != null)
+			pattern = Instruction.getPattern(regex);
+
+		List<String> result = new ArrayList<String>();
+		if (root == null) {
+			if (reporter != null) reporter.error("FileRepo root directory is not set.");
+		} else {
+			File[] list = root.listFiles();
+			if (list != null) {
+				for (File f : list) {
+					if (!f.isDirectory()) continue; // ignore non-directories
+					String fileName = f.getName();
+					if (fileName.charAt(0) == '.') continue; // ignore hidden files
+					if (pattern == null || pattern.matches(fileName))
+						result.add(fileName);
+				}
+			} else 
+				if ( reporter != null)
+					reporter.error("FileRepo root directory (%s) does not exist", root);
+		}
+
+		return result;
+	}
+
+	public List<Version> versions(String bsn) throws Exception {
+		init();
+		File dir = new File(root, bsn);
+		if (dir.isDirectory()) {
+			String versions[] = dir.list();
+			List<Version> list = new ArrayList<Version>();
+			for (String v : versions) {
+				Matcher m = REPO_FILE.matcher(v);
+				if (m.matches()) {
+					String version = m.group(2);
+					if (version.equals("latest"))
+						version = "99";
+					list.add(new Version(version));
+				}
+			}
+			return list;
+		}
+		return null;
+	}
+
+	public String toString() {
+		return String
+				.format("%-40s r/w=%s", root.getAbsolutePath(), canWrite());
+	}
+
+	public File getRoot() {
+		return root;
+	}
+
+	public boolean refresh() {
+		if (dirty) {
+			dirty = false;
+			return true;
+		} else
+			return false;
+	}
+
+	public String getName() {
+		if (name == null) {
+			return toString();
+		}
+		return name;
+	}
+	public File get(String bsn, String version, Strategy strategy, Map<String,String> properties) throws Exception {
+		if ( version == null)
+			version = "0.0.0";
+		
+		if ( strategy == Strategy.EXACT) {				
+			VersionRange vr = new VersionRange(version);
+			if ( vr.isRange())
+				return null;
+			
+			File file = IO.getFile(root, bsn + "/" + version +"/" + bsn + "-" + version + ".jar");
+			if ( file.isFile())
+				return file;
+			else
+				return null;
+
+		}
+		File[] files = get(bsn, version);
+		if ( files == null || files.length == 0)
+			return null;
+		
+		if (files.length >= 0) {
+			switch (strategy) {
+			case LOWEST:
+				return files[0];
+			case HIGHEST:
+				return files[files.length - 1];
+			}
+		}
+		return null;
+	}
+
+	public void setRegistry(Registry registry) {
+		this.registry = registry;
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/AbstractBaseOBR.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/AbstractBaseOBR.java
new file mode 100644
index 0000000..c6a1af5
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/AbstractBaseOBR.java
@@ -0,0 +1,564 @@
+package aQute.lib.deployer.obr;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.SAXException;
+
+import aQute.bnd.build.ResolverMode;
+import aQute.bnd.service.OBRIndexProvider;
+import aQute.bnd.service.OBRResolutionMode;
+import aQute.bnd.service.Plugin;
+import aQute.bnd.service.Registry;
+import aQute.bnd.service.RegistryPlugin;
+import aQute.bnd.service.RemoteRepositoryPlugin;
+import aQute.bnd.service.ResourceHandle;
+import aQute.bnd.service.ResourceHandle.Location;
+import aQute.lib.filter.Filter;
+import aQute.lib.osgi.Jar;
+import aQute.libg.generics.Create;
+import aQute.libg.reporter.Reporter;
+import aQute.libg.version.Version;
+import aQute.libg.version.VersionRange;
+
+/**
+ * Abstract base class for OBR-based repositories.
+ * 
+ * <p>
+ * The repository implementation is read-only by default. To implement a writable
+ * repository, subclasses should override {@link #canWrite()} and {@link #put(Jar)}.
+ * 
+ * @author Neil Bartlett
+ *
+ */
+public abstract class AbstractBaseOBR implements RegistryPlugin, Plugin, RemoteRepositoryPlugin, OBRIndexProvider {
+	
+	public static final String PROP_NAME = "name";
+	public static final String PROP_RESOLUTION_MODE = "mode";
+	public static final String PROP_RESOLUTION_MODE_ANY = "any";
+	
+	public static final String REPOSITORY_FILE_NAME = "repository.xml";
+	
+	protected Registry registry;
+	protected Reporter reporter;
+	protected String name = this.getClass().getName();
+	protected Set<OBRResolutionMode> supportedModes = EnumSet.allOf(OBRResolutionMode.class);
+
+	private boolean initialised = false;
+	private final Map<String, SortedMap<Version, Resource>> pkgResourceMap = new HashMap<String, SortedMap<Version, Resource>>();
+	private final Map<String, SortedMap<Version, Resource>> bsnMap = new HashMap<String, SortedMap<Version, Resource>>();
+	
+	protected abstract File getCacheDirectory();
+
+	protected void addResourceToIndex(Resource resource) {
+		addBundleSymbolicNameToIndex(resource);
+		addPackagesToIndex(resource);
+	}
+
+	protected synchronized void reset() {
+		initialised = false;
+	}
+
+	/**
+	 * Initialise the indexes prior to main initialisation of internal
+	 * data structures. This implementation does nothing, but subclasses
+	 * may override if they need to perform such initialisation.
+	 * @throws Exception 
+	 */
+	protected void initialiseIndexes() throws Exception {
+	}
+
+	protected final synchronized void init() throws Exception {
+		if (!initialised) {
+			bsnMap.clear();
+			pkgResourceMap.clear();
+			
+			initialiseIndexes();
+			
+			IResourceListener listener = new IResourceListener() {
+				public boolean processResource(Resource resource) {
+					addResourceToIndex(resource);
+					return true;
+				}
+			};
+			Collection<URL> indexes = getOBRIndexes();
+			for (URL indexLocation : indexes) {
+				try {
+					InputStream stream = indexLocation.openStream();
+					readIndex(indexLocation.toString(), stream, listener);
+				} catch (Exception e) {
+					e.printStackTrace();
+					reporter.error("Unable to read index at URL '%s'.", indexLocation);
+				}
+			}
+			
+			initialised = true;
+		}
+	}
+	
+	public void setRegistry(Registry registry) {
+		this.registry = registry;
+	}
+
+	public void setProperties(Map<String, String> map) {
+		if (map.containsKey(PROP_NAME))
+			name = map.get(PROP_NAME);
+		
+		if (map.containsKey(PROP_RESOLUTION_MODE)) {
+			supportedModes = EnumSet.noneOf(OBRResolutionMode.class);
+			StringTokenizer tokenizer = new StringTokenizer(map.get(PROP_RESOLUTION_MODE), ",");
+			while (tokenizer.hasMoreTokens()) {
+				String token = tokenizer.nextToken().trim();
+				if (PROP_RESOLUTION_MODE_ANY.equalsIgnoreCase(token))
+					supportedModes = EnumSet.allOf(OBRResolutionMode.class);
+				else {
+					try {
+						supportedModes.add(OBRResolutionMode.valueOf(token));
+					} catch (Exception e) {
+						if (reporter != null) reporter.error("Unknown OBR resolution mode: " + token);
+					}
+				}
+			}
+		}
+	}
+	
+	public File[] get(String bsn, String range) throws Exception {
+		ResourceHandle[] handles = getHandles(bsn, range);
+		
+		return requestAll(handles);
+	}
+	
+	protected static File[] requestAll(ResourceHandle[] handles) throws IOException {
+		File[] result = (handles == null) ? new File[0] : new File[handles.length];
+		for (int i = 0; i < result.length; i++) {
+			result[i] = handles[i].request();
+		}
+		return result;
+	}
+
+	protected ResourceHandle[] getHandles(String bsn, String rangeStr) throws Exception {
+		init();
+		
+		// If the range is set to "project", we cannot resolve it.
+		if ("project".equals(rangeStr))
+			return null;
+		
+		
+		SortedMap<Version, Resource> versionMap = bsnMap.get(bsn);
+		if (versionMap == null || versionMap.isEmpty())
+			return null;
+		List<Resource> resources = narrowVersionsByVersionRange(versionMap, rangeStr);
+		List<ResourceHandle> handles = mapResourcesToHandles(resources);
+		
+		return (ResourceHandle[]) handles.toArray(new ResourceHandle[handles.size()]);
+	}
+	
+	public void setReporter(Reporter reporter) {
+		this.reporter = reporter;
+	}
+	
+	public File get(String bsn, String range, Strategy strategy, Map<String, String> properties) throws Exception {
+		ResourceHandle handle = getHandle(bsn, range, strategy, properties);
+		return handle != null ? handle.request() : null;
+	}
+	
+	public ResourceHandle getHandle(String bsn, String range, Strategy strategy, Map<String, String> properties) throws Exception {
+		ResourceHandle result;
+		if (bsn != null)
+			result = resolveBundle(bsn, range, strategy);
+		else {
+			String pkgName = properties.get(CapabilityType.PACKAGE.getTypeName());
+			
+			String modeName = properties.get(CapabilityType.MODE.getTypeName());
+			ResolverMode mode = (modeName != null) ? ResolverMode.valueOf(modeName) : null;
+			
+			if (pkgName != null)
+				result = resolvePackage(pkgName, range, strategy, mode, properties);
+			else
+				throw new IllegalArgumentException("Cannot resolve bundle: neither bsn nor package specified.");
+		}
+		return result;
+	}
+
+	public boolean canWrite() {
+		return false;
+	}
+
+	public File put(Jar jar) throws Exception {
+		throw new UnsupportedOperationException("Read-only repository.");
+	}
+
+	public List<String> list(String regex) throws Exception {
+		init();
+		Pattern pattern = regex != null ? Pattern.compile(regex) : null;
+		List<String> result = new LinkedList<String>();
+		
+		for (String bsn : bsnMap.keySet()) {
+			if (pattern == null || pattern.matcher(bsn).matches())
+				result.add(bsn);
+		}
+		
+		return result;
+	}
+
+	public List<Version> versions(String bsn) throws Exception {
+		init();
+		SortedMap<Version, Resource> versionMap = bsnMap.get(bsn);
+		List<Version> list;
+		if (versionMap != null) {
+			list = new ArrayList<Version>(versionMap.size());
+			list.addAll(versionMap.keySet());
+		} else {
+			list = Collections.emptyList();
+		}
+		return list;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	void addBundleSymbolicNameToIndex(Resource resource) {
+		String bsn = resource.getSymbolicName();
+		Version version;
+		String versionStr = resource.getVersion();
+		try {
+			version = new Version(versionStr);
+		} catch (Exception e) {
+			version = new Version("0.0.0");
+		}
+		SortedMap<Version, Resource> versionMap = bsnMap.get(bsn);
+		if (versionMap == null) {
+			versionMap = new TreeMap<Version, Resource>();
+			bsnMap.put(bsn, versionMap);
+		}
+		versionMap.put(version, resource);
+	}
+
+	void addPackagesToIndex(Resource resource) {
+		for (Capability capability : resource.getCapabilities()) {
+			if (CapabilityType.PACKAGE.getTypeName().equals(capability.getName())) {
+				String pkgName = null;
+				String versionStr = null;
+				
+				for (Property prop : capability.getProperties()) {
+					if (Property.PACKAGE.equals(prop.getName()))
+						pkgName = prop.getValue();
+					else if (Property.VERSION.equals(prop.getName()))
+						versionStr = prop.getValue();
+				}
+				
+				Version version;
+				try {
+					version = new Version(versionStr);
+				} catch (Exception e) {
+					version = new Version("0.0.0");
+				}
+				
+				if (pkgName != null) {
+					SortedMap<Version, Resource> versionMap = pkgResourceMap.get(pkgName);
+					if (versionMap == null) {
+						versionMap = new TreeMap<Version, Resource>();
+						pkgResourceMap.put(pkgName, versionMap);
+					}
+					versionMap.put(version, resource);
+				}
+			}
+		}
+	}
+
+	/**
+	 * @return Whether to continue parsing other indexes
+	 * @throws IOException 
+	 */
+	boolean readIndex(String baseUrl, InputStream stream, IResourceListener listener) throws ParserConfigurationException, SAXException, IOException {
+		SAXParserFactory parserFactory = SAXParserFactory.newInstance();
+		SAXParser parser = parserFactory.newSAXParser();
+		try {
+			parser.parse(stream, new OBRSAXHandler(baseUrl, listener));
+			return true;
+		} catch (StopParseException e) {
+			return false;
+		} finally {
+			stream.close();
+		}
+	}
+
+	List<Resource> narrowVersionsByFilter(String pkgName, SortedMap<Version, Resource> versionMap, Filter filter) {
+		List<Resource> result = new ArrayList<Resource>(versionMap.size());
+		
+		Dictionary<String, String> dict = new Hashtable<String, String>();
+		dict.put("package", pkgName);
+		
+		for (Version version : versionMap.keySet()) {
+			dict.put("version", version.toString());
+			if (filter.match(dict))
+				result.add(versionMap.get(version));
+		}
+		
+		return result;
+	}
+
+	List<Resource> narrowVersionsByVersionRange(SortedMap<Version, Resource> versionMap, String rangeStr) {
+		List<Resource> result;
+		if ("latest".equals(rangeStr)) {
+			Version highest = versionMap.lastKey();
+			result = Create.list(new Resource[] { versionMap.get(highest) });
+		} else {
+			VersionRange range = rangeStr != null ? new VersionRange(rangeStr) : null;
+			
+			// optimisation: skip versions definitely less than the range
+			if (range != null && range.getLow() != null)
+				versionMap = versionMap.tailMap(range.getLow());
+			
+			result = new ArrayList<Resource>(versionMap.size());
+			for (Version version : versionMap.keySet()) {
+				if (range == null || range.includes(version))
+					result.add(versionMap.get(version));
+				
+				// optimisation: skip versions definitely higher than the range
+				if (range != null && range.isRange() && version.compareTo(range.getHigh()) >= 0)
+					break;
+			}
+		}
+		return result;
+	}
+	
+	void filterResourcesByResolverMode(Collection<Resource> resources, ResolverMode mode) {
+		assert mode != null;
+		
+		Properties modeCapability = new Properties();
+		modeCapability.setProperty(CapabilityType.MODE.getTypeName(), mode.name());
+		
+		for (Iterator<Resource> iter = resources.iterator(); iter.hasNext(); ) {
+			Resource resource = iter.next();
+			
+			Require modeRequire = resource.findRequire(CapabilityType.MODE.getTypeName());
+			if (modeRequire == null)
+				continue;
+			else if (modeRequire.getFilter() == null)
+				iter.remove();
+			else {
+				try {
+					Filter filter = new Filter(modeRequire.getFilter());
+					if (!filter.match(modeCapability))
+						iter.remove();
+				} catch (IllegalArgumentException e) {
+					if (reporter != null)
+						reporter.error("Error parsing mode filter requirement on resource %s: %s", resource.getUrl(), modeRequire.getFilter());
+					iter.remove();
+				}
+			}
+		}
+	}
+	
+	List<ResourceHandle> mapResourcesToHandles(Collection<Resource> resources) throws Exception {
+		List<ResourceHandle> result = new ArrayList<ResourceHandle>(resources.size());
+		
+		for (Resource resource : resources) {
+			ResourceHandle handle = mapResourceToHandle(resource);
+			if (handle != null)
+				result.add(handle);
+		}
+		
+		return result;
+	}
+	
+	ResourceHandle mapResourceToHandle(Resource resource) throws Exception {
+		ResourceHandle result = null;
+		
+		URLResourceHandle handle ;
+		try {
+			handle = new URLResourceHandle(resource.getUrl(), resource.getBaseUrl(), getCacheDirectory());
+		} catch (FileNotFoundException e) {
+			throw new FileNotFoundException("Broken link in repository index: " + e.getMessage());
+		}
+		if (handle.getLocation() == Location.local || getCacheDirectory() != null)
+			result = handle;
+		
+		return result;
+	}
+
+	ResourceHandle resolveBundle(String bsn, String rangeStr, Strategy strategy) throws Exception {
+		if (rangeStr == null) rangeStr = "0.0.0";
+		
+		if (strategy == Strategy.EXACT) {
+			return findExactMatch(bsn, rangeStr, bsnMap);
+		}
+		
+		ResourceHandle[] handles = getHandles(bsn, rangeStr);
+		ResourceHandle selected;
+		if (handles == null || handles.length == 0)
+			selected = null;
+		else {
+			switch(strategy) {
+			case LOWEST:
+				selected = handles[0];
+				break;
+			default:
+				selected = handles[handles.length - 1];
+			}
+		}
+		return selected;
+	}
+
+	ResourceHandle resolvePackage(String pkgName, String rangeStr, Strategy strategy, ResolverMode mode, Map<String, String> props) throws Exception {
+		init();
+		if (rangeStr == null) rangeStr = "0.0.0";
+		
+		SortedMap<Version, Resource> versionMap = pkgResourceMap.get(pkgName);
+		if (versionMap == null)
+			return null;
+		
+		// Was a filter expression supplied?
+		Filter filter = null;
+		String filterStr = props.get("filter");
+		if (filterStr != null) {
+			filter = new Filter(filterStr);
+		}
+		
+		// Narrow the resources by version range string or filter.
+		List<Resource> resources;
+		if (filter != null)
+			resources = narrowVersionsByFilter(pkgName, versionMap, filter);
+		else
+			resources = narrowVersionsByVersionRange(versionMap, rangeStr);
+		
+		// Remove resources that are invalid for the current resolution mode
+		if (mode != null)
+			filterResourcesByResolverMode(resources, mode);
+		
+		// Select the most suitable one
+		Resource selected;
+		if (resources == null || resources.isEmpty())
+			selected = null;
+		else {
+			switch (strategy) {
+			case LOWEST:
+				selected = resources.get(0);
+				break;
+			default:
+				selected = resources.get(resources.size() - 1);
+			}
+			expandPackageUses(pkgName, selected, props);
+		}
+		return selected != null ? mapResourceToHandle(selected) : null;
+	}
+
+	void expandPackageUses(String pkgName, Resource resource, Map<String, String> props) {
+		List<String> internalUses = new LinkedList<String>();
+		Map<String, Require> externalUses = new HashMap<String, Require>();
+		
+		internalUses.add(pkgName);
+		
+		Capability capability = resource.findPackageCapability(pkgName);
+		Property usesProp = capability.findProperty(Property.USES);
+		if (usesProp != null) {
+			StringTokenizer tokenizer = new StringTokenizer(usesProp.getValue(), ",");
+			while (tokenizer.hasMoreTokens()) {
+				String usesPkgName = tokenizer.nextToken();
+				Capability usesPkgCap = resource.findPackageCapability(usesPkgName);
+				if (usesPkgCap != null)
+					internalUses.add(usesPkgName);
+				else {
+					Require require = resource.findPackageRequire(usesPkgName);
+					if (require != null)
+						externalUses.put(usesPkgName, require);
+				}
+			}
+		}
+		props.put("packages", listToString(internalUses));
+		props.put("import-uses", formatPackageRequires(externalUses));
+	}
+	
+	String listToString(List<?> list) {
+		StringBuilder builder = new StringBuilder();
+		
+		int count = 0;
+		for (Object item : list) {
+			if (count++ > 0) builder.append(',');
+			builder.append(item);
+		}
+		
+		return builder.toString();
+	}
+
+	String formatPackageRequires(Map<String, Require> externalUses) {
+		StringBuilder builder = new StringBuilder();
+		
+		int count = 0;
+		for (Entry<String, Require> entry : externalUses.entrySet()) {
+			String pkgName = entry.getKey();
+			String filter = entry.getValue().getFilter();
+
+			if (count++ > 0)
+				builder.append(',');
+			builder.append(pkgName);
+			builder.append(";filter='");
+			builder.append(filter);
+			builder.append('\'');
+		}
+		
+		return builder.toString();
+	}
+
+	ResourceHandle findExactMatch(String identity, String version, Map<String, SortedMap<Version, Resource>> resourceMap) throws Exception {
+		Resource resource;
+		VersionRange range = new VersionRange(version);
+		if (range.isRange())
+			return null;
+		
+		SortedMap<Version, Resource> versions = resourceMap.get(identity);
+		resource = versions.get(range.getLow());
+		
+		return mapResourceToHandle(resource);
+	}
+	
+	/**
+	 * Utility function for parsing lists of URLs.
+	 * 
+	 * @param locationsStr
+	 *            Comma-separated list of URLs
+	 * @throws MalformedURLException
+	 */
+	protected static List<URL> parseLocations(String locationsStr) throws MalformedURLException {
+		StringTokenizer tok = new StringTokenizer(locationsStr, ",");
+		List<URL> urls = new ArrayList<URL>(tok.countTokens());
+		while (tok.hasMoreTokens()) {
+			String urlStr = tok.nextToken().trim();
+			urls.add(new URL(urlStr));
+		}
+		return urls;
+	}
+
+	public Set<OBRResolutionMode> getSupportedModes() {
+		return supportedModes;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/Capability.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Capability.java
new file mode 100644
index 0000000..983afed
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Capability.java
@@ -0,0 +1,93 @@
+package aQute.lib.deployer.obr;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+public class Capability {
+	
+	private final String name;
+	private final List<Property> properties;
+
+	private Capability(String name, List<Property> properties) {
+		this.name = name;
+		this.properties = properties;
+	}
+	
+	public static class Builder {
+		private String name;
+		private final List<Property> properties = new LinkedList<Property>();
+		
+		public Builder setName(String name) {
+			this.name = name;
+			return this;
+		}
+		
+		public Builder addProperty(Property property) {
+			this.properties.add(property);
+			return this;
+		}
+		
+		public Capability build() {
+			if (name == null) throw new IllegalStateException("'name' field is not initialised.");
+			return new Capability(name, Collections.unmodifiableList(properties));
+		}
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public List<Property> getProperties() {
+		return properties;
+	}
+	
+	public Property findProperty(String propertyName) {
+		assert propertyName != null;
+		for (Property prop : properties) {
+			if (propertyName.equals(prop.getName()))
+				return prop;
+		}
+		return null;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder builder = new StringBuilder();
+		builder.append("Capability [name=").append(name).append(", properties=").append(properties).append("]");
+		return builder.toString();
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((name == null) ? 0 : name.hashCode());
+		result = prime * result
+				+ ((properties == null) ? 0 : properties.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Capability other = (Capability) obj;
+		if (name == null) {
+			if (other.name != null)
+				return false;
+		} else if (!name.equals(other.name))
+			return false;
+		if (properties == null) {
+			if (other.properties != null)
+				return false;
+		} else if (!properties.equals(other.properties))
+			return false;
+		return true;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/CapabilityType.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/CapabilityType.java
new file mode 100644
index 0000000..d3f8ae3
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/CapabilityType.java
@@ -0,0 +1,31 @@
+package aQute.lib.deployer.obr;
+
+public enum CapabilityType {
+	
+	PACKAGE("package"),
+	EE("ee"),
+	BUNDLE("bundle"),
+	MODE("mode"),
+	OTHER(null);
+	
+	private String typeName;
+
+	CapabilityType(String name) {
+		this.typeName = name;
+	}
+	
+	public String getTypeName() {
+		return typeName;
+	}
+	
+	/**
+	 * @throws IllegalArgumentException
+	 */
+	public static CapabilityType getForTypeName(String typeName) {
+		for (CapabilityType type : CapabilityType.values()) {
+			if (type.typeName != null && type.typeName.equals(typeName))
+				return type;
+		}
+		throw new IllegalArgumentException("Unknown capability type: " + typeName);
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/IResourceListener.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/IResourceListener.java
new file mode 100644
index 0000000..22dae7a
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/IResourceListener.java
@@ -0,0 +1,15 @@
+package aQute.lib.deployer.obr;
+
+public interface IResourceListener {
+	/**
+	 * Process an OBR resource descriptor from the index document, and possibly
+	 * request early termination of the parser.
+	 * 
+	 * @param resource
+	 *            The resource descriptor to be processed.
+	 * @return Whether to continue parsing the document; returning {@code false}
+	 *         will result in the parser being stopped with a
+	 *         {@link StopParseException}.
+	 */
+	boolean processResource(Resource resource);
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/LocalOBR.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/LocalOBR.java
new file mode 100644
index 0000000..e59266a
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/LocalOBR.java
@@ -0,0 +1,198 @@
+package aQute.lib.deployer.obr;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.transform.stream.StreamResult;
+
+import org.osgi.service.bindex.BundleIndexer;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+
+import aQute.bnd.service.Refreshable;
+import aQute.bnd.service.Registry;
+import aQute.bnd.service.RegistryPlugin;
+import aQute.lib.deployer.FileRepo;
+import aQute.lib.io.IO;
+import aQute.lib.osgi.Jar;
+import aQute.libg.reporter.Reporter;
+import aQute.libg.sax.SAXUtil;
+import aQute.libg.sax.filters.MergeContentFilter;
+import aQute.libg.version.Version;
+
+public class LocalOBR extends OBR implements Refreshable, RegistryPlugin {
+	
+	public static final String PROP_LOCAL_DIR = "local";
+	public static final String PROP_READONLY = "readonly";
+
+	private final FileRepo storageRepo = new FileRepo();
+	
+	private Registry registry;
+	private File storageDir;
+	private File localIndex;
+	
+	private List<URL> indexUrls;
+	
+	public void setRegistry(Registry registry) {
+		this.registry = registry;
+	}
+	
+	@Override
+	public void setReporter(Reporter reporter) {
+		super.setReporter(reporter);
+		storageRepo.setReporter(reporter);
+	}
+
+	@Override
+	public void setProperties(Map<String, String> map) {
+		super.setProperties(map);
+		
+		// Load essential properties
+		String localDirPath = map.get(PROP_LOCAL_DIR);
+		if (localDirPath == null)
+			throw new IllegalArgumentException(String.format("Attribute '%s' must be set on LocalOBR plugin.", PROP_LOCAL_DIR));
+		storageDir = new File(localDirPath);
+		if (!storageDir.isDirectory())
+			throw new IllegalArgumentException(String.format("Local path '%s' does not exist or is not a directory.", localDirPath));
+		
+		// Configure the storage repository
+		Map<String, String> storageRepoConfig = new HashMap<String, String>(2);
+		storageRepoConfig.put(FileRepo.LOCATION, localDirPath);
+		storageRepoConfig.put(FileRepo.READONLY, map.get(PROP_READONLY));
+		storageRepo.setProperties(storageRepoConfig);
+		
+		// Set the local index and cache directory locations
+		localIndex = new File(storageDir, REPOSITORY_FILE_NAME);
+		if (localIndex.exists() && !localIndex.isFile())
+			throw new IllegalArgumentException(String.format("Cannot build local repository index: '%s' already exists but is not a plain file.", localIndex.getAbsolutePath()));
+		cacheDir = new File(storageDir, ".obrcache");
+		if (cacheDir.exists() && !cacheDir.isDirectory())
+			throw new IllegalArgumentException(String.format("Cannot create repository cache: '%s' already exists but is not directory.", cacheDir.getAbsolutePath()));
+	}
+	
+	@Override
+	protected void initialiseIndexes() throws Exception {
+		if (!localIndex.exists()) {
+			regenerateIndex();
+		}
+		try {
+			Collection<URL> remotes = super.getOBRIndexes();
+			indexUrls = new ArrayList<URL>(remotes.size() + 1);
+			indexUrls.add(localIndex.toURI().toURL());
+			indexUrls.addAll(remotes);
+		} catch (IOException e) {
+			throw new IllegalArgumentException("Error initialising local index URL", e);
+		}
+	}
+	
+	private void regenerateIndex() throws Exception {
+		BundleIndexer indexer = registry.getPlugin(BundleIndexer.class);
+		if (indexer == null)
+			throw new IllegalStateException("Cannot index repository: no Bundle Indexer service or plugin found.");
+		
+		Set<File> allFiles = new HashSet<File>();
+		gatherFiles(allFiles);
+		
+		FileOutputStream out = null;
+		try {
+			out = new FileOutputStream(localIndex);
+			if (!allFiles.isEmpty()) {
+				Map<String, String> config = new HashMap<String, String>();
+				config.put(BundleIndexer.REPOSITORY_NAME, this.getName());
+				config.put(BundleIndexer.ROOT_URL, localIndex.toURI().toURL().toString());
+				indexer.index(allFiles, out, config);
+			} else {
+				ByteArrayInputStream emptyRepo = new ByteArrayInputStream("<?xml version='1.0' encoding='UTF-8'?>\n<repository lastmodified='0'/>".getBytes());
+				IO.copy(emptyRepo, out);
+			}
+		} finally {
+			out.close();
+		}
+	}
+
+	private void gatherFiles(Set<File> allFiles) throws Exception {
+		List<String> bsns = storageRepo.list(null);
+		if (bsns != null) for (String bsn : bsns) {
+			List<Version> versions = storageRepo.versions(bsn);
+			if (versions != null) for (Version version : versions) {
+				File file = storageRepo.get(bsn, version.toString(), Strategy.HIGHEST, null);
+				if (file != null)
+					allFiles.add(file);
+			}
+		}
+	}
+
+	@Override
+	public List<URL> getOBRIndexes() {
+		return indexUrls;
+	}
+	
+	@Override
+	public boolean canWrite() {
+		return storageRepo.canWrite();
+	}
+	
+	@Override
+	public synchronized File put(Jar jar) throws Exception {
+		File newFile = storageRepo.put(jar);
+		
+		// Index the new file
+		BundleIndexer indexer = registry.getPlugin(BundleIndexer.class);
+		if (indexer == null)
+			throw new IllegalStateException("Cannot index repository: no Bundle Indexer service or plugin found.");
+		ByteArrayOutputStream newIndexBuffer = new ByteArrayOutputStream();
+		indexer.index(Collections.singleton(newFile), newIndexBuffer, null);
+		
+		// Merge into main index
+		File tempIndex = File.createTempFile("repository", ".xml");
+		FileOutputStream tempIndexOutput = new FileOutputStream(tempIndex);
+		MergeContentFilter merger = new MergeContentFilter();
+		XMLReader reader = SAXUtil.buildPipeline(new StreamResult(tempIndexOutput), new UniqueResourceFilter(), merger);
+		
+		try {
+			// Parse the newly generated index
+			reader.parse(new InputSource(new ByteArrayInputStream(newIndexBuffer.toByteArray())));
+			
+			// Parse the existing index (which may be empty/missing)
+			try {
+				reader.parse(new InputSource(new FileInputStream(localIndex)));
+			} catch (Exception e) {
+				reporter.warning("Existing local index is invalid or missing, overwriting (%s).", localIndex.getAbsolutePath());
+			}
+			
+			merger.closeRootAndDocument();
+		} finally {
+			tempIndexOutput.flush();
+			tempIndexOutput.close();
+		}
+		IO.copy(tempIndex, localIndex);
+		
+		// Re-read the index
+		reset();
+		init();
+		
+		return newFile;
+	}
+
+	public boolean refresh() {
+		reset();
+		return true;
+	}
+
+	public File getRoot() {
+		return storageDir;
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBR.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBR.java
new file mode 100644
index 0000000..ddf5fcc
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBR.java
@@ -0,0 +1,120 @@
+package aQute.lib.deployer.obr;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import aQute.lib.deployer.FileRepo;
+
+/**
+ * A simple read-only OBR-based repository that uses a list of index locations
+ * and a basic local cache.
+ * 
+ * <p>
+ * <h2>Properties</h2>
+ * <ul>
+ * <li><b>locations:</b> comma-separated list of index URLs. <b>NB:</b> surround with single quotes!</li>
+ * <li><b>name:</b> repository name; defaults to the index URLs.
+ * <li><b>cache:</b> local cache directory. May be omitted, in which case the repository will only be
+ * able to serve resources with {@code file:} URLs.</li>
+ * <li><b>location:</b> (deprecated) alias for "locations".
+ * </ul>
+ * 
+ * <p>
+ * <h2>Example</h2>
+ * 
+ * <pre>
+ * -plugin: aQute.lib.deployer.obr.OBR;locations='http://www.example.com/repository.xml';cache=${workspace}/.cache
+ * </pre>
+ * 
+ * @author Neil Bartlett
+ *
+ */
+public class OBR extends AbstractBaseOBR {
+	
+	public static final String PROP_LOCATIONS = "locations";
+	@Deprecated
+	public static final String PROP_LOCATION = "location";
+	public static final String PROP_CACHE = "cache";
+
+	protected List<URL> locations;
+	protected File cacheDir;
+
+	public void setProperties(Map<String, String> map) {
+		super.setProperties(map);
+		
+		String locationsStr = map.get(PROP_LOCATIONS);
+		// backwards compatibility
+		if (locationsStr == null) locationsStr = map.get(PROP_LOCATION);
+		
+		try {
+			if (locationsStr != null)
+				locations = parseLocations(locationsStr);
+			else
+				locations = Collections.emptyList();
+		} catch (MalformedURLException e) {
+			throw new IllegalArgumentException(String.format("Invalid location, unable to parse as URL list: %s", locationsStr), e);
+		}
+		
+		String cacheDirStr = map.get(PROP_CACHE);
+		if (cacheDirStr != null)
+			cacheDir = new File(cacheDirStr);
+	}
+	
+	private FileRepo lookupCachedFileRepo() {
+		if (registry != null) {
+			List<FileRepo> repos = registry.getPlugins(FileRepo.class);
+			for (FileRepo repo : repos) {
+				if ("cache".equals(repo.getName()))
+					return repo;
+			}
+		}
+		return null;
+	}
+
+	public List<URL> getOBRIndexes() {
+		return locations;
+	}
+
+	@Override
+	public synchronized File getCacheDirectory() {
+		if (cacheDir == null) {
+			FileRepo cacheRepo = lookupCachedFileRepo();
+			if (cacheRepo != null) {
+				File temp = new File(cacheRepo.getRoot(), ".obr");
+				temp.mkdirs();
+				if (temp.exists())
+					cacheDir = temp;
+			}
+		}
+		return cacheDir;
+	}
+	
+	public void setCacheDirectory(File cacheDir) {
+		this.cacheDir = cacheDir;
+	}
+	
+	@Override
+	public String getName() {
+		if (name != null && name != this.getClass().getName())
+			return name;
+		
+		StringBuilder builder = new StringBuilder();
+		
+		int count = 0;
+		for (URL location : locations) {
+			if (count++ > 0 ) builder.append(',');
+			builder.append(location);
+		}
+		return builder.toString();
+	}
+
+	public void setLocations(URL[] urls) {
+		this.locations = Arrays.asList(urls);
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBRSAXHandler.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBRSAXHandler.java
new file mode 100644
index 0000000..3634cb9
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/OBRSAXHandler.java
@@ -0,0 +1,79 @@
+package aQute.lib.deployer.obr;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class OBRSAXHandler extends DefaultHandler {
+	
+	private static final String TAG_RESOURCE = "resource";
+	private static final String ATTR_RESOURCE_ID = "id";
+	private static final String ATTR_RESOURCE_PRESENTATION_NAME = "presentationname";
+	private static final String ATTR_RESOURCE_SYMBOLIC_NAME = "symbolicname";
+	private static final String ATTR_RESOURCE_URI = "uri";
+	private static final String ATTR_RESOURCE_VERSION = "version";
+	
+	private static final String TAG_CAPABILITY = "capability";
+	private static final String ATTR_CAPABILITY_NAME = "name";
+	
+	private static final String TAG_REQUIRE = "require";
+	private static final String ATTR_REQUIRE_NAME = "name";
+	private static final String ATTR_REQUIRE_FILTER = "filter";
+	private static final String ATTR_REQUIRE_OPTIONAL = "optional";
+	
+	private static final String TAG_PROPERTY = "p";
+	private static final String ATTR_PROPERTY_NAME = "n";
+	private static final String ATTR_PROPERTY_TYPE = "t";
+	private static final String ATTR_PROPERTY_VALUE = "v";
+
+	private final String baseUrl;
+	private final IResourceListener resourceListener;
+	
+	private Resource.Builder resourceBuilder = null;
+	private Capability.Builder capabilityBuilder = null;
+	private Require require = null;
+
+	public OBRSAXHandler(String baseUrl, IResourceListener listener) {
+		this.baseUrl = baseUrl;
+		this.resourceListener = listener;
+	}
+	
+
+	@Override
+	public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
+		if (TAG_RESOURCE.equals(qName)) {
+			resourceBuilder = new Resource.Builder()
+				.setId(atts.getValue(ATTR_RESOURCE_ID))
+				.setPresentationName(atts.getValue(ATTR_RESOURCE_PRESENTATION_NAME))
+				.setSymbolicName(atts.getValue(ATTR_RESOURCE_SYMBOLIC_NAME))
+				.setUrl(atts.getValue(ATTR_RESOURCE_URI))
+				.setVersion(atts.getValue(ATTR_RESOURCE_VERSION))
+				.setBaseUrl(baseUrl);
+		} else if (TAG_CAPABILITY.equals(qName)) {
+			capabilityBuilder = new Capability.Builder()
+				.setName(atts.getValue(ATTR_CAPABILITY_NAME));
+		} else if (TAG_REQUIRE.equals(qName)) {
+			require = new Require(atts.getValue(ATTR_REQUIRE_NAME), atts.getValue(ATTR_REQUIRE_FILTER), "true".equalsIgnoreCase(atts.getValue(ATTR_REQUIRE_OPTIONAL)));
+		} else if (TAG_PROPERTY.equals(qName)) {
+			Property p = new Property(atts.getValue(ATTR_PROPERTY_NAME), atts.getValue(ATTR_PROPERTY_TYPE), atts.getValue(ATTR_PROPERTY_VALUE));
+			if (capabilityBuilder != null)
+				capabilityBuilder.addProperty(p);
+		}
+	}
+
+	@Override
+	public void endElement(String uri, String localName, String qName) throws SAXException {
+		if (TAG_CAPABILITY.equals(qName)) {
+			resourceBuilder.addCapability(capabilityBuilder);
+			capabilityBuilder = null;
+		} else if (TAG_RESOURCE.equals(qName)) {
+			if (!resourceListener.processResource(resourceBuilder.build()))
+				throw new StopParseException();
+			resourceBuilder = null;
+		} else if (TAG_REQUIRE.equals(qName)) {
+			resourceBuilder.addRequire(require);
+			require = null;
+		}
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/Property.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Property.java
new file mode 100644
index 0000000..1eaaa8a
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Property.java
@@ -0,0 +1,79 @@
+package aQute.lib.deployer.obr;
+
+/**
+ * @immutable
+ * @author Neil Bartlett
+ */
+public class Property {
+	
+	static final String PACKAGE = "package";
+	static final String USES = "uses";
+	static final String VERSION = "version";
+
+	private final String name;
+	private final String type;
+	private final String value;
+
+	public Property(String name, String type, String value) {
+		this.name = name;
+		this.type = type;
+		this.value = value;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public String getValue() {
+		return value;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder builder = new StringBuilder();
+		builder.append("Property [name=").append(name).append(", type=").append(type).append(", value=").append(value).append("]");
+		return builder.toString();
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((name == null) ? 0 : name.hashCode());
+		result = prime * result + ((type == null) ? 0 : type.hashCode());
+		result = prime * result + ((value == null) ? 0 : value.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Property other = (Property) obj;
+		if (name == null) {
+			if (other.name != null)
+				return false;
+		} else if (!name.equals(other.name))
+			return false;
+		if (type == null) {
+			if (other.type != null)
+				return false;
+		} else if (!type.equals(other.type))
+			return false;
+		if (value == null) {
+			if (other.value != null)
+				return false;
+		} else if (!value.equals(other.value))
+			return false;
+		return true;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/Require.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Require.java
new file mode 100644
index 0000000..d8d0fbb
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Require.java
@@ -0,0 +1,73 @@
+package aQute.lib.deployer.obr;
+
+public class Require {
+
+	private final String name;
+	private final String filter;
+	private final boolean optional;
+
+	public Require(String name, String filter, boolean optional) {
+		this.name = name;
+		this.filter = filter;
+		this.optional = optional;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public String getFilter() {
+		return filter;
+	}
+
+	public boolean isOptional() {
+		return optional;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder builder = new StringBuilder();
+		builder.append("Require [");
+		if (name != null)
+			builder.append("name=").append(name).append(", ");
+		if (filter != null)
+			builder.append("filter=").append(filter).append(", ");
+		builder.append("optional=").append(optional).append("]");
+		return builder.toString();
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((filter == null) ? 0 : filter.hashCode());
+		result = prime * result + ((name == null) ? 0 : name.hashCode());
+		result = prime * result + (optional ? 1231 : 1237);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Require other = (Require) obj;
+		if (filter == null) {
+			if (other.filter != null)
+				return false;
+		} else if (!filter.equals(other.filter))
+			return false;
+		if (name == null) {
+			if (other.name != null)
+				return false;
+		} else if (!name.equals(other.name))
+			return false;
+		if (optional != other.optional)
+			return false;
+		return true;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/Resource.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Resource.java
new file mode 100644
index 0000000..a355326
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/Resource.java
@@ -0,0 +1,240 @@
+package aQute.lib.deployer.obr;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @immutable
+ * @author Neil Bartlett
+ */
+public class Resource {
+	
+	private final String id;
+	private final String presentationName;
+	private final String symbolicName;
+	private final String baseUrl;
+	private final String url;
+	private final String version;
+	private final List<Capability> capabilities;
+	private final List<Require> requires;
+
+	private Resource(String id, String presentationName, String symbolicName, String baseUrl, String url, String version, List<Capability> capabilities, List<Require> requires) {
+		this.id = id;
+		this.presentationName = presentationName;
+		this.symbolicName = symbolicName;
+		this.baseUrl = baseUrl;
+		this.url = url;
+		this.version = version;
+		
+		this.capabilities = capabilities;
+		this.requires = requires;
+	}
+	
+	public static class Builder {
+		private String id;
+		private String presentationName;
+		private String symbolicName;
+		private String baseUrl;
+		private String url;
+		private String version;
+		private final List<Capability> capabilities = new LinkedList<Capability>();
+		private final List<Require> requires = new LinkedList<Require>();
+		
+		public Builder setId(String id) {
+			this.id = id;
+			return this;
+		}
+		public Builder setPresentationName(String presentationName) {
+			this.presentationName = presentationName;
+			return this;
+		}
+		public Builder setSymbolicName(String symbolicName) {
+			this.symbolicName = symbolicName;
+			return this;
+		}
+		public Builder setBaseUrl(String baseUrl) {
+			this.baseUrl = baseUrl;
+			return this;
+		}
+		public Builder setUrl(String url) {
+			this.url = url;
+			return this;
+		}
+		public Builder setVersion(String version) {
+			this.version = version;
+			return this;
+		}
+		public Builder addCapability(Capability capability) {
+			this.capabilities.add(capability);
+			return this;
+		}
+		public Builder addCapability(Capability.Builder capabilityBuilder) {
+			this.capabilities.add(capabilityBuilder.build());
+			return this;
+		}
+		public Builder addRequire(Require require) {
+			this.requires.add(require);
+			return this;
+		}
+		
+		public Resource build() {
+			if (id == null) throw new IllegalStateException("'id' field is not initialised");
+			if (symbolicName == null) throw new IllegalStateException("'symbolicName' field is not initialised");
+			if (url == null) throw new IllegalStateException("'url' field is not initialised");
+			
+			return new Resource(id, presentationName, symbolicName, baseUrl, url, version, Collections.unmodifiableList(capabilities), Collections.unmodifiableList(requires));
+		}
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public String getPresentationName() {
+		return presentationName;
+	}
+
+	public String getSymbolicName() {
+		return symbolicName;
+	}
+	
+	public String getBaseUrl() {
+		return baseUrl;
+	}
+
+	public String getUrl() {
+		return url;
+	}
+
+	public String getVersion() {
+		return version;
+	}
+
+	public List<Capability> getCapabilities() {
+		return capabilities;
+	}
+	
+	public Capability findPackageCapability(String pkgName) {
+		for (Capability capability : capabilities) {
+			if (CapabilityType.PACKAGE.getTypeName().equals(capability.getName())) {
+				List<Property> props = capability.getProperties();
+				for (Property prop : props) {
+					if (Property.PACKAGE.equals(prop.getName())) {
+						if (pkgName.equals(prop.getValue()))
+							return capability;
+						else
+							break;
+					}
+				}
+			}
+		}
+		return null;
+	}
+
+
+	
+	public List<Require> getRequires() {
+		return requires;
+	}
+	
+	public Require findRequire(String name) {
+		for (Require require : requires) {
+			if (name.equals(require.getName()))
+				return require;
+		}
+		return null;
+	}
+	
+	public Require findPackageRequire(String usesPkgName) {
+		String matchString = String.format("(package=%s)", usesPkgName);
+		
+		for (Require require : requires) {
+			if (CapabilityType.PACKAGE.getTypeName().equals(require.getName())) {
+				String filter = require.getFilter();
+				if (filter.indexOf(matchString) > -1)
+					return require;
+			}
+		}
+		return null;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder builder = new StringBuilder();
+		builder.append("Resource [id=").append(id)
+				.append(", presentationName=").append(presentationName)
+				.append(", symbolicName=").append(symbolicName)
+				.append(", baseUrl=").append(baseUrl)
+				.append(", url=").append(url).append(", version=")
+				.append(version).append(", capabilities=").append(capabilities)
+				.append("]");
+		return builder.toString();
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((baseUrl == null) ? 0 : baseUrl.hashCode());
+		result = prime * result
+				+ ((capabilities == null) ? 0 : capabilities.hashCode());
+		result = prime * result + ((id == null) ? 0 : id.hashCode());
+		result = prime
+				* result
+				+ ((presentationName == null) ? 0 : presentationName.hashCode());
+		result = prime * result
+				+ ((symbolicName == null) ? 0 : symbolicName.hashCode());
+		result = prime * result + ((url == null) ? 0 : url.hashCode());
+		result = prime * result + ((version == null) ? 0 : version.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		Resource other = (Resource) obj;
+		if (baseUrl == null) {
+			if (other.baseUrl != null)
+				return false;
+		} else if (!baseUrl.equals(other.baseUrl))
+			return false;
+		if (capabilities == null) {
+			if (other.capabilities != null)
+				return false;
+		} else if (!capabilities.equals(other.capabilities))
+			return false;
+		if (id == null) {
+			if (other.id != null)
+				return false;
+		} else if (!id.equals(other.id))
+			return false;
+		if (presentationName == null) {
+			if (other.presentationName != null)
+				return false;
+		} else if (!presentationName.equals(other.presentationName))
+			return false;
+		if (symbolicName == null) {
+			if (other.symbolicName != null)
+				return false;
+		} else if (!symbolicName.equals(other.symbolicName))
+			return false;
+		if (url == null) {
+			if (other.url != null)
+				return false;
+		} else if (!url.equals(other.url))
+			return false;
+		if (version == null) {
+			if (other.version != null)
+				return false;
+		} else if (!version.equals(other.version))
+			return false;
+		return true;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/StopParseException.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/StopParseException.java
new file mode 100644
index 0000000..2ca3005
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/StopParseException.java
@@ -0,0 +1,7 @@
+package aQute.lib.deployer.obr;
+
+import org.xml.sax.SAXException;
+
+public class StopParseException extends SAXException {
+	private static final long serialVersionUID = 1L;
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/URLResourceHandle.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/URLResourceHandle.java
new file mode 100644
index 0000000..75b9243
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/URLResourceHandle.java
@@ -0,0 +1,136 @@
+package aQute.lib.deployer.obr;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLEncoder;
+
+import aQute.bnd.service.ResourceHandle;
+
+public class URLResourceHandle implements ResourceHandle {
+	
+	static final String FILE_SCHEME = "file:";
+	static final String HTTP_SCHEME = "http:";
+	
+	final File cacheDir;
+	
+	// The resolved, absolute URL of the resource
+	final URL url;
+	
+	// The local file, if the resource IS a file, otherwise null.
+	final File localFile;
+	
+	// The cached file copy of the resource, if it is remote and has been downloaded.
+	final File cachedFile;
+
+	public URLResourceHandle(String url, String baseUrl, final File cacheDir) throws IOException {
+		this.cacheDir = cacheDir;
+		if (url.startsWith(FILE_SCHEME)) {
+			// File URL may be relative or absolute
+			File file = new File(url.substring(FILE_SCHEME.length()));
+			if (file.isAbsolute()) {
+				this.localFile = file;
+			} else {
+				if (!baseUrl.startsWith(FILE_SCHEME))
+					throw new IllegalArgumentException("Relative file URLs cannot be resolved if the base URL is a non-file URL.");
+				this.localFile = resolveFile(baseUrl.substring(FILE_SCHEME.length()), file.toString());
+			}
+			this.url = localFile.toURI().toURL();
+			if (!localFile.isFile() && !localFile.isDirectory())
+				throw new FileNotFoundException("File URL " + this.url + " points at a non-existing file.");
+			this.cachedFile = null;
+		} else if (url.startsWith(HTTP_SCHEME)) {
+			// HTTP URLs must be absolute
+			this.url = new URL(url);
+			this.localFile = null;
+			this.cachedFile = mapRemoteURL(this.url);
+		} else {
+			// A path with no scheme means resolve relative to the base URL
+			if (baseUrl.startsWith(FILE_SCHEME)) {
+				this.localFile = resolveFile(baseUrl.substring(FILE_SCHEME.length()), url);
+				this.url = localFile.toURI().toURL();
+				this.cachedFile = null;
+			} else {
+				URL base = new URL(baseUrl);
+				this.url = new URL(base, url);
+				this.localFile = null;
+				this.cachedFile = mapRemoteURL(this.url);
+			}
+		}
+	}
+	
+	File resolveFile(String baseFileName, String fileName) {
+		File resolved;
+		
+		File baseFile = new File(baseFileName);
+		if (baseFile.isDirectory())
+			resolved = new File(baseFile, fileName);
+		else if (baseFile.isFile())
+			resolved = new File(baseFile.getParentFile(), fileName);
+		else
+			throw new IllegalArgumentException("Cannot resolve relative to non-existant base file path: " + baseFileName);
+		
+		return resolved;
+	}
+
+	private File mapRemoteURL(URL url) throws UnsupportedEncodingException {
+		String encoded = URLEncoder.encode(url.toString(), "UTF-8");
+		return new File(cacheDir, encoded);
+	}
+
+	public String getName() {
+		return url.toString();
+	}
+
+	public Location getLocation() {
+		Location result;
+		
+		if (localFile != null)
+			result = Location.local;
+		else if (cachedFile.exists())
+			result = Location.remote_cached;
+		else
+			result = Location.remote;
+		
+		return result;
+	}
+
+	public File request() throws IOException {
+		if (localFile != null)
+			return localFile;
+		if (cachedFile == null)
+			throw new IllegalStateException("Invalid URLResourceHandle: both local file and cache file are uninitialised.");
+		
+		if (!cachedFile.exists()) {
+			cacheDir.mkdirs();
+			downloadToFile(url, cachedFile);
+		}
+		
+		return cachedFile;
+	}
+	
+	private static void downloadToFile(URL url, File file) throws IOException {
+		InputStream in = null;
+		OutputStream out = null;
+		try {
+			in = url.openStream();
+			out = new FileOutputStream(file);
+			
+			byte[] buf = new byte[1024];
+			for(;;) {
+				int bytes = in.read(buf, 0, 1024);
+				if (bytes < 0) break;
+				out.write(buf, 0, bytes);
+			}
+		} finally {
+			try { if (in != null) in.close(); } catch (IOException e) {};
+			try { if (out != null) in.close(); } catch (IOException e) {};
+		}
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/deployer/obr/UniqueResourceFilter.java b/bundleplugin/src/main/java/aQute/lib/deployer/obr/UniqueResourceFilter.java
new file mode 100644
index 0000000..0ad2cfb
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/deployer/obr/UniqueResourceFilter.java
@@ -0,0 +1,54 @@
+package aQute.lib.deployer.obr;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.xml.sax.Attributes;
+
+import aQute.libg.sax.filters.ElementSelectionFilter;
+
+public class UniqueResourceFilter extends ElementSelectionFilter {
+	
+	final Set<String> uris = new HashSet<String>();
+	final Map<String, List<String>> filteredResources = new HashMap<String, List<String>>();
+
+	@Override
+	protected boolean select(int depth, String uri, String localName, String qName, Attributes attribs) {
+		if ("resource".equals(qName)) {
+			String resourceUri = attribs.getValue("uri");
+			if (uris.contains(resourceUri)) {
+				addFilteredBundle(attribs.getValue("symbolicname"), attribs.getValue("version"));
+				return false;
+			}
+			uris.add(resourceUri);
+		}
+		return true;
+	}
+
+	private void addFilteredBundle(String bsn, String version) {
+		List<String> versions = filteredResources.get(bsn);
+		if (versions == null) {
+			versions = new LinkedList<String>();
+			filteredResources.put(bsn, versions);
+		}
+		versions.add(version);
+	}
+	
+	public Collection<String> getFilteredBSNs() {
+		return filteredResources.keySet();
+	}
+	
+	public Collection<String> getFilteredVersions(String bsn) {
+		List<String> list = filteredResources.get(bsn);
+		if (list == null)
+			list = Collections.emptyList();
+		return list;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/filter/Filter.java b/bundleplugin/src/main/java/aQute/lib/filter/Filter.java
new file mode 100644
index 0000000..320c3ac
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/filter/Filter.java
@@ -0,0 +1,353 @@
+/**
+ * Copyright (c) 2000 Gatespace AB. All Rights Reserved.
+ *
+ * Gatespace grants Open Services Gateway Initiative (OSGi) an irrevocable,
+ * perpetual, non-exclusive, worldwide, paid-up right and license to
+ * reproduce, display, perform, prepare and have prepared derivative works
+ * based upon and distribute and sublicense this material and derivative
+ * works thereof as set out in the OSGi MEMBER AGREEMENT as of January 24
+ * 2000, for use in accordance with Section 2.2 of the BY-LAWS of the
+ * OSGi MEMBER AGREEMENT.
+ */
+
+package aQute.lib.filter;
+
+import java.lang.reflect.*;
+import java.math.*;
+import java.util.*;
+
+public class Filter {
+    final char     WILDCARD = 65535;
+
+    final int      EQ       = 0;
+    final int      LE       = 1;
+    final int      GE       = 2;
+    final int      APPROX   = 3;
+
+    private String filter;
+
+    abstract class Query {
+        static final String GARBAGE   = "Trailing garbage";
+        static final String MALFORMED = "Malformed query";
+        static final String EMPTY     = "Empty list";
+        static final String SUBEXPR   = "No subexpression";
+        static final String OPERATOR  = "Undefined operator";
+        static final String TRUNCATED = "Truncated expression";
+        static final String EQUALITY  = "Only equality supported";
+
+        private String      tail;
+
+        boolean match() throws IllegalArgumentException {
+            tail = filter;
+            boolean val = doQuery();
+            if (tail.length() > 0)
+                error(GARBAGE);
+            return val;
+        }
+
+        private boolean doQuery() throws IllegalArgumentException {
+            if (tail.length() < 3 || !prefix("("))
+                error(MALFORMED);
+            boolean val;
+
+            switch (tail.charAt(0)) {
+            case '&':
+                val = doAnd();
+                break;
+            case '|':
+                val = doOr();
+                break;
+            case '!':
+                val = doNot();
+                break;
+            default:
+                val = doSimple();
+                break;
+            }
+
+            if (!prefix(")"))
+                error(MALFORMED);
+            return val;
+        }
+
+        private boolean doAnd() throws IllegalArgumentException {
+            tail = tail.substring(1);
+            boolean val = true;
+            if (!tail.startsWith("("))
+                error(EMPTY);
+            do {
+                if (!doQuery())
+                    val = false;
+            } while (tail.startsWith("("));
+            return val;
+        }
+
+        private boolean doOr() throws IllegalArgumentException {
+            tail = tail.substring(1);
+            boolean val = false;
+            if (!tail.startsWith("("))
+                error(EMPTY);
+            do {
+                if (doQuery())
+                    val = true;
+            } while (tail.startsWith("("));
+            return val;
+        }
+
+        private boolean doNot() throws IllegalArgumentException {
+            tail = tail.substring(1);
+            if (!tail.startsWith("("))
+                error(SUBEXPR);
+            return !doQuery();
+        }
+
+        private boolean doSimple() throws IllegalArgumentException {
+            int op = 0;
+            Object attr = getAttr();
+
+            if (prefix("="))
+                op = EQ;
+            else if (prefix("<="))
+                op = LE;
+            else if (prefix(">="))
+                op = GE;
+            else if (prefix("~="))
+                op = APPROX;
+            else
+                error(OPERATOR);
+
+            return compare(attr, op, getValue());
+        }
+
+        private boolean prefix(String pre) {
+            if (!tail.startsWith(pre))
+                return false;
+            tail = tail.substring(pre.length());
+            return true;
+        }
+
+        private Object getAttr() {
+            int len = tail.length();
+            int ix = 0;
+            label: for (; ix < len; ix++) {
+                switch (tail.charAt(ix)) {
+                case '(':
+                case ')':
+                case '<':
+                case '>':
+                case '=':
+                case '~':
+                case '*':
+                case '\\':
+                    break label;
+                }
+            }
+            String attr = tail.substring(0, ix).toLowerCase();
+            tail = tail.substring(ix);
+            return getProp(attr);
+        }
+
+        abstract Object getProp(String key);
+
+        private String getValue() {
+            StringBuffer sb = new StringBuffer();
+            int len = tail.length();
+            int ix = 0;
+            label: for (; ix < len; ix++) {
+                char c = tail.charAt(ix);
+                switch (c) {
+                case '(':
+                case ')':
+                    break label;
+                case '*':
+                    sb.append(WILDCARD);
+                    break;
+                case '\\':
+                    if (ix == len - 1)
+                        break label;
+                    sb.append(tail.charAt(++ix));
+                    break;
+                default:
+                    sb.append(c);
+                    break;
+                }
+            }
+            tail = tail.substring(ix);
+            return sb.toString();
+        }
+
+        private void error(String m) throws IllegalArgumentException {
+            throw new IllegalArgumentException(m + " " + tail);
+        }
+
+        private boolean compare(Object obj, int op, String s) {
+            if (obj == null)
+                return false;
+            try {
+                Class<?> numClass = obj.getClass();
+                if (numClass == String.class) {
+                    return compareString((String) obj, op, s);
+                } else if (numClass == Character.class) {
+                    return compareString(obj.toString(), op, s);
+                } else if (numClass == Long.class) {
+                    return compareSign(op, Long.valueOf(s)
+                            .compareTo((Long) obj));
+                } else if (numClass == Integer.class) {
+                    return compareSign(op, Integer.valueOf(s).compareTo(
+                            (Integer) obj));
+                } else if (numClass == Short.class) {
+                    return compareSign(op, Short.valueOf(s).compareTo(
+                            (Short) obj));
+                } else if (numClass == Byte.class) {
+                    return compareSign(op, Byte.valueOf(s)
+                            .compareTo((Byte) obj));
+                } else if (numClass == Double.class) {
+                    return compareSign(op, Double.valueOf(s).compareTo(
+                            (Double) obj));
+                } else if (numClass == Float.class) {
+                    return compareSign(op, Float.valueOf(s).compareTo(
+                            (Float) obj));
+                } else if (numClass == Boolean.class) {
+                    if (op != EQ)
+                        return false;
+                    int a = Boolean.valueOf(s).booleanValue() ? 1 : 0;
+                    int b = ((Boolean) obj).booleanValue() ? 1 : 0;
+                    return compareSign(op, a - b);
+                } else if (numClass == BigInteger.class) {
+                    return compareSign(op, new BigInteger(s)
+                            .compareTo((BigInteger) obj));
+                } else if (numClass == BigDecimal.class) {
+                    return compareSign(op, new BigDecimal(s)
+                            .compareTo((BigDecimal) obj));
+                } else if (obj instanceof Collection<?>) {
+                    for (Object x : (Collection<?>) obj)
+                        if (compare(x, op, s))
+                            return true;
+                } else if (numClass.isArray()) {
+                    int len = Array.getLength(obj);
+                    for (int i = 0; i < len; i++)
+                        if (compare(Array.get(obj, i), op, s))
+                            return true;
+                }
+            } catch (Exception e) {
+            }
+            return false;
+        }
+    }
+
+    class DictQuery extends Query {
+        private Dictionary<?,?> dict;
+
+        DictQuery(Dictionary<?,?> dict) {
+            this.dict = dict;
+        }
+
+        Object getProp(String key) {
+            return dict.get(key);
+        }
+    }
+
+    public Filter(String filter) throws IllegalArgumentException {
+        // NYI: Normalize the filter string?
+        this.filter = filter;
+        if (filter == null || filter.length() == 0)
+            throw new IllegalArgumentException("Null query");
+    }
+
+    public boolean match(Dictionary<?,?> dict) {
+        try {
+            return new DictQuery(dict).match();
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+    }
+
+    public String verify() {
+        try {
+            new DictQuery(new Hashtable<Object,Object>()).match();
+        } catch (IllegalArgumentException e) {
+            return e.getMessage();
+        }
+        return null;
+    }
+
+    public String toString() {
+        return filter;
+    }
+
+    public boolean equals(Object obj) {
+        return obj != null && obj instanceof Filter
+                && filter.equals(((Filter) obj).filter);
+    }
+
+    public int hashCode() {
+        return filter.hashCode();
+    }
+
+    boolean compareString(String s1, int op, String s2) {
+        switch (op) {
+        case EQ:
+            return patSubstr(s1, s2);
+        case APPROX:
+            return fixupString(s2).equals(fixupString(s1));
+        default:
+            return compareSign(op, s2.compareTo(s1));
+        }
+    }
+
+    boolean compareSign(int op, int cmp) {
+        switch (op) {
+        case LE:
+            return cmp >= 0;
+        case GE:
+            return cmp <= 0;
+        case EQ:
+            return cmp == 0;
+        default: /* APPROX */
+            return cmp == 0;
+        }
+    }
+
+    String fixupString(String s) {
+        StringBuffer sb = new StringBuffer();
+        int len = s.length();
+        boolean isStart = true;
+        boolean isWhite = false;
+        for (int i = 0; i < len; i++) {
+            char c = s.charAt(i);
+            if (Character.isWhitespace(c)) {
+                isWhite = true;
+            } else {
+                if (!isStart && isWhite)
+                    sb.append(' ');
+                if (Character.isUpperCase(c))
+                    c = Character.toLowerCase(c);
+                sb.append(c);
+                isStart = false;
+                isWhite = false;
+            }
+        }
+        return sb.toString();
+    }
+
+    boolean patSubstr(String s, String pat) {
+        if (s == null)
+            return false;
+        if (pat.length() == 0)
+            return s.length() == 0;
+        if (pat.charAt(0) == WILDCARD) {
+            pat = pat.substring(1);
+            for (;;) {
+                if (patSubstr(s, pat))
+                    return true;
+                if (s.length() == 0)
+                    return false;
+                s = s.substring(1);
+            }
+        } else {
+            if (s.length() == 0 || s.charAt(0) != pat.charAt(0))
+                return false;
+            return patSubstr(s.substring(1), pat.substring(1));
+        }
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/filter/packageinfo b/bundleplugin/src/main/java/aQute/lib/filter/packageinfo
new file mode 100644
index 0000000..7c8de03
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/filter/packageinfo
@@ -0,0 +1 @@
+version 1.0
diff --git a/bundleplugin/src/main/java/aQute/lib/hex/Hex.java b/bundleplugin/src/main/java/aQute/lib/hex/Hex.java
new file mode 100644
index 0000000..c1ad416
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/hex/Hex.java
@@ -0,0 +1,59 @@
+package aQute.lib.hex;
+
+import java.io.*;
+
+
+/*
+ * Hex converter.
+ * 
+ * TODO Implement string to byte[]
+ */
+public class Hex {
+	byte[]				data;
+	final static char[] HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+	public final static byte[] toByteArray(String string) {
+		string = string.trim();
+		if ( (string.length() & 1) != 0)
+			throw new IllegalArgumentException("a hex string must have an even length");
+
+		byte[]out = new byte[ string.length()/2];
+		for ( int i=0; i < out.length; i++) {
+			out[i] = (byte) (nibble(string.charAt(i*2))<<4 + nibble(string.charAt(i*2+1)));
+		}
+		return out;
+	}
+
+	
+	public final static int nibble( char c) {
+		if (c >= '0' && c <= '9')
+			return c - '0';
+		
+		if ( c>='A' && c<='F')
+			return c - 'A' + 10;
+		if ( c>='a' && c<='f')
+			return c - 'a' + 10;
+		
+		throw new IllegalArgumentException("Not a hex digit: " + c);
+	}
+	
+	public final static String toHexString(byte data[]) {
+		StringBuilder sb = new StringBuilder();
+		try {
+			append(sb,data);
+		} catch (IOException e) {
+			// cannot happen with sb
+		}
+		return sb.toString();
+	}
+	
+	public final static void append( Appendable sb, byte [] data ) throws IOException {
+		for ( int i =0; i<data.length; i++) {
+			sb.append( nibble( data[i] >> 4));
+			sb.append( nibble( data[i]));
+		}
+	}
+
+	private final static char nibble(int i) {	
+		return HEX[i & 0xF];
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/hex/packageinfo b/bundleplugin/src/main/java/aQute/lib/hex/packageinfo
new file mode 100644
index 0000000..7c8de03
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/hex/packageinfo
@@ -0,0 +1 @@
+version 1.0
diff --git a/bundleplugin/src/main/java/aQute/lib/index/Index.java b/bundleplugin/src/main/java/aQute/lib/index/Index.java
new file mode 100644
index 0000000..35662d0
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/index/Index.java
@@ -0,0 +1,352 @@
+package aQute.lib.index;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.nio.channels.FileChannel.MapMode;
+import java.util.*;
+
+/**
+ * <pre>
+ *   0   ->   0, 122   -> 1
+ *   123 -> 123, 244   -> 2
+ *   245 -> 245, ...
+ * </pre>
+ * 
+ * 
+ */
+public class Index implements Iterable<byte[]> {
+	final static int					LEAF		= 0;
+	final static int					INDEX		= 1;
+
+	final static int					SIGNATURE	= 0;
+	final static int					MAGIC		= 0x494C4458;
+	final static int					KEYSIZE		= 4;
+
+	private FileChannel					file;
+	final int							pageSize	= 4096;
+	final int							keySize;
+	final int							valueSize	= 8;
+	final int							capacity;
+	public Page							root;
+	final LinkedHashMap<Integer, Page>	cache		= new LinkedHashMap<Integer, Index.Page>();
+	final MappedByteBuffer				settings;
+
+	private int							nextPage;
+
+	class Page {
+		final int				TYPE_OFFSET		= 0;
+		final int				COUNT_OFFSET	= 2;
+		final static int		START_OFFSET	= 4;
+		final int				number;
+		boolean					leaf;
+		final MappedByteBuffer	buffer;
+		int						n				= 0;
+		boolean					dirty;
+
+		Page(int number) throws IOException {
+			this.number = number;
+			buffer = file.map(MapMode.READ_WRITE, number * pageSize, pageSize);
+			n = buffer.getShort(COUNT_OFFSET);
+			int type = buffer.getShort(TYPE_OFFSET);
+			leaf = type != 0;
+		}
+
+		Page(int number, boolean leaf) throws IOException {
+			this.number = number;
+			this.leaf = leaf;
+			this.n = 0;
+			buffer = file.map(MapMode.READ_WRITE, number * pageSize, pageSize);
+		}
+
+		Iterator<byte[]> iterator() {
+			return new Iterator<byte[]>() {
+				Iterator<byte[]>	i;
+				int					rover	= 0;
+
+				public byte[] next() {
+					if (leaf) {
+						return k(rover++);
+					}
+
+					return i.next();
+				}
+
+				public boolean hasNext() {
+					try {
+						if (leaf)
+							return rover < n;
+						else {
+							while (i == null || i.hasNext() == false) {
+								int c = (int) c(rover++);
+								i = getPage(c).iterator();
+							}
+							return i.hasNext();
+						}
+					} catch (IOException e) {
+						throw new RuntimeException(e);
+					}
+
+				}
+
+				public void remove() {
+					throw new UnsupportedOperationException();
+				}
+
+			};
+		}
+
+		void write() throws IOException {
+			buffer.putShort(COUNT_OFFSET, (short) n);
+			buffer.put(TYPE_OFFSET, (byte) (leaf ? 1 : 0));
+			buffer.force();
+		}
+
+		int compare(byte[] key, int i) {
+			int index = pos(i);
+			for (int j = 0; j < keySize; j++, index++) {
+				int a = 0;
+				if (j < key.length)
+					a = key[j] & 0xFF;
+
+				int b = buffer.get(index) & 0xFF;
+				if (a == b)
+					continue;
+
+				return a > b ? 1 : -1;
+			}
+			return 0;
+		}
+
+		int pos(int i) {
+			return START_OFFSET + size(i);
+		}
+
+		int size(int n) {
+			return n * (keySize + valueSize);
+		}
+
+		void copyFrom(Page page, int start, int length) {
+			copy(page.buffer, pos(start), buffer, pos(0), size(length));
+		}
+
+		void copy(ByteBuffer src, int srcPos, ByteBuffer dst, int dstPos, int length) {
+			if (srcPos < dstPos) {
+				while (length-- > 0)
+					dst.put(dstPos + length, src.get(srcPos + length));
+
+			} else {
+				while (length-- > 0)
+					dst.put(dstPos++, src.get(srcPos++));
+			}
+		}
+
+		long search(byte[] k) throws Exception {
+			int cmp = 0;
+			int i = n - 1;
+			while (i >= 0 && (cmp = compare(k, i)) < 0)
+				i--;
+
+			if (leaf) {
+				if (cmp != 0)
+					return -1;
+				else
+					return c(i);
+			} else {
+				long value = c(i);
+				Page child = getPage((int) value);
+				return child.search(k);
+			}
+		}
+
+		void insert(byte[] k, long v) throws IOException {
+			if (n == capacity) {
+				int t = capacity / 2;
+				Page left = allocate(leaf);
+				Page right = allocate(leaf);
+				left.copyFrom(this, 0, t);
+				left.n = t;
+				right.copyFrom(this, t, capacity - t);
+				right.n = capacity - t;
+				leaf = false;
+				set(0, left.k(0), left.number);
+				set(1, right.k(0), right.number);
+				n = 2;
+				left.write();
+				right.write();
+			}
+			insertNonFull(k, v);
+		}
+
+		byte[] k(int i) {
+			buffer.position(pos(i));
+			byte[] key = new byte[keySize];
+			buffer.get(key);
+			return key;
+		}
+
+		long c(int i) {
+			if (i < 0) {
+				System.out.println("Arghhh");
+			}
+			int index = pos(i) + keySize;
+			return buffer.getLong(index);
+		}
+
+		void set(int i, byte[] k, long v) {
+			int index = pos(i);
+			for (int j = 0; j < keySize; j++) {
+				byte a = 0;
+				if (j < k.length)
+					a = k[j];
+				buffer.put(index + j, a);
+			}
+			buffer.putLong(index + keySize, v);
+		}
+
+		void insertNonFull(byte[] k, long v) throws IOException {
+			int cmp = 0;
+			int i = n - 1;
+			while (i >= 0 && (cmp = compare(k, i)) < 0)
+				i--;
+
+			if (leaf) {
+				if (cmp != 0) {
+					i++;
+					if (i != n)
+						copy(buffer, pos(i), buffer, pos(i + 1), size(n - i));
+				}
+				set(i, k, v);
+				n++;
+				dirty = true;
+			} else {
+				long value = c(i);
+				Page child = getPage((int) value);
+
+				if (child.n == capacity) {
+					Page left = child;
+					int t = capacity / 2;
+					Page right = allocate(child.leaf);
+					right.copyFrom(left, t, capacity - t);
+					right.n = capacity - t;
+					left.n = t;
+					i++; // place to insert
+					if (i < n) // ok if at end
+						copy(buffer, pos(i), buffer, pos(i + 1), size(n - i));
+					set(i, right.k(0), right.number);
+					n++;
+					assert i < n;
+					child = right.compare(k, 0) >= 0 ? right : left;
+					left.dirty = true;
+					right.dirty = true;
+					this.dirty = true;
+				}
+				child.insertNonFull(k, v);
+			}
+			write();
+		}
+
+		public String toString() {
+			StringBuilder sb = new StringBuilder();
+			try {
+				toString( sb, "");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+			return sb.toString();
+		}
+		
+		public void toString( StringBuilder sb, String indent ) throws IOException {
+			for (int i = 0; i < n; i++) {
+				sb.append(String.format("%s %02d:%02d %20s %s %d\n", indent, number, i, hex(k(i), 0, 4), leaf ? "==" : "->", c(i)));
+				if (! leaf ) {
+					long c = c(i);
+					Page sub = getPage((int)c);
+					sub.toString(sb,indent+" ");
+				}
+			}
+		}
+
+		private String hex(byte[] k, int i, int j) {
+			StringBuilder sb = new StringBuilder();
+			
+			while ( i < j) {
+				int b = 0xFF & k[i];
+				sb.append(nibble(b>>4));
+				sb.append(nibble(b));
+				i++;
+			}
+			return sb.toString();
+		}
+
+		private char nibble(int i) {
+			i = i & 0xF;
+			return (char) ( i >= 10 ? i + 'A' - 10 : i + '0');
+		}
+
+	}
+
+	public Index(File file, int keySize) throws IOException {
+		capacity = (pageSize - Page.START_OFFSET) / (keySize + valueSize);
+		RandomAccessFile raf = new RandomAccessFile(file, "rw");
+		this.file = raf.getChannel();
+		settings = this.file.map(MapMode.READ_WRITE, 0, pageSize);
+		if (this.file.size() == pageSize) {
+			this.keySize = keySize;
+			settings.putInt(SIGNATURE, MAGIC);
+			settings.putInt(KEYSIZE, keySize);
+			nextPage = 1;
+			root = allocate(true);
+			root.n = 1;
+			root.set(0, new byte[KEYSIZE], 0);
+			root.write();
+		} else {
+			if (settings.getInt(SIGNATURE) != MAGIC)
+				throw new IllegalStateException("No Index file, magic is not " + MAGIC);
+
+			this.keySize = settings.getInt(KEYSIZE);
+			if (keySize != 0 && this.keySize != keySize)
+				throw new IllegalStateException("Invalid key size for Index file. The file is "
+						+ this.keySize + " and was expected to be " + this.keySize);
+
+			root = getPage(1);
+			nextPage = (int) (this.file.size() / pageSize);
+		}
+	}
+
+	public void insert(byte[] k, long v) throws Exception {
+		root.insert(k, v);
+	}
+
+	public long search(byte[] k) throws Exception {
+		return root.search(k);
+	}
+
+	Page allocate(boolean leaf) throws IOException {
+		Page page = new Page(nextPage++, leaf);
+		cache.put(page.number, page);
+		return page;
+	}
+
+	Page getPage(int number) throws IOException {
+		Page page = cache.get(number);
+		if (page == null) {
+			page = new Page(number);
+			cache.put(number, page);
+		}
+		return page;
+	}
+
+	public String toString() {
+		return root.toString();
+	}
+	
+	public void close() throws IOException {
+		file.close();
+		cache.clear();
+	}
+
+	public Iterator<byte[]> iterator() {
+		return root.iterator();
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/index/packageinfo b/bundleplugin/src/main/java/aQute/lib/index/packageinfo
new file mode 100644
index 0000000..7c8de03
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/index/packageinfo
@@ -0,0 +1 @@
+version 1.0
diff --git a/bundleplugin/src/main/java/aQute/lib/io/IO.java b/bundleplugin/src/main/java/aQute/lib/io/IO.java
new file mode 100644
index 0000000..d2cb411
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/io/IO.java
@@ -0,0 +1,162 @@
+package aQute.lib.io;
+
+import java.io.*;
+import java.net.*;
+import java.nio.*;
+import java.util.*;
+
+public class IO {
+	public static void copy(InputStream in, OutputStream out) throws IOException {
+		DataOutputStream dos = new DataOutputStream(out);
+		copy(in, (DataOutput) dos);
+	}
+
+	public static void copy(InputStream in, DataOutput out) throws IOException {
+		byte[] buffer = new byte[10000];
+		try {
+			int size = in.read(buffer);
+			while (size > 0) {
+				out.write(buffer, 0, size);
+				size = in.read(buffer);
+			}
+		} finally {
+			in.close();
+		}
+	}
+
+	public static void copy(InputStream in, ByteBuffer bb) throws IOException {
+		byte[] buffer = new byte[10000];
+		try {
+			int size = in.read(buffer);
+			while (size > 0) {
+				bb.put(buffer, 0, size);
+				size = in.read(buffer);
+			}
+		} finally {
+			in.close();
+		}
+	}
+
+	public static void copy(File a, File b) throws IOException {
+		FileOutputStream out = new FileOutputStream(b);
+		try {
+			copy(new FileInputStream(a), out);
+		} finally {
+			out.close();
+		}
+	}
+
+	public static void copy(InputStream a, File b) throws IOException {
+		FileOutputStream out = new FileOutputStream(b);
+		try {
+			copy(a, out);
+		} finally {
+			out.close();
+		}
+	}
+
+	public static void copy(File a, OutputStream b) throws IOException {
+		copy(new FileInputStream(a), b);
+	}
+
+	public static String collect(File a, String encoding) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		copy(a, out);
+		return new String(out.toByteArray(), encoding);
+	}
+
+	public static String collect(URL a, String encoding) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		copy(a.openStream(), out);
+		return new String(out.toByteArray(), encoding);
+	}
+
+	public static String collect(File a) throws IOException {
+		return collect(a, "UTF-8");
+	}
+
+	public static String collect(InputStream a, String encoding) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		copy(a, out);
+		return new String(out.toByteArray(), encoding);
+	}
+
+	public static String collect(InputStream a) throws IOException {
+		return collect(a, "UTF-8");
+	}
+
+	public static String collect(Reader a) throws IOException {
+		StringWriter sw = new StringWriter();
+		char[] buffer = new char[10000];
+		int size = a.read(buffer);
+		while (size > 0) {
+			sw.write(buffer, 0, size);
+			size = a.read(buffer);
+		}
+		return sw.toString();
+	}
+
+	public static File getFile(File base, String file) {
+		File f = new File(file);
+		if (f.isAbsolute())
+			return f;
+		int n;
+
+		f = base.getAbsoluteFile();
+		while ((n = file.indexOf('/')) > 0) {
+			String first = file.substring(0, n);
+			file = file.substring(n + 1);
+			if (first.equals(".."))
+				f = f.getParentFile();
+			else
+				f = new File(f, first);
+		}
+		if (file.equals(".."))
+			return f.getParentFile();
+		else
+			return new File(f, file).getAbsoluteFile();
+	}
+
+	public static void delete(File f) {
+		f = f.getAbsoluteFile();
+		if (f.getParentFile() == null)
+			throw new IllegalArgumentException("Cannot recursively delete root for safety reasons");
+
+		if (f.isDirectory()) {
+			File[] subs = f.listFiles();
+			for (File sub : subs)
+				delete(sub);
+		}
+
+		f.delete();
+	}
+
+	public static void drain(InputStream in) throws IOException {
+		byte[] buffer = new byte[10000];
+		try {
+			int size = in.read(buffer);
+			while (size > 0) {
+				size = in.read(buffer);
+			}
+		} finally {
+			in.close();
+		}
+	}
+
+	public void copy(Collection<?> c, OutputStream out) {
+		PrintStream ps = new PrintStream(out);
+		for (Object o : c) {
+			ps.println(o);
+		}
+		ps.flush();
+	}
+
+	public static Throwable close(Closeable in) {
+		try {
+			in.close();
+			return null;
+		} catch (Throwable e) {
+			return e;
+		}
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/io/LimitedInputStream.java b/bundleplugin/src/main/java/aQute/lib/io/LimitedInputStream.java
new file mode 100644
index 0000000..bf9a21f
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/io/LimitedInputStream.java
@@ -0,0 +1,78 @@
+package aQute.lib.io;
+
+import java.io.*;
+
+public class LimitedInputStream extends InputStream {
+
+	final InputStream	in;
+	final int			size;
+	int					left;
+
+	public LimitedInputStream(InputStream in, int size) {
+		this.in = in;
+		this.left = size;
+		this.size = size;
+	}
+
+	@Override public int read() throws IOException {
+		if (left <= 0) {
+			eof();
+			return -1;
+		}
+
+		left--;
+		return in.read();
+	}
+
+	@Override public int available() throws IOException {		
+		return Math.min(left, in.available());
+	}
+
+	@Override public void close() throws IOException {
+		eof();
+		in.close();
+	}
+
+	protected void eof() {
+	}
+
+	@Override public synchronized void mark(int readlimit) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override public boolean markSupported() {
+		return false;
+	}
+
+	@Override public int read(byte[] b, int off, int len) throws IOException {
+		int min = Math.min(len, left);
+		if (min == 0)
+			return 0;
+
+		int read = in.read(b, off, min);
+		if (read > 0)
+			left -= read;
+		return read;
+	}
+
+	@Override public int read(byte[] b) throws IOException {
+		return read(b,0,b.length);
+	}
+
+	@Override public synchronized void reset() throws IOException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override public long skip(long n) throws IOException {
+		long count = 0;
+		byte buffer[] = new byte[1024];
+		while ( n > 0 && read() >= 0) {
+			int size = read(buffer);
+			if ( size <= 0)
+				return count;
+			count+=size;
+			n-=size;
+		}
+		return count;
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/io/packageinfo b/bundleplugin/src/main/java/aQute/lib/io/packageinfo
new file mode 100644
index 0000000..7c8de03
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/io/packageinfo
@@ -0,0 +1 @@
+version 1.0
diff --git a/bundleplugin/src/main/java/aQute/lib/jardiff/Diff.java b/bundleplugin/src/main/java/aQute/lib/jardiff/Diff.java
new file mode 100644
index 0000000..b5daec8
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/jardiff/Diff.java
@@ -0,0 +1,152 @@
+package aQute.lib.jardiff;
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+import aQute.lib.osgi.*;
+
+public class Diff {
+
+    /**
+     * Compare two JAR files with each other.
+     * 
+     * @param a
+     * @param b
+     * @param strict
+     * @return
+     * @throws IOException
+     */
+    public Map<String, Object> diff(Jar a, Jar b, boolean strict)
+            throws Exception {
+        Map<String, Object> different = new TreeMap<String, Object>();
+        compareManifest(different, a.getManifest(), b.getManifest(), strict);
+        diff(different, a.getResources().keySet(), b.getResources().keySet());
+
+        Set<String> shared = new HashSet<String>(a.getResources().keySet());
+        shared.retainAll(b.getResources().keySet());
+
+        for (String path : a.getResources().keySet()) {
+            Resource ra = a.getResource(path);
+            Resource rb = a.getResource(path);
+            if (rb != null) {
+                if (ra.getClass() != rb.getClass()) {
+                    different.put(path, "Different types: "
+                            + a.getClass().getName() + " : "
+                            + b.getClass().getName());
+                } else {
+                    if (path.endsWith(".jar")) {
+                        Jar aa = new Jar(path, ra.openInputStream());
+                        Jar bb = new Jar(path, rb.openInputStream());
+                        try {
+                            Map<String, Object> result = diff(aa, bb, strict);
+                            if (!result.isEmpty())
+                                different.put(path, result);
+                        } finally {
+                            aa.close();
+                            bb.close();
+                        }
+                    } else {
+                        String cmp = diff(ra.openInputStream(), rb
+                                .openInputStream());
+                        if (cmp != null)
+                            different.put(path, cmp);
+                    }
+                }
+            }
+        }
+        return different;
+    }
+
+    String diff(InputStream a, InputStream b) throws IOException {
+        int n = 0;
+        int binary = 0;
+        StringBuffer sb = new StringBuffer();
+        while (true) {
+            int ac = a.read();
+            int bc = b.read();
+            if (ac < 0) {
+                if (bc < 0)
+                    return null;
+
+                return "a is smaller";
+            } else if (bc < 0) {
+                return "b is smaller";
+            }
+
+            if (ac != bc) {
+                String s = "Difference at pos: " + n;
+                if (binary == 0 && sb.length() > 5) {
+                    s = s + "Context: " + sb.substring(sb.length() - 5);
+                }
+            }
+
+            if (ac >= ' ' && ac <= '~')
+                sb.append((char) ac);
+            else
+                binary++;
+
+            n++;
+        }
+    }
+
+    void diff(Map<String, Object> different, Set<?> a, Set<?> b) {
+        Set<Object> onlyInA = new HashSet<Object>(a);
+        onlyInA.removeAll(b);
+        Set<Object> onlyInB = new HashSet<Object>(b);
+        onlyInB.removeAll(a);
+
+        for (Object element : onlyInA) {
+            different.put(element.toString(), "a");
+        }
+        for (Object element : onlyInB) {
+            different.put(element.toString(), "b");
+        }
+    }
+
+    public void compareManifest(Map<String, Object> different, Manifest a,
+            Manifest b, boolean strict) {
+        if (a == null || b == null) {
+            different.put("Manifest null", (a == null ? "a=null" : "a exists")
+                    + " " + (b == null ? "b=null" : "b exists"));
+            return;
+        }
+
+        Attributes attrs = a.getMainAttributes();
+        Attributes bttrs = b.getMainAttributes();
+        diff(different, attrs.keySet(), bttrs.keySet());
+        for (Object element : attrs.keySet()) {
+            Attributes.Name name = (Attributes.Name) element;
+            String av = attrs.getValue(name);
+            String bv = bttrs.getValue(name);
+            if (bv != null) {
+                if (!av.equals(bv))
+                    different.put(name.toString(), "M:" + name + ":" + av
+                            + "!=" + bv);
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public void print(PrintStream pout, Map<String, Object> map, int indent) {
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            for (int j = 0; j < indent; j++) {
+                pout.print(" ");
+            }
+            String key = entry.getKey();
+            pout.print(key);
+            for (int j = 0; j < 70 - indent - key.length(); j++) {
+                pout.print(" ");
+            }
+            if (entry.getValue() instanceof Map) {
+                pout.println();
+                print(pout, (Map<String, Object>) entry.getValue(), indent + 1);
+            } else
+                pout.println(entry.getValue());
+        }
+    }
+
+    public void close() {
+
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/About.java b/bundleplugin/src/main/java/aQute/lib/osgi/About.java
new file mode 100644
index 0000000..ade8497
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/About.java
@@ -0,0 +1,46 @@
+package aQute.lib.osgi;
+
+/**
+ * This package contains a number of classes that assists by analyzing JARs and
+ * constructing bundles.
+ * 
+ * The Analyzer class can be used to analyze an existing bundle and can create a
+ * manifest specification from proposed (wildcard) Export-Package,
+ * Bundle-Includes, and Import-Package headers.
+ * 
+ * The Builder class can use the headers to construct a JAR from the classpath.
+ * 
+ * The Verifier class can take an existing JAR and verify that all headers are
+ * correctly set. It will verify the syntax of the headers, match it against the
+ * proper contents, and verify imports and exports.
+ * 
+ * A number of utility classes are available.
+ * 
+ * Jar, provides an abstraction of a Jar file. It has constructors for creating
+ * a Jar from a stream, a directory, or a jar file. A Jar, keeps a collection
+ * Resource's. There are Resource implementations for File, from ZipFile, or from
+ * a stream (which copies the data). The Jar tries to minimize the work during
+ * build up so that it is cheap to use. The Resource's can be used to iterate 
+ * over the names and later read the resources when needed.
+ * 
+ * Clazz, provides a parser for the class files. This will be used to define the
+ * imports and exports.
+ * 
+ * A key component in this library is the Map. Headers are translated to Maps of Maps. OSGi
+ * header syntax is like:
+ * <pre>
+ * 	  header = clause ( ',' clause ) *
+ *    clause = file ( ';' file ) * ( parameter ) *
+ *    param  = attr '=' value | directive ':=' value
+ * </pre>
+ * These headers are translated to a Map that contains all headers (the order is
+ * maintained). Each additional file in a header definition will have its own
+ * entry (only native code does not work this way). The clause is represented
+ * as another map. The ':' of directives is considered part of the name. This
+ * allows attributes and directives to be maintained in the clause map. 
+ * 
+ * @version $Revision$
+ */
+public class About {
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/AbstractResource.java b/bundleplugin/src/main/java/aQute/lib/osgi/AbstractResource.java
new file mode 100644
index 0000000..532b33e
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/AbstractResource.java
@@ -0,0 +1,50 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+
+public abstract class AbstractResource implements Resource {
+    String extra;
+    byte[] calculated;
+    long   lastModified;
+
+    protected AbstractResource(long modified) {
+        lastModified = modified;
+    }
+
+    public String getExtra() {
+        return extra;
+    }
+
+    public long lastModified() {
+        return lastModified;
+    }
+
+    public InputStream openInputStream() throws IOException {
+        return new ByteArrayInputStream(getLocalBytes());
+    }
+
+    private byte[] getLocalBytes() throws IOException {
+        try {
+            if (calculated != null)
+                return calculated;
+
+            return calculated = getBytes();
+        } catch (IOException e) {
+            throw e;
+        } catch (Exception e) {
+            IOException ee = new IOException("Opening resource");
+            ee.initCause(e);
+            throw ee;
+        }
+    }
+
+    public void setExtra(String extra) {
+        this.extra = extra;
+    }
+
+    public void write(OutputStream out) throws IOException {
+        out.write(getLocalBytes());
+    }
+
+    abstract protected byte[] getBytes() throws Exception;
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Analyzer.java b/bundleplugin/src/main/java/aQute/lib/osgi/Analyzer.java
new file mode 100644
index 0000000..c3ab0cf
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Analyzer.java
@@ -0,0 +1,2169 @@
+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.Name;
+import java.util.regex.*;
+
+import aQute.bnd.annotation.*;
+import aQute.bnd.service.*;
+import aQute.lib.collections.*;
+import aQute.lib.filter.*;
+import aQute.lib.osgi.Clazz.QUERY;
+import aQute.libg.generics.*;
+import aQute.libg.header.*;
+import aQute.libg.tarjan.*;
+import aQute.libg.version.Version;
+
+public class Analyzer extends Processor {
+
+	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();							// refers
+	// package
+	final Map<String, Set<String>>			uses					= newHashMap();							// package
+	Map<String, Clazz>						classspace;
+	Map<String, Clazz>						importedClassesCache	= newMap();
+	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;
+	String									versionPolicyUses;
+	String									versionPolicyImplemented;
+	boolean									diagnostics				= false;
+	SortedSet<Clazz.JAVA>					formats					= new TreeSet<Clazz.JAVA>();
+	private boolean							inited;
+
+	public Analyzer(Processor parent) {
+		super(parent);
+	}
+
+	public Analyzer() {
+	}
+
+	/**
+	 * Specifically for Maven
+	 * 
+	 * @param properties
+	 *            the properties
+	 */
+
+	public static Properties getManifest(File dirOrJar) throws Exception {
+		Analyzer analyzer = new Analyzer();
+		try {
+			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;
+		} finally {
+			analyzer.close();
+		}
+	}
+
+	/**
+	 * Calculates the data structures for generating a manifest.
+	 * 
+	 * @throws IOException
+	 */
+	public void analyze() throws Exception {
+		if (!analyzed) {
+			analyzed = true;
+			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(doExportsToImports(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() && !(p.equals("*")))
+						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
+			// exports. I.e. look on the classpath
+			augmentExports();
+
+			// 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 isTrue(getProperty(RESOURCEONLY));
+	}
+
+	/**
+	 * 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 Exception {
+		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-" + getBndVersion());
+			main.putValue(BND_LASTMODIFIED, "" + System.currentTimeMillis());
+		}
+
+		String exportHeader = printClauses(exports, 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));
+		} 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 (isMissingPlugin(header.trim())) {
+				error("Missing plugin for command %s", header);
+			}
+			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_HEADER))
+						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(REMOVEHEADERS));
+		Set<Instruction> matchers = Instruction.replaceWithInstruction(removes).keySet();
+
+		Collection<Object> toBeRemoved = Instruction.select(matchers, main.keySet());
+		Iterator<Object> i = main.keySet().iterator();
+		while (i.hasNext())
+			if (toBeRemoved.contains(i.next()))
+				i.remove();
+
+		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();
+
+			String projectName = getBase().getName();
+			if (value == null || value.equals("bnd.bnd")) {
+				value = projectName;
+			} else if (value.endsWith(".bnd")) {
+				value = value.substring(0, value.length() - 4);
+				if (!value.startsWith(getBase().getName()))
+					value = projectName + "." + value;
+			}
+		}
+
+		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 for this bnd
+	 * 
+	 * @return version or unknown.
+	 */
+	public String getBndVersion() {
+		return getBndInfo("version", "1.42.1");
+	}
+
+	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: %s", 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() {
+		if (inited == false) {
+			inited = true;
+			super.begin();
+
+			updateModified(getBndLastModified(), "bnd last modified");
+			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 = Clazz.fqnToPath(interfaceName);
+		if (classspace.containsKey(path))
+			return true;
+
+		if (interfaceName.startsWith("java."))
+			return true;
+
+		if (imports != null && !imports.isEmpty()) {
+			String pack = interfaceName;
+			int n = pack.lastIndexOf('.');
+			if (n > 0)
+				pack = pack.substring(0, n);
+			else
+				pack = ".";
+
+			if (imports.containsKey(pack))
+				return true;
+		}
+		int n = interfaceName.lastIndexOf('.');
+		if (n > 0 && n + 1 < interfaceName.length()
+				&& Character.isUpperCase(interfaceName.charAt(n + 1))) {
+			return checkClass(interfaceName.substring(0, n) + '$' + interfaceName.substring(n + 1));
+		}
+		return false;
+	}
+
+	/**
+	 * 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 manifests
+	 * @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.
+	 * 
+	 * The following method is really tricky and evolved over time. Coming from
+	 * the original background of OSGi, it was a weird idea for me to have a
+	 * public package that should not be substitutable. I was so much convinced
+	 * that this was the right rule that I rücksichtlos imported them all. Alas,
+	 * the real world was more subtle than that. It turns out that it is not a
+	 * good idea to always import. First, there must be a need to import, i.e.
+	 * there must be a contained package that refers to the exported package for
+	 * it to make use importing that package. Second, if an exported package
+	 * refers to an internal package than it should not be imported.
+	 * 
+	 * Additionally, it is necessary to treat the exports in groups. If an
+	 * exported package refers to another exported packages than it must be in
+	 * the same group. A framework can only substitute exports for imports for
+	 * the whole of such a group. WHY????? Not clear anymore ...
+	 * 
+	 */
+	/**
+	 * I could no longer argue why the groups are needed :-( See what happens
+	 * ... The getGroups calculated the groups and then removed the imports from
+	 * there. Now we only remove imports that have internal references. Using
+	 * internal code for an exported package means that a bundle cannot import
+	 * that package from elsewhere because its assumptions might be violated if
+	 * it receives a substitution. //
+	 */
+	Map<String, Map<String, String>> doExportsToImports(Map<String, Map<String, String>> exports) {
+
+		// private packages = contained - exported.
+		Set<String> privatePackages = new HashSet<String>(contained.keySet());
+		privatePackages.removeAll(exports.keySet());
+
+		// private references = ∀ p : private packages | uses(p)
+		Set<String> privateReferences = newSet();
+		for (String p : privatePackages) {
+			Set<String> uses = this.uses.get(p);
+			if (uses != null)
+				privateReferences.addAll(uses);
+		}
+
+		// Assume we are going to export all exported packages
+		Set<String> toBeImported = new HashSet<String>(exports.keySet());
+
+		// Remove packages that are not referenced privately
+		toBeImported.retainAll(privateReferences);
+
+		// Not necessary to import anything that is already
+		// imported in the Import-Package statement.
+		if (imports != null)
+			toBeImported.removeAll(imports.keySet());
+
+		// Remove exported packages that are referring to
+		// private packages.
+		// Each exported package has a uses clause. We just use
+		// the used packages for each exported package to find out
+		// if it refers to an internal package.
+		//
+
+		for (Iterator<String> i = toBeImported.iterator(); i.hasNext();) {
+			Set<String> usedByExportedPackage = this.uses.get(i.next());
+			for (String privatePackage : privatePackages) {
+				if (usedByExportedPackage.contains(privatePackage)) {
+					i.remove();
+					break;
+				}
+			}
+		}
+
+		// Clean up attributes and generate result map
+		Map<String, Map<String, String>> result = newMap();
+		for (Iterator<String> i = toBeImported.iterator(); i.hasNext();) {
+			String ep = i.next();
+			Map<String, String> parameters = exports.get(ep);
+
+			String noimport = parameters.get(NO_IMPORT_DIRECTIVE);
+			if (noimport != null && noimport.equalsIgnoreCase("true"))
+				continue;
+
+			// // we can't substitute when there is no version
+			// String version = parameters.get(VERSION_ATTRIBUTE);
+			// if (version == null) {
+			// if (isPedantic())
+			// warning(
+			// "Cannot automatically import exported package %s because it has no version defined",
+			// ep);
+			// continue;
+			// }
+
+			parameters = newMap(parameters);
+			parameters.remove(VERSION_ATTRIBUTE);
+			result.put(ep, parameters);
+		}
+		return result;
+	}
+
+	private <T> boolean intersects(Collection<T> aa, Collection<T> bb) {
+		if (aa.equals(bb))
+			return true;
+
+		if (aa.size() > bb.size())
+			return intersects(bb, aa);
+
+		for (T t : aa)
+			if (bb.contains(t))
+				return true;
+		return false;
+	}
+
+	public boolean referred(String packageName) {
+		// return true;
+		for (Map.Entry<String, Set<String>> contained : uses.entrySet()) {
+			if (!contained.getKey().equals(packageName)) {
+				if (contained.getValue().contains(packageName))
+					return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Create the imports/exports by parsing
+	 * 
+	 * @throws IOException
+	 */
+	void analyzeClasspath() throws Exception {
+		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();
+					try {
+						String version = parsePackageInfo(in);
+						setPackageInfo(dir, VERSION_ATTRIBUTE, version);
+					} finally {
+						in.close();
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * 
+	 * @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) {
+						for (Map.Entry<String, Map<String, String>> entry : exported.entrySet()) {
+							if (!classpathExports.containsKey(entry.getKey())) {
+								classpathExports.put(entry.getKey(), entry.getValue());
+							}
+						}
+					}
+				}
+			}
+		} 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));
+				}
+
+				fixupAttributes(importAttributes);
+				removeAttributes(importAttributes);
+
+			} finally {
+				unsetProperty(CURRENT_PACKAGE);
+			}
+		}
+	}
+
+	/**
+	 * Provide any macro substitutions and versions for exported packages.
+	 */
+
+	void augmentExports() {
+		for (String packageName : exports.keySet()) {
+			setProperty(CURRENT_PACKAGE, packageName);
+			try {
+				Map<String, String> attributes = exports.get(packageName);
+				Map<String, String> exporterAttributes = classpathExports.get(packageName);
+				if (exporterAttributes == null)
+					continue;
+
+				for (Map.Entry<String, String> entry : exporterAttributes.entrySet()) {
+					String key = entry.getKey();
+					if (key.equalsIgnoreCase(SPECIFICATION_VERSION))
+						key = VERSION_ATTRIBUTE;
+					if (!key.endsWith(":") && !attributes.containsKey(key)) {
+						attributes.put(key, entry.getValue());
+					}
+				}
+
+				fixupAttributes(attributes);
+				removeAttributes(attributes);
+
+			} finally {
+				unsetProperty(CURRENT_PACKAGE);
+			}
+		}
+	}
+
+	/**
+	 * Fixup Attributes
+	 * 
+	 * Execute any macros on an export and
+	 */
+
+	void fixupAttributes(Map<String, String> attributes) {
+		// Convert any attribute values that have macros.
+		for (String key : attributes.keySet()) {
+			String value = attributes.get(key);
+			if (value.indexOf('$') >= 0) {
+				value = getReplacer().process(value);
+				attributes.put(key, value);
+			}
+		}
+
+	}
+
+	/*
+	 * Remove the attributes mentioned in the REMOVE_ATTRIBUTE_DIRECTIVE.
+	 */
+
+	void removeAttributes(Map<String, String> attributes) {
+		// 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 = attributes.remove(REMOVE_ATTRIBUTE_DIRECTIVE);
+		Instruction removeInstr = null;
+
+		if (remove != null)
+			removeInstr = Instruction.getPattern(remove);
+
+		for (Iterator<Map.Entry<String, String>> i = attributes.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 ...
+			}
+		}
+	}
+
+	/**
+	 * 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_ATTRIBUTE);
+		if (exportVersion == null)
+			return;
+
+		exportVersion = cleanupVersion(exportVersion);
+		String importRange = currentAttributes.get(VERSION_ATTRIBUTE);
+		boolean impl = isTrue(currentAttributes.get(PROVIDE_DIRECTIVE));
+		try {
+			setProperty("@", exportVersion);
+
+			if (importRange != null) {
+				importRange = cleanupVersion(importRange);
+				importRange = getReplacer().process(importRange);
+			} else
+				importRange = getVersionPolicy(impl);
+
+		} finally {
+			unsetProperty("@");
+		}
+		// See if we can borrow the version
+		// we must 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_ATTRIBUTE, importRange);
+	}
+
+	/**
+	 * Calculate a version from a version policy.
+	 * 
+	 * @param version
+	 *            The actual exported version
+	 * @param impl
+	 *            true for implementations and false for clients
+	 */
+
+	String calculateVersionRange(String version, boolean impl) {
+		setProperty("@", version);
+		try {
+			return getVersionPolicy(impl);
+		} finally {
+			unsetProperty("@");
+		}
+	}
+
+	/**
+	 * Add the uses clauses. This method iterates over the exports and cal
+	 * 
+	 * @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 {
+				doUses(packageName, exports, uses, imports);
+			} finally {
+				unsetProperty(CURRENT_PACKAGE);
+			}
+
+		}
+	}
+
+	/**
+	 * @param packageName
+	 * @param exports
+	 * @param uses
+	 * @param imports
+	 */
+	protected void doUses(String packageName, Map<String, Map<String, String>> exports,
+			Map<String, Set<String>> uses, Map<String, Map<String, String>> imports) {
+		Map<String, String> clause = exports.get(packageName);
+
+		// Check if someone already set the uses: directive
+		String override = clause.get(USES_DIRECTIVE);
+		if (override == null)
+			override = USES_USES;
+
+		// Get the used packages
+		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_DIRECTIVE, override);
+			}
+		}
+	}
+
+	/**
+	 * 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);
+			}
+			if (!map.containsKey(VERSION_ATTRIBUTE))
+				map.put(key, value);
+			else if (!map.get(VERSION_ATTRIBUTE).equals(value)) {
+				// System.out.println("duplicate version info for " + dir + " "
+				// + value + " and " + map.get(VERSION_ATTRIBUTE));
+			}
+		}
+	}
+
+	public void close() {
+		if (diagnostics) {
+			PrintStream out = System.out;
+			out.printf("Current directory            : %s\n", new File("").getAbsolutePath());
+			out.println("Classpath used");
+			for (Jar jar : getClasspath()) {
+				out.printf("File                                : %s\n", jar.getSource());
+				out.printf("File abs path                       : %s\n", jar.getSource()
+						.getAbsolutePath());
+				out.printf("Name                                : %s\n", jar.getName());
+				Map<String, Map<String, Resource>> dirs = jar.getDirectories();
+				for (Map.Entry<String, Map<String, Resource>> entry : dirs.entrySet()) {
+					Map<String, Resource> dir = entry.getValue();
+					String name = entry.getKey().replace('/', '.');
+					if (dir != null) {
+						out.printf("                                      %-30s %d\n", name,
+								dir.size());
+					} else {
+						out.printf("                                      %-30s <<empty>>\n", name);
+					}
+				}
+			}
+		}
+
+		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);
+					else
+						warning("Cannot find entry on -classpath: %s", s);
+				}
+		}
+		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;
+	}
+
+	protected 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 Exception {
+		Map<String, Clazz> classSpace = new HashMap<String, Clazz>();
+		Set<String> hide = Create.set();
+		boolean containsDirectory = false;
+		
+		for (String path : bundleClasspath.keySet()) {
+			if ( dot.getDirectories().containsKey(path)) {
+				containsDirectory = true;
+				break;
+			}
+		}
+		
+		if (bundleClasspath.isEmpty()) {
+			analyzeJar(dot, "", classSpace, contained, referred, uses, hide, true);
+		} else {
+			for (String path : bundleClasspath.keySet()) {
+				Map<String, String> info = bundleClasspath.get(path);
+
+				if (path.equals(".")) {
+					analyzeJar(dot, "", classSpace, contained, referred, uses, hide, !containsDirectory);
+					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, hide, true);
+					} catch (Exception e) {
+						warning("Invalid bundle classpath entry: " + path + " " + e);
+					}
+				} else {
+					if (dot.getDirectories().containsKey(path)) {
+						// if directories are used, we should not have dot as we
+						// would have the classes in these directories on the
+						// class
+						// path twice.
+						if (bundleClasspath.containsKey("."))
+							warning("Bundle-ClassPath uses a directory '%s' as well as '.', this implies the directory is seen \n"
+									+ "twice by the class loader. bnd assumes that the classes are only "
+									+ "loaded from '%s'. It is better to unroll the directory to create a flat bundle.",
+									path, path);
+						analyzeJar(dot, Processor.appendPath(path) + "/", classSpace, contained,
+								referred, uses, hide,true);
+					} else {
+						if (!"optional".equals(info.get(RESOLUTION_DIRECTIVE)))
+							warning("No sub JAR or directory " + path);
+					}
+				}
+			}
+
+			for (Clazz c : classSpace.values()) {
+				formats.add(c.getFormat());
+			}
+		}
+		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, Set<String> hide, boolean reportWrongPath) throws Exception {
+
+		next: for (String path : jar.getResources().keySet()) {
+			if (path.startsWith(prefix) /* && !hide.contains(path) */) {
+				hide.add(path);
+				String relativePath = path.substring(prefix.length());
+
+				// // TODO this check (and the whole hide) is likely redundant
+				// // it only protects against repeated checks for non-class
+				// // bundle resources, but should not affect results otherwise.
+				// if (!hide.add(relativePath)) {
+				// continue;
+				// }
+
+				// Check if we'd already had this one.
+				// Notice that we're flattening the space because
+				// this is what class loaders do.
+				if (classSpace.containsKey(relativePath))
+					continue;
+
+				String pack = getPackage(relativePath);
+
+				if (pack != null && !contained.containsKey(pack)) {
+					// For each package we encounter for the first
+					// time
+					if (!isMetaData(relativePath)) {
+
+						Map<String, String> info = newMap();
+						contained.put(pack, info);
+
+						Resource pinfo = jar.getResource(prefix + pack.replace('.', '/')
+								+ "/packageinfo");
+						if (pinfo != null) {
+							InputStream in = pinfo.openInputStream();
+							String version;
+							try {
+								version = parsePackageInfo(in);
+							} finally {
+								in.close();
+							}
+							if (version != null)
+								info.put(VERSION_ATTRIBUTE, version);
+						}
+					}
+				}
+
+				// Check class resources, we need to analyze them
+				if (path.endsWith(".class")) {
+					Resource resource = jar.getResource(path);
+					Clazz clazz;
+
+					try {
+						InputStream in = resource.openInputStream();
+						clazz = new Clazz(relativePath, resource);
+						try {
+							// Check if we have a package-info
+							if (relativePath.endsWith("/package-info.class")) {
+								// package-info can contain an Export annotation
+								Map<String, String> info = contained.get(pack);
+								parsePackageInfoClass(clazz, info);
+							} else {
+								// Otherwise we just parse it simply
+								clazz.parseClassFile();
+							}
+						} finally {
+							in.close();
+						}
+					} catch (Throwable e) {
+						error("Invalid class file: " + relativePath, e);
+						e.printStackTrace();
+						continue next;
+					}
+
+					String calculatedPath = clazz.getClassName() + ".class";
+					if (!calculatedPath.equals(relativePath)) {
+						if (!isNoBundle() && reportWrongPath) {
+							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);
+
+					// Look at the referred packages
+					// and copy them to our baseline
+					for (String p : clazz.getReferred()) {
+						Map<String, String> attrs = referred.get(p);
+						if (attrs == null) {
+							attrs = newMap();
+							referred.put(p, attrs);
+						}
+					}
+
+					// 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());
+					t.remove(pack);
+				}
+			}
+		}
+	}
+
+	static Pattern	OBJECT_REFERENCE	= Pattern.compile("L([^/]+/)*([^;]+);");
+
+	private void parsePackageInfoClass(final Clazz clazz, final Map<String, String> info)
+			throws Exception {
+		clazz.parseClassFileWithCollector(new ClassDataCollector() {
+			@Override public void annotation(Annotation a) {
+				if (a.name.equals(Clazz.toDescriptor(aQute.bnd.annotation.Version.class))) {
+
+					// Check version
+					String version = a.get("value");
+					if (!info.containsKey(Constants.VERSION_ATTRIBUTE)) {
+						if (version != null) {
+							version = getReplacer().process(version);
+							if (Verifier.VERSION.matcher(version).matches())
+								info.put(VERSION_ATTRIBUTE, version);
+							else
+								error("Export annotatio in %s has invalid version info: %s", clazz,
+										version);
+						}
+					} else {
+						// Verify this matches with packageinfo
+						String presentVersion = info.get(VERSION_ATTRIBUTE);
+						try {
+							Version av = new Version(presentVersion);
+							Version bv = new Version(version);
+							if (!av.equals(bv)) {
+								error("Version from annotation for %s differs with packageinfo or Manifest",
+										Clazz.getPackage(clazz.className));
+							}
+						} catch (Exception e) {
+							// Ignore
+						}
+					}
+				} else if (a.name.equals(Clazz.toDescriptor(Export.class))) {
+
+					// Check mandatory attributes
+					Map<String, String> attrs = doAttrbutes((Object[]) a.get(Export.MANDATORY),
+							clazz, getReplacer());
+					if (!attrs.isEmpty()) {
+						info.putAll(attrs);
+						info.put(MANDATORY_DIRECTIVE, Processor.join(attrs.keySet()));
+					}
+
+					// Check optional attributes
+					attrs = doAttrbutes((Object[]) a.get(Export.OPTIONAL), clazz, getReplacer());
+					if (!attrs.isEmpty()) {
+						info.putAll(attrs);
+					}
+
+					// Check Included classes
+					Object[] included = a.get(Export.INCLUDE);
+					if (included != null && included.length > 0) {
+						StringBuilder sb = new StringBuilder();
+						String del = "";
+						for (Object i : included) {
+							Matcher m = OBJECT_REFERENCE.matcher((String) i);
+							if (m.matches()) {
+								sb.append(del);
+								sb.append(m.group(2));
+								del = ",";
+							}
+						}
+						info.put(INCLUDE_DIRECTIVE, sb.toString());
+					}
+
+					// Check Excluded classes
+					Object[] excluded = a.get(Export.EXCLUDE);
+					if (excluded != null && excluded.length > 0) {
+						StringBuilder sb = new StringBuilder();
+						String del = "";
+						for (Object i : excluded) {
+							Matcher m = OBJECT_REFERENCE.matcher((String) i);
+							if (m.matches()) {
+								sb.append(del);
+								sb.append(m.group(2));
+								del = ",";
+							}
+						}
+						info.put(EXCLUDE_DIRECTIVE, sb.toString());
+					}
+
+					// Check Uses
+					Object[] uses = a.get(Export.USES);
+					if (uses != null && uses.length > 0) {
+						String old = info.get(USES_DIRECTIVE);
+						if (old == null)
+							old = "";
+						StringBuilder sb = new StringBuilder(old);
+						String del = sb.length() == 0 ? "" : ",";
+
+						for (Object use : uses) {
+							sb.append(del);
+							sb.append(use);
+							del = ",";
+						}
+						info.put(USES_DIRECTIVE, sb.toString());
+					}
+				}
+			}
+
+		});
+	}
+
+	/**
+	 * 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) {
+		Matcher m = Verifier.VERSIONRANGE.matcher(version);
+
+		if (m.matches()) {
+			return version;
+		}
+
+		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 = removeLeadingZeroes(m.group(1));
+				String minor = removeLeadingZeroes(m.group(3));
+				String micro = removeLeadingZeroes(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;
+	}
+
+	private static String removeLeadingZeroes(String group) {
+		int n = 0;
+		while (group != null && n < group.length() - 1 && group.charAt(n) == '0')
+			n++;
+		if (n == 0)
+			return group;
+
+		return group.substring(n);
+	}
+
+	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;
+	}
+
+	final static String	DEFAULT_PROVIDER_POLICY	= "${range;[==,=+)}";
+	final static String	DEFAULT_CONSUMER_POLICY	= "${range;[==,+)}";
+
+	@SuppressWarnings("deprecation") public String getVersionPolicy(boolean implemented) {
+		if (implemented) {
+			String s = getProperty(PROVIDER_POLICY);
+			if (s != null)
+				return s;
+
+			s = getProperty(VERSIONPOLICY_IMPL);
+			if (s != null)
+				return s;
+
+			return getProperty(VERSIONPOLICY, DEFAULT_PROVIDER_POLICY);
+		} else {
+			String s = getProperty(CONSUMER_POLICY);
+			if (s != null)
+				return s;
+
+			s = getProperty(VERSIONPOLICY_USES);
+			if (s != null)
+				return s;
+
+			return getProperty(VERSIONPOLICY, DEFAULT_CONSUMER_POLICY);
+		}
+		// String vp = implemented ? getProperty(VERSIONPOLICY_IMPL) :
+		// getProperty(VERSIONPOLICY_USES);
+		//
+		// if (vp != null)
+		// return vp;
+		//
+		// if (implemented)
+		// return getProperty(VERSIONPOLICY_IMPL, "{$range;[==,=+}");
+		// else
+		// return getProperty(VERSIONPOLICY, "${range;[==,+)}");
+	}
+
+	/**
+	 * 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) throws Exception {
+		// Macro.verifyCommand(args, _classesHelp, new
+		// Pattern[]{null,Pattern.compile("(implementing|implements|extending|extends|importing|imports|any)"),
+		// null}, 3,3);
+
+		Collection<Clazz> matched = getClasses(args);
+		if (matched.isEmpty())
+			return "";
+
+		return join(matched);
+	}
+
+	public Collection<Clazz> getClasses(String... args) throws Exception {
+
+		Set<Clazz> matched = new HashSet<Clazz>(classspace.values());
+		for (int i = 1; i < args.length; i++) {
+			if (args.length < i + 1)
+				throw new IllegalArgumentException(
+						"${classes} macro must have odd number of arguments. " + _classesHelp);
+
+			String typeName = args[i];
+			if (typeName.equalsIgnoreCase("extending"))
+				typeName = "extends";
+			else if (typeName.equalsIgnoreCase("importing"))
+				typeName = "imports";
+			else if (typeName.equalsIgnoreCase("implementing"))
+				typeName = "implements";
+
+			Clazz.QUERY type = Clazz.QUERY.valueOf(typeName.toUpperCase());
+
+			if (type == null)
+				throw new IllegalArgumentException("${classes} has invalid type: " + typeName
+						+ ". " + _classesHelp);
+
+			Instruction instr = null;
+			if (Clazz.HAS_ARGUMENT.contains(type)) {
+				StringBuilder sb = new StringBuilder();
+				String s = args[++i];
+				if (type == QUERY.ANNOTATION) {
+					// Annotations use the descriptor format ...
+					// But at least they're always an object
+					sb.append("L");
+					for (int ci = 0; ci < s.length(); ci++) {
+						char c = s.charAt(ci);
+						if (c == '.')
+							sb.append("/");
+						else
+							sb.append(c);
+					}
+					sb.append(';');
+				} else {
+					// 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. We also have to take into account
+					// that some classes are nested and use $ for separator
+					for (int ci = 0; ci < s.length(); ci++) {
+						char c = s.charAt(ci);
+						if (c == '.')
+							sb.append("(/|\\$)");
+						else
+							sb.append(c);
+					}
+				}
+				instr = Instruction.getPattern(sb.toString());
+			}
+			for (Iterator<Clazz> c = matched.iterator(); c.hasNext();) {
+				Clazz clazz = c.next();
+				if (!clazz.is(type, instr, this)) {
+					c.remove();
+				}
+			}
+		}
+		return 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;
+	}
+
+	/**
+	 * Locate a resource on the class path.
+	 * 
+	 * @param path
+	 *            Path of the reosurce
+	 * @return A resource or <code>null</code>
+	 */
+	public Resource findResource(String path) {
+		for (Jar entry : getClasspath()) {
+			Resource r = entry.getResource(path);
+			if (r != null)
+				return r;
+		}
+		return null;
+	}
+
+	/**
+	 * Find a clazz on the class path. This class has been parsed.
+	 * 
+	 * @param path
+	 * @return
+	 */
+	public Clazz findClass(String path) throws Exception {
+		Clazz c = classspace.get(path);
+		if (c != null)
+			return c;
+
+		c = importedClassesCache.get(path);
+		if (c != null)
+			return c;
+
+		Resource r = findResource(path);
+		if (r != null) {
+			c = new Clazz(path, r);
+			c.parseClassFile();
+			importedClassesCache.put(path, c);
+		}
+		return c;
+	}
+
+	/**
+	 * Answer the bundle version.
+	 * 
+	 * @return
+	 */
+	public String getVersion() {
+		String version = getProperty(BUNDLE_VERSION);
+		if (version == null)
+			version = "0.0.0";
+		return version;
+	}
+
+	public boolean isNoBundle() {
+		return isTrue(getProperty(RESOURCEONLY)) || isTrue(getProperty(NOMANIFEST));
+	}
+
+	public void referTo(String impl) {
+		String pack = Clazz.getPackage(impl);
+		if (!referred.containsKey(pack))
+			referred.put(pack, new LinkedHashMap<String, String>());
+	}
+
+	/**
+	 * Calculate the groups inside the bundle. A group consists of packages that
+	 * have a reference to each other.
+	 */
+
+	public MultiMap<Set<String>, String> getGroups() {
+		MultiMap<String, String> map = new MultiMap<String, String>();
+		Set<String> keys = uses.keySet();
+
+		for (Map.Entry<String, Set<String>> entry : uses.entrySet()) {
+			Set<String> newSet = new HashSet<String>(entry.getValue());
+			newSet.retainAll(keys);
+			map.put(entry.getKey(), newSet);
+		}
+
+		// Calculate strongly connected packages
+		Set<Set<String>> scc = Tarjan.tarjan(map);
+
+		MultiMap<Set<String>, String> grouped = new MultiMap<Set<String>, String>();
+		for (Set<String> group : scc) {
+			for (String p : group) {
+				grouped.addAll(group, uses.get(p));
+			}
+		}
+		return grouped;
+	}
+
+	/**
+	 * Ensure that we are running on the correct bnd.
+	 */
+	void doRequireBnd() {
+		Map<String, String> require = OSGiHeader.parseProperties(getProperty(REQUIRE_BND));
+		if (require == null || require.isEmpty())
+			return;
+
+		Hashtable<String, String> map = new Hashtable<String, String>();
+		map.put(Constants.VERSION_FILTER, getBndVersion());
+
+		for (String filter : require.keySet()) {
+			try {
+				Filter f = new Filter(filter);
+				if (f.match(map))
+					continue;
+				error("%s fails %s", REQUIRE_BND, require.get(filter));
+			} catch (Exception t) {
+				error("%s with value %s throws exception", t, REQUIRE_BND, require);
+			}
+		}
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Annotation.java b/bundleplugin/src/main/java/aQute/lib/osgi/Annotation.java
new file mode 100644
index 0000000..d0e6b12
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Annotation.java
@@ -0,0 +1,59 @@
+package aQute.lib.osgi;
+
+import java.lang.annotation.*;
+import java.util.*;
+
+import aQute.bnd.annotation.metatype.*;
+
+public class Annotation {
+	String				name;
+	Map<String, Object>	elements;
+	ElementType			member;
+	RetentionPolicy		policy;
+
+	public Annotation(String name, Map<String, Object> elements, ElementType member,
+			RetentionPolicy policy) {
+		this.name = name;
+		if ( elements == null)
+			this.elements = Collections.emptyMap();
+		else
+			this.elements = elements;
+		this.member = member;
+		this.policy = policy;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public String toString() {
+		return name + ":" + member + ":" + policy + ":" + elements;
+	}
+
+	@SuppressWarnings("unchecked") public <T> T get(String string) {
+		if (elements == null)
+			return null;
+
+		return (T) elements.get(string);
+	}
+
+	public <T> void put(String string, Object v) {
+		if (elements == null)
+			return;
+
+		elements.put(string, v);
+	}
+
+	@SuppressWarnings("unchecked") public <T extends java.lang.annotation.Annotation> T getAnnotation() throws Exception {
+		String cname = Clazz.objectDescriptorToFQN(name);
+		Class<T> c = (Class<T>) getClass().getClassLoader().loadClass(cname);
+		return getAnnotation(c);
+	}
+	public <T extends java.lang.annotation.Annotation> T getAnnotation(Class<T> c)
+			throws Exception {
+		String cname = Clazz.objectDescriptorToFQN(name);
+		if ( ! c.getName().equals(cname))
+			return null;
+		return Configurable.createConfigurable(c, elements );
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Builder.java b/bundleplugin/src/main/java/aQute/lib/osgi/Builder.java
new file mode 100644
index 0000000..903aaf7
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Builder.java
@@ -0,0 +1,1217 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+import java.util.regex.*;
+import java.util.zip.*;
+
+import aQute.bnd.component.*;
+import aQute.bnd.make.*;
+import aQute.bnd.make.component.*;
+import aQute.bnd.make.metatype.*;
+import aQute.bnd.maven.*;
+import aQute.bnd.service.*;
+
+/**
+ * Include-Resource: ( [name '=' ] file )+
+ * 
+ * Private-Package: package-decl ( ',' package-decl )*
+ * 
+ * Export-Package: package-decl ( ',' package-decl )*
+ * 
+ * Import-Package: package-decl ( ',' package-decl )*
+ * 
+ * @version $Revision$
+ */
+public class Builder extends Analyzer {
+	Pattern						xdoNotCopy			= null;
+	private static final int	SPLIT_MERGE_LAST	= 1;
+	private static final int	SPLIT_MERGE_FIRST	= 2;
+	private static final int	SPLIT_ERROR			= 3;
+	private static final int	SPLIT_FIRST			= 4;
+	private static final int	SPLIT_DEFAULT		= 0;
+
+	List<File>					sourcePath			= new ArrayList<File>();
+
+	Make						make				= new Make(this);
+
+	public Builder(Processor parent) {
+		super(parent);
+	}
+
+	public Builder() {
+	}
+
+	public Jar build() throws Exception {
+		init();
+		if (isTrue(getProperty(NOBUNDLES)))
+			return null;
+
+		if (getProperty(CONDUIT) != null)
+			error("Specified " + CONDUIT
+					+ " but calls build() instead of builds() (might be a programmer error");
+
+		dot = new Jar("dot");
+		addClose(dot);
+		try {
+			long modified = Long.parseLong(getProperty("base.modified"));
+			dot.updateModified(modified, "Base modified");
+		} catch (Exception e) {
+		}
+
+		doExpand(dot);
+		doIncludeResources(dot);
+		doConditional(dot);
+		dot = doWab(dot);
+
+		// NEW!
+		// Check if we override the calculation of the
+		// manifest. We still need to calculated it because
+		// we need to have analyzed the classpath.
+
+		Manifest manifest = calcManifest();
+
+		String mf = getProperty(MANIFEST);
+		if (mf != null) {
+			File mff = getFile(mf);
+			if (mff.isFile()) {
+				try {
+					InputStream in = new FileInputStream(mff);
+					manifest = new Manifest(in);
+					in.close();
+				} catch (Exception e) {
+					error(MANIFEST + " while reading manifest file", e);
+				}
+			} else {
+				error(MANIFEST + ", no such file " + mf);
+			}
+		}
+
+		if (getProperty(NOMANIFEST) == null)
+			dot.setManifest(manifest);
+		else
+			dot.setDoNotTouchManifest();
+
+		// This must happen after we analyzed so
+		// we know what it is on the classpath
+		addSources(dot);
+
+		if (getProperty(POM) != null)
+			dot.putResource("pom.xml", new PomResource(dot.getManifest()));
+
+		if (!isNoBundle())
+			doVerify(dot);
+
+		if (dot.getResources().isEmpty())
+			error("The JAR is empty: " + dot.getName());
+
+		dot.updateModified(lastModified(), "Last Modified Processor");
+		dot.setName(getBsn());
+
+		sign(dot);
+
+		doSaveManifest(dot);
+		return dot;
+	}
+
+	/**
+	 * Allow any local initialization by subclasses before we build.
+	 */
+	public void init() throws Exception {
+		begin();
+		doRequireBnd();
+	}
+
+	/**
+	 * Turn this normal bundle in a web and add any resources.
+	 * 
+	 * @throws Exception
+	 */
+	private Jar doWab(Jar dot) throws Exception {
+		String wab = getProperty(WAB);
+		String wablib = getProperty(WABLIB);
+		if (wab == null && wablib == null)
+			return dot;
+
+		setProperty(BUNDLE_CLASSPATH, append("WEB-INF/classes", getProperty(BUNDLE_CLASSPATH)));
+
+		Jar next = new Jar(dot.getName());
+		addClose(next);
+
+		for (Map.Entry<String, Resource> entry : dot.getResources().entrySet()) {
+			String path = entry.getKey();
+			if (path.indexOf('/') > 0 && !Character.isUpperCase(path.charAt(0))) {
+				trace("wab: moving: %s", path);
+				next.putResource("WEB-INF/classes/" + path, entry.getValue());
+			} else {
+				trace("wab: not moving: %s", path);
+				next.putResource(path, entry.getValue());
+			}
+		}
+
+		Map<String, Map<String, String>> clauses = parseHeader(getProperty(WABLIB));
+		for (String key : clauses.keySet()) {
+			File f = getFile(key);
+			addWabLib(next, f);
+		}
+		doIncludeResource(next, wab);
+		return next;
+	}
+
+	/**
+	 * Add a wab lib to the jar.
+	 * 
+	 * @param f
+	 */
+	private void addWabLib(Jar dot, File f) throws Exception {
+		if (f.exists()) {
+			Jar jar = new Jar(f);
+			jar.setDoNotTouchManifest();
+			addClose(jar);
+			String path = "WEB-INF/lib/" + f.getName();
+			dot.putResource(path, new JarResource(jar));
+			setProperty(BUNDLE_CLASSPATH, append(getProperty(BUNDLE_CLASSPATH), path));
+
+			Manifest m = jar.getManifest();
+			String cp = m.getMainAttributes().getValue("Class-Path");
+			if (cp != null) {
+				Collection<String> parts = split(cp, ",");
+				for (String part : parts) {
+					File sub = getFile(f.getParentFile(), part);
+					if (!sub.exists() || !sub.getParentFile().equals(f.getParentFile())) {
+						warning("Invalid Class-Path entry %s in %s, must exist and must reside in same directory",
+								sub, f);
+					} else {
+						addWabLib(dot, sub);
+					}
+				}
+			}
+		} else {
+			error("WAB lib does not exist %s", f);
+		}
+	}
+
+	/**
+	 * Get the manifest and write it out separately if -savemanifest is set
+	 * 
+	 * @param dot
+	 */
+	private void doSaveManifest(Jar dot) throws Exception {
+		String output = getProperty(SAVEMANIFEST);
+		if (output == null)
+			return;
+
+		File f = getFile(output);
+		if (f.isDirectory()) {
+			f = new File(f, "MANIFEST.MF");
+		}
+		f.delete();
+		f.getParentFile().mkdirs();
+		OutputStream out = new FileOutputStream(f);
+		try {
+			Jar.writeManifest(dot.getManifest(), out);
+		} finally {
+			out.close();
+		}
+		changedFile(f);
+	}
+
+	protected void changedFile(File f) {
+	}
+
+	/**
+	 * Sign the jar file.
+	 * 
+	 * -sign : <alias> [ ';' 'password:=' <password> ] [ ';' 'keystore:='
+	 * <keystore> ] [ ';' 'sign-password:=' <pw> ] ( ',' ... )*
+	 * 
+	 * @return
+	 */
+
+	void sign(Jar jar) throws Exception {
+		String signing = getProperty("-sign");
+		if (signing == null)
+			return;
+
+		trace("Signing %s, with %s", getBsn(), signing);
+		List<SignerPlugin> signers = getPlugins(SignerPlugin.class);
+
+		Map<String, Map<String, String>> infos = parseHeader(signing);
+		for (Map.Entry<String, Map<String, String>> entry : infos.entrySet()) {
+			for (SignerPlugin signer : signers) {
+				signer.sign(this, entry.getKey());
+			}
+		}
+	}
+
+	public boolean hasSources() {
+		return isTrue(getProperty(SOURCES));
+	}
+
+	protected String getImportPackages() {
+		String ip = super.getImportPackages();
+		if (ip != null)
+			return ip;
+
+		return "*";
+	}
+
+	private void doConditional(Jar dot) throws Exception {
+		Map<String, Map<String, String>> conditionals = getHeader(CONDITIONAL_PACKAGE);
+		if (conditionals.isEmpty())
+			return;
+
+		while (true) {
+			analyze();
+			Map<String, Map<String, String>> imports = getImports();
+
+			// Match the packages specified in conditionals
+			// against the imports. Any match must become a
+			// Private-Package
+			Map<String, Map<String, String>> filtered = merge(CONDITIONAL_PACKAGE, conditionals,
+					imports, new HashSet<String>(), null);
+
+			// Imports can also specify a private import. These
+			// packages must also be copied to the bundle
+			for (Map.Entry<String, Map<String, String>> entry : getImports().entrySet()) {
+				String type = entry.getValue().get(IMPORT_DIRECTIVE);
+				if (type != null && type.equals(PRIVATE_DIRECTIVE))
+					filtered.put(entry.getKey(), entry.getValue());
+			}
+
+			// remove existing packages to prevent merge errors
+			filtered.keySet().removeAll(dot.getPackages());
+			if (filtered.size() == 0)
+				break;
+
+			int size = dot.getResources().size();
+			doExpand(dot, CONDITIONAL_PACKAGE + " Private imports",
+					Instruction.replaceWithInstruction(filtered), false);
+
+			// Were there any expansions?
+			if (size == dot.getResources().size())
+				break;
+
+			analyzed = false;
+		}
+	}
+
+	/**
+	 * Intercept the call to analyze and cleanup versions after we have analyzed
+	 * the setup. We do not want to cleanup if we are going to verify.
+	 */
+
+	public void analyze() throws Exception {
+		super.analyze();
+		cleanupVersion(imports, null);
+		cleanupVersion(exports, getVersion());
+		String version = getProperty(BUNDLE_VERSION);
+		if (version != null) {
+			version = cleanupVersion(version);
+			if (version.endsWith(".SNAPSHOT")) {
+				version = version.replaceAll("SNAPSHOT$", getProperty(SNAPSHOT, "SNAPSHOT"));
+			}
+			setProperty(BUNDLE_VERSION, version);
+		}
+	}
+
+	public void cleanupVersion(Map<String, Map<String, String>> mapOfMap, String defaultVersion) {
+		for (Iterator<Map.Entry<String, Map<String, String>>> e = mapOfMap.entrySet().iterator(); e
+				.hasNext();) {
+			Map.Entry<String, Map<String, String>> entry = e.next();
+			Map<String, String> attributes = entry.getValue();
+			String v = attributes.get(Constants.VERSION_ATTRIBUTE);
+			if (v == null && defaultVersion != null) {
+				if (!isTrue(getProperty(Constants.NODEFAULTVERSION))) {
+					v = defaultVersion;
+					if (isPedantic())
+						warning("Used bundle version %s for exported package %s", v, entry.getKey());
+				} else {
+					if (isPedantic())
+						warning("No export version for exported package %s", entry.getKey());
+				}
+			}
+			if (v != null)
+				attributes.put(Constants.VERSION_ATTRIBUTE, cleanupVersion(v));
+		}
+	}
+
+	/**
+     * 
+     */
+	private void addSources(Jar dot) {
+		if (!hasSources())
+			return;
+
+		Set<String> packages = new HashSet<String>();
+
+		try {
+			ByteArrayOutputStream out = new ByteArrayOutputStream();
+			getProperties().store(out, "Generated by BND, at " + new Date());
+			dot.putResource("OSGI-OPT/bnd.bnd", new EmbeddedResource(out.toByteArray(), 0));
+			out.close();
+		} catch (Exception e) {
+			error("Can not embed bnd file in JAR: " + e);
+		}
+
+		for (Iterator<String> cpe = classspace.keySet().iterator(); cpe.hasNext();) {
+			String path = cpe.next();
+			path = path.substring(0, path.length() - ".class".length()) + ".java";
+			String pack = getPackage(path).replace('.', '/');
+			if (pack.length() > 1)
+				pack = pack + "/";
+			boolean found = false;
+			String[] fixed = { "packageinfo", "package.html", "module-info.java",
+					"package-info.java" };
+			for (Iterator<File> i = getSourcePath().iterator(); i.hasNext();) {
+				File root = i.next();
+				File f = getFile(root, path);
+				if (f.exists()) {
+					found = true;
+					if (!packages.contains(pack)) {
+						packages.add(pack);
+						File bdir = getFile(root, pack);
+						for (int j = 0; j < fixed.length; j++) {
+							File ff = getFile(bdir, fixed[j]);
+							if (ff.isFile()) {
+								dot.putResource("OSGI-OPT/src/" + pack + fixed[j],
+										new FileResource(ff));
+							}
+						}
+					}
+					dot.putResource("OSGI-OPT/src/" + path, new FileResource(f));
+				}
+			}
+			if (!found) {
+				for (Jar jar : classpath) {
+					Resource resource = jar.getResource(path);
+					if (resource != null) {
+						dot.putResource("OSGI-OPT/src/"+path, resource);
+					} else {
+						resource = jar.getResource("OSGI-OPT/src/" + path);
+						if (resource != null) {
+							dot.putResource("OSGI-OPT/src/"+path, resource);
+						}
+					}
+				}
+			}
+			if (getSourcePath().isEmpty())
+				warning("Including sources but " + SOURCEPATH
+						+ " does not contain any source directories ");
+			// TODO copy from the jars where they came from
+		}
+	}
+
+	boolean	firstUse	= true;
+
+	public Collection<File> getSourcePath() {
+		if (firstUse) {
+			firstUse = false;
+			String sp = getProperty(SOURCEPATH);
+			if (sp != null) {
+				Map<String, Map<String, String>> map = parseHeader(sp);
+				for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
+					String file = i.next();
+					if (!isDuplicate(file)) {
+						File f = getFile(file);
+						if (!f.isDirectory()) {
+							error("Adding a sourcepath that is not a directory: " + f);
+						} else {
+							sourcePath.add(f);
+						}
+					}
+				}
+			}
+		}
+		return sourcePath;
+	}
+
+	private void doVerify(Jar dot) throws Exception {
+		Verifier verifier = new Verifier(dot, getProperties());
+		verifier.setPedantic(isPedantic());
+
+		// Give the verifier the benefit of our analysis
+		// prevents parsing the files twice
+		verifier.setClassSpace(classspace, contained, referred, uses);
+		verifier.verify();
+		getInfo(verifier);
+	}
+
+	private void doExpand(Jar jar) throws IOException {
+		if (getClasspath().size() == 0
+				&& (getProperty(EXPORT_PACKAGE) != null || getProperty(EXPORT_PACKAGE) != null || getProperty(PRIVATE_PACKAGE) != null))
+			warning("Classpath is empty. Private-Package and Export-Package can only expand from the classpath when there is one");
+
+		Map<Instruction, Map<String, String>> privateMap = Instruction
+				.replaceWithInstruction(getHeader(PRIVATE_PACKAGE));
+		Map<Instruction, Map<String, String>> exportMap = Instruction
+				.replaceWithInstruction(getHeader(EXPORT_PACKAGE));
+
+		if (isTrue(getProperty(Constants.UNDERTEST))) {
+			privateMap.putAll(Instruction.replaceWithInstruction(parseHeader(getProperty(
+					Constants.TESTPACKAGES, "test;presence:=optional"))));
+		}
+		if (!privateMap.isEmpty())
+			doExpand(jar, "Private-Package, or -testpackages", privateMap, true);
+
+		if (!exportMap.isEmpty()) {
+			Jar exports = new Jar("exports");
+			doExpand(exports, EXPORT_PACKAGE, exportMap, true);
+			jar.addAll(exports);
+			exports.close();
+		}
+
+		if (!isNoBundle()) {
+			if (privateMap.isEmpty() && exportMap.isEmpty() && !isResourceOnly()
+					&& getProperty(EXPORT_CONTENTS) == null) {
+				warning("None of Export-Package, Provide-Package, Private-Package, -testpackages, or -exportcontents is set, therefore no packages will be included");
+			}
+		}
+	}
+
+	/**
+	 * 
+	 * @param jar
+	 * @param name
+	 * @param instructions
+	 */
+	private void doExpand(Jar jar, String name, Map<Instruction, Map<String, String>> instructions,
+			boolean mandatory) {
+		Set<Instruction> superfluous = removeMarkedDuplicates(instructions.keySet());
+
+		for (Iterator<Jar> c = getClasspath().iterator(); c.hasNext();) {
+			Jar now = c.next();
+			doExpand(jar, instructions, now, superfluous);
+		}
+
+		if (mandatory && superfluous.size() > 0) {
+			StringBuilder sb = new StringBuilder();
+			String del = "Instructions in " + name + " that are never used: ";
+			for (Iterator<Instruction> i = superfluous.iterator(); i.hasNext();) {
+				Instruction p = i.next();
+				sb.append(del);
+				sb.append(p.toString());
+				del = "\n                ";
+			}
+			sb.append("\nClasspath: ");
+			sb.append(Processor.join(getClasspath()));
+			sb.append("\n");
+
+			warning(sb.toString());
+			if (isPedantic())
+				diagnostics = true;
+		}
+	}
+
+	/**
+	 * Iterate over each directory in the class path entry and check if that
+	 * directory is a desired package.
+	 * 
+	 * @param included
+	 * @param classpathEntry
+	 */
+	private void doExpand(Jar jar, Map<Instruction, Map<String, String>> included,
+			Jar classpathEntry, Set<Instruction> superfluous) {
+
+		loop: for (Map.Entry<String, Map<String, Resource>> directory : classpathEntry
+				.getDirectories().entrySet()) {
+			String path = directory.getKey();
+
+			if (doNotCopy(getName(path)))
+				continue;
+
+			if (directory.getValue() == null)
+				continue;
+
+			String pack = path.replace('/', '.');
+			Instruction instr = matches(included, pack, superfluous, classpathEntry.getName());
+			if (instr != null) {
+				// System.out.println("Pattern match: " + pack + " " +
+				// instr.getPattern() + " " + instr.isNegated());
+				if (!instr.isNegated()) {
+					Map<String, Resource> contents = directory.getValue();
+
+					// What to do with split packages? Well if this
+					// directory already exists, we will check the strategy
+					// and react accordingly.
+					boolean overwriteResource = true;
+					if (jar.hasDirectory(path)) {
+						Map<String, String> directives = included.get(instr);
+
+						switch (getSplitStrategy((String) directives.get(SPLIT_PACKAGE_DIRECTIVE))) {
+						case SPLIT_MERGE_LAST:
+							overwriteResource = true;
+							break;
+
+						case SPLIT_MERGE_FIRST:
+							overwriteResource = false;
+							break;
+
+						case SPLIT_ERROR:
+							error(diagnostic(pack, getClasspath(), classpathEntry.source));
+							continue loop;
+
+						case SPLIT_FIRST:
+							continue loop;
+
+						default:
+							warning(diagnostic(pack, getClasspath(), classpathEntry.source));
+							overwriteResource = false;
+							break;
+						}
+					}
+
+					jar.addDirectory(contents, overwriteResource);
+
+					String key = path + "/bnd.info";
+					Resource r = jar.getResource(key);
+					if (r != null)
+						jar.putResource(key, new PreprocessResource(this, r));
+
+					if (hasSources()) {
+						String srcPath = "OSGI-OPT/src/" + path;
+						Map<String, Resource> srcContents = classpathEntry.getDirectories().get(
+								srcPath);
+						if (srcContents != null) {
+							jar.addDirectory(srcContents, overwriteResource);
+						}
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Analyze the classpath for a split package
+	 * 
+	 * @param pack
+	 * @param classpath
+	 * @param source
+	 * @return
+	 */
+	private String diagnostic(String pack, List<Jar> classpath, File source) {
+		// Default is like merge-first, but with a warning
+		// Find the culprits
+		pack = pack.replace('.', '/');
+		List<Jar> culprits = new ArrayList<Jar>();
+		for (Iterator<Jar> i = classpath.iterator(); i.hasNext();) {
+			Jar culprit = (Jar) i.next();
+			if (culprit.getDirectories().containsKey(pack)) {
+				culprits.add(culprit);
+			}
+		}
+		return "Split package "
+				+ pack
+				+ "\nUse directive -split-package:=(merge-first|merge-last|error|first) on Export/Private Package instruction to get rid of this warning\n"
+				+ "Package found in   " + culprits + "\n" + "Reference from     " + source + "\n"
+				+ "Classpath          " + classpath;
+	}
+
+	private int getSplitStrategy(String type) {
+		if (type == null)
+			return SPLIT_DEFAULT;
+
+		if (type.equals("merge-last"))
+			return SPLIT_MERGE_LAST;
+
+		if (type.equals("merge-first"))
+			return SPLIT_MERGE_FIRST;
+
+		if (type.equals("error"))
+			return SPLIT_ERROR;
+
+		if (type.equals("first"))
+			return SPLIT_FIRST;
+
+		error("Invalid strategy for split-package: " + type);
+		return SPLIT_DEFAULT;
+	}
+
+	/**
+	 * Matches the instructions against a package.
+	 * 
+	 * @param instructions
+	 *            The list of instructions
+	 * @param pack
+	 *            The name of the package
+	 * @param superfluousPatterns
+	 *            The total list of patterns, matched patterns are removed
+	 * @param source
+	 *            The name of the source container, can be filtered upon with
+	 *            the from: directive.
+	 * @return
+	 */
+	private Instruction matches(Map<Instruction, Map<String, String>> instructions, String pack,
+			Set<Instruction> superfluousPatterns, String source) {
+		for (Map.Entry<Instruction, Map<String, String>> entry : instructions.entrySet()) {
+			Instruction pattern = entry.getKey();
+
+			// It is possible to filter on the source of the
+			// package with the from: directive. This is an
+			// instruction that must match the name of the
+			// source class path entry.
+
+			String from = entry.getValue().get(FROM_DIRECTIVE);
+			if (from != null) {
+				Instruction f = Instruction.getPattern(from);
+				if (!f.matches(source) || f.isNegated())
+					return null;
+			}
+
+			// Now do the normal
+			// matching
+			if (pattern.matches(pack)) {
+				if (superfluousPatterns != null)
+					superfluousPatterns.remove(pattern);
+				return pattern;
+			}
+		}
+		return null;
+	}
+
+	private Map<String, Map<String, String>> getHeader(String string) {
+		if (string == null)
+			return Collections.emptyMap();
+		return parseHeader(getProperty(string));
+	}
+
+	/**
+	 * Parse the Bundle-Includes header. Files in the bundles Include header are
+	 * included in the jar. The source can be a directory or a file.
+	 * 
+	 * @throws IOException
+	 * @throws FileNotFoundException
+	 */
+	private void doIncludeResources(Jar jar) throws Exception {
+		String includes = getProperty("Bundle-Includes");
+		if (includes == null) {
+			includes = getProperty(INCLUDERESOURCE);
+			if (includes == null || includes.length() == 0)
+				includes = getProperty("Include-Resource");
+		} else
+			warning("Please use -includeresource instead of Bundle-Includes");
+
+		doIncludeResource(jar, includes);
+
+	}
+
+	private void doIncludeResource(Jar jar, String includes) throws Exception {
+		Map<String, Map<String, String>> clauses = parseHeader(includes);
+		doIncludeResource(jar, clauses);
+	}
+
+	private void doIncludeResource(Jar jar, Map<String, Map<String, String>> clauses)
+			throws ZipException, IOException, Exception {
+		for (Map.Entry<String, Map<String, String>> entry : clauses.entrySet()) {
+			doIncludeResource(jar, entry.getKey(), entry.getValue());
+		}
+	}
+
+	private void doIncludeResource(Jar jar, String name, Map<String, String> extra)
+			throws ZipException, IOException, Exception {
+		boolean preprocess = false;
+		if (name.startsWith("{") && name.endsWith("}")) {
+			preprocess = true;
+			name = name.substring(1, name.length() - 1).trim();
+		}
+
+		String parts[] = name.split("\\s*=\\s*");
+		String source = parts[0];
+		String destination = parts[0];
+		if (parts.length == 2)
+			source = parts[1];
+
+		if (source.startsWith("@")) {
+			extractFromJar(jar, source.substring(1), parts.length == 1 ? "" : destination);
+		} else if (extra.containsKey("literal")) {
+			String literal = (String) extra.get("literal");
+			Resource r = new EmbeddedResource(literal.getBytes("UTF-8"), 0);
+			String x = (String) extra.get("extra");
+			if (x != null)
+				r.setExtra(x);
+			jar.putResource(name, r);
+		} else {
+			File sourceFile;
+			String destinationPath;
+
+			sourceFile = getFile(source);
+			if (parts.length == 1) {
+				// Directories should be copied to the root
+				// but files to their file name ...
+				if (sourceFile.isDirectory())
+					destinationPath = "";
+				else
+					destinationPath = sourceFile.getName();
+			} else {
+				destinationPath = parts[0];
+			}
+			// Handle directories
+			if (sourceFile.isDirectory()) {
+				destinationPath = doResourceDirectory(jar, extra, preprocess, sourceFile,
+						destinationPath);
+				return;
+			}
+
+			// destinationPath = checkDestinationPath(destinationPath);
+
+			if (!sourceFile.exists()) {
+				noSuchFile(jar, name, extra, source, destinationPath);
+			} else
+				copy(jar, destinationPath, sourceFile, preprocess, extra);
+		}
+	}
+
+	private String doResourceDirectory(Jar jar, Map<String, String> extra, boolean preprocess,
+			File sourceFile, String destinationPath) throws Exception {
+		String filter = extra.get("filter:");
+		boolean flatten = isTrue(extra.get("flatten:"));
+		boolean recursive = true;
+		String directive = extra.get("recursive:");
+		if (directive != null) {
+			recursive = isTrue(directive);
+		}
+
+		InstructionFilter iFilter = null;
+		if (filter != null) {
+			iFilter = new InstructionFilter(Instruction.getPattern(filter), recursive,
+					getDoNotCopy());
+		} else {
+			iFilter = new InstructionFilter(null, recursive, getDoNotCopy());
+		}
+
+		Map<String, File> files = newMap();
+		resolveFiles(sourceFile, iFilter, recursive, destinationPath, files, flatten);
+
+		for (Map.Entry<String, File> entry : files.entrySet()) {
+			copy(jar, entry.getKey(), entry.getValue(), preprocess, extra);
+		}
+		return destinationPath;
+	}
+
+	private void resolveFiles(File dir, FileFilter filter, boolean recursive, String path,
+			Map<String, File> files, boolean flatten) {
+
+		if (doNotCopy(dir.getName())) {
+			return;
+		}
+
+		File[] fs = dir.listFiles(filter);
+		for (File file : fs) {
+			if (file.isDirectory()) {
+				if (recursive) {
+					String nextPath;
+					if (flatten)
+						nextPath = path;
+					else
+						nextPath = appendPath(path, file.getName());
+
+					resolveFiles(file, filter, recursive, nextPath, files, flatten);
+				}
+				// Directories are ignored otherwise
+			} else {
+				String p = appendPath(path, file.getName());
+				if (files.containsKey(p))
+					warning("Include-Resource overwrites entry %s from file %s", p, file);
+				files.put(p, file);
+			}
+		}
+	}
+
+	private void noSuchFile(Jar jar, String clause, Map<String, String> extra, String source,
+			String destinationPath) throws Exception {
+		Jar src = getJarFromName(source, "Include-Resource " + source);
+		if (src != null) {
+			JarResource jarResource = new JarResource(src);
+			jar.putResource(destinationPath, jarResource);
+		} else {
+			Resource lastChance = make.process(source);
+			if (lastChance != null) {
+				String x = extra.get("extra");
+				if (x != null)
+					lastChance.setExtra(x);
+				jar.putResource(destinationPath, lastChance);
+			} else
+				error("Input file does not exist: " + source);
+		}
+	}
+
+	/**
+	 * Extra resources from a Jar and add them to the given jar. The clause is
+	 * the
+	 * 
+	 * @param jar
+	 * @param clauses
+	 * @param i
+	 * @throws ZipException
+	 * @throws IOException
+	 */
+	private void extractFromJar(Jar jar, String source, String destination) throws ZipException,
+			IOException {
+		// Inline all resources and classes from another jar
+		// optionally appended with a modified regular expression
+		// like @zip.jar!/META-INF/MANIFEST.MF
+		int n = source.lastIndexOf("!/");
+		Instruction instr = null;
+		if (n > 0) {
+			instr = Instruction.getPattern(source.substring(n + 2));
+			source = source.substring(0, n);
+		}
+
+		// Pattern filter = null;
+		// if (n > 0) {
+		// String fstring = source.substring(n + 2);
+		// source = source.substring(0, n);
+		// filter = wildcard(fstring);
+		// }
+		Jar sub = getJarFromName(source, "extract from jar");
+		if (sub == null)
+			error("Can not find JAR file " + source);
+		else {
+			jar.addAll(sub, instr, destination);
+		}
+	}
+
+	private void copy(Jar jar, String path, File from, boolean preprocess, Map<String, String> extra)
+			throws Exception {
+		if (doNotCopy(from.getName()))
+			return;
+
+		if (from.isDirectory()) {
+
+			File files[] = from.listFiles();
+			for (int i = 0; i < files.length; i++) {
+				copy(jar, appendPath(path, files[i].getName()), files[i], preprocess, extra);
+			}
+		} else {
+			if (from.exists()) {
+				Resource resource = new FileResource(from);
+				if (preprocess) {
+					resource = new PreprocessResource(this, resource);
+				}
+				String x = extra.get("extra");
+				if (x != null)
+					resource.setExtra(x);
+				if (path.endsWith("/"))
+					path = path + from.getName();
+				jar.putResource(path, resource);
+
+				if (isTrue(extra.get(LIB_DIRECTIVE))) {
+					setProperty(BUNDLE_CLASSPATH, append(getProperty(BUNDLE_CLASSPATH), path));
+				}
+			} else {
+				error("Input file does not exist: " + from);
+			}
+		}
+	}
+
+	private String getName(String where) {
+		int n = where.lastIndexOf('/');
+		if (n < 0)
+			return where;
+
+		return where.substring(n + 1);
+	}
+
+	public void setSourcepath(File[] files) {
+		for (int i = 0; i < files.length; i++)
+			addSourcepath(files[i]);
+	}
+
+	public void addSourcepath(File cp) {
+		if (!cp.exists())
+			warning("File on sourcepath that does not exist: " + cp);
+
+		sourcePath.add(cp);
+	}
+
+	public void close() {
+		super.close();
+	}
+
+	/**
+	 * Build Multiple jars. If the -sub command is set, we filter the file with
+	 * the given patterns.
+	 * 
+	 * @return
+	 * @throws Exception
+	 */
+	public Jar[] builds() throws Exception {
+		begin();
+
+		// Are we acting as a conduit for another JAR?
+		String conduit = getProperty(CONDUIT);
+		if (conduit != null) {
+			Map<String, Map<String, String>> map = parseHeader(conduit);
+			Jar[] result = new Jar[map.size()];
+			int n = 0;
+			for (String file : map.keySet()) {
+				Jar c = new Jar(getFile(file));
+				addClose(c);
+				String name = map.get(file).get("name");
+				if (name != null)
+					c.setName(name);
+
+				result[n++] = c;
+			}
+			return result;
+		}
+
+		List<Jar> result = new ArrayList<Jar>();
+		List<Builder> builders;
+
+		builders = getSubBuilders();
+
+		for (Builder builder : builders) {
+			try {
+				Jar jar = builder.build();
+				jar.setName(builder.getBsn());
+				result.add(jar);
+			} catch (Exception e) {
+				e.printStackTrace();
+				error("Sub Building " + builder.getBsn(), e);
+			}
+			if (builder != this)
+				getInfo(builder, builder.getBsn() + ": ");
+		}
+		return result.toArray(new Jar[result.size()]);
+	}
+
+	/**
+	 * Answer a list of builders that represent this file or a list of files
+	 * specified in -sub. This list can be empty. These builders represents to
+	 * be created artifacts and are each scoped to such an artifacts. The
+	 * builders can be used to build the bundles or they can be used to find out
+	 * information about the to be generated bundles.
+	 * 
+	 * @return List of 0..n builders representing artifacts.
+	 * @throws Exception
+	 */
+	public List<Builder> getSubBuilders() throws Exception {
+		String sub = (String) getProperty(SUB);
+		if (sub == null || sub.trim().length() == 0 || EMPTY_HEADER.equals(sub))
+			return Arrays.asList(this);
+
+		List<Builder> builders = new ArrayList<Builder>();
+		if (isTrue(getProperty(NOBUNDLES)))
+			return builders;
+
+		Map<String, Map<String, String>> subsMap = parseHeader(sub);
+		for (Iterator<String> i = subsMap.keySet().iterator(); i.hasNext();) {
+			File file = getFile(i.next());
+			if (file.isFile()) {
+				builders.add(getSubBuilder(file));
+				i.remove();
+			}
+		}
+
+		Set<Instruction> subs = Instruction.replaceWithInstruction(subsMap).keySet();
+
+		List<File> members = new ArrayList<File>(Arrays.asList(getBase().listFiles()));
+
+		nextFile: while (members.size() > 0) {
+
+			File file = members.remove(0);
+
+			// Check if the file is one of our parents
+			Processor p = this;
+			while (p != null) {
+				if (file.equals(p.getPropertiesFile()))
+					continue nextFile;
+				p = p.getParent();
+			}
+
+			for (Iterator<Instruction> i = subs.iterator(); i.hasNext();) {
+
+				Instruction instruction = i.next();
+				if (instruction.matches(file.getName())) {
+
+					if (!instruction.isNegated()) {
+						builders.add(getSubBuilder(file));
+					}
+
+					// Because we matched (even though we could be negated)
+					// we skip any remaining searches
+					continue nextFile;
+				}
+			}
+		}
+		return builders;
+	}
+
+	public Builder getSubBuilder(File file) throws Exception {
+		Builder builder = getSubBuilder();
+		if (builder != null) {
+			builder.setProperties(file);
+			addClose(builder);
+		}
+		return builder;
+	}
+
+	public Builder getSubBuilder() throws Exception {
+		Builder builder = new Builder(this);
+		builder.setBase(getBase());
+
+		for (Jar file : getClasspath()) {
+			builder.addClasspath(file);
+		}
+
+		return builder;
+	}
+
+	/**
+	 * A macro to convert a maven version to an OSGi version
+	 */
+
+	public String _maven_version(String args[]) {
+		if (args.length > 2)
+			error("${maven_version} macro receives too many arguments " + Arrays.toString(args));
+		else if (args.length < 2)
+			error("${maven_version} macro has no arguments, use ${maven_version;1.2.3-SNAPSHOT}");
+		else {
+			return cleanupVersion(args[1]);
+		}
+		return null;
+	}
+
+	public String _permissions(String args[]) throws IOException {
+		StringBuilder sb = new StringBuilder();
+
+		for (String arg : args) {
+			if ("packages".equals(arg) || "all".equals(arg)) {
+				for (String imp : getImports().keySet()) {
+					if (!imp.startsWith("java.")) {
+						sb.append("(org.osgi.framework.PackagePermission \"");
+						sb.append(imp);
+						sb.append("\" \"import\")\r\n");
+					}
+				}
+				for (String exp : getExports().keySet()) {
+					sb.append("(org.osgi.framework.PackagePermission \"");
+					sb.append(exp);
+					sb.append("\" \"export\")\r\n");
+				}
+			} else if ("admin".equals(arg) || "all".equals(arg)) {
+				sb.append("(org.osgi.framework.AdminPermission)");
+			} else if ("permissions".equals(arg))
+				;
+			else
+				error("Invalid option in ${permissions}: %s", arg);
+		}
+		return sb.toString();
+	}
+
+	/**
+     * 
+     */
+	public void removeBundleSpecificHeaders() {
+		Set<String> set = new HashSet<String>(Arrays.asList(BUNDLE_SPECIFIC_HEADERS));
+		setForceLocal(set);
+	}
+
+	/**
+	 * Check if the given resource is in scope of this bundle. That is, it
+	 * checks if the Include-Resource includes this resource or if it is a class
+	 * file it is on the class path and the Export-Pacakge or Private-Package
+	 * include this resource.
+	 * 
+	 * For now, include resources are skipped.
+	 * 
+	 * @param f
+	 * @return
+	 */
+	public boolean isInScope(Collection<File> resources) throws Exception {
+		Map<String, Map<String, String>> clauses = parseHeader(getProperty(Constants.EXPORT_PACKAGE));
+		clauses.putAll(parseHeader(getProperty(Constants.PRIVATE_PACKAGE)));
+		if (isTrue(getProperty(Constants.UNDERTEST))) {
+			clauses.putAll(parseHeader(getProperty(Constants.TESTPACKAGES,
+					"test;presence:=optional")));
+		}
+		Map<Instruction, Map<String, String>> instructions = Instruction
+				.replaceWithInstruction(clauses);
+
+		for (File r : resources) {
+			String cpEntry = getClasspathEntrySuffix(r);
+			if (cpEntry != null) {
+				String pack = Clazz.getPackage(cpEntry);
+				Instruction i = matches(instructions, pack, null, r.getName());
+				if (i != null)
+					return !i.isNegated();
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Answer the string of the resource that it has in the container.
+	 * 
+	 * @param resource
+	 *            The resource to look for
+	 * @return
+	 * @throws Exception
+	 */
+	public String getClasspathEntrySuffix(File resource) throws Exception {
+		for (Jar jar : getClasspath()) {
+			File source = jar.getSource();
+			if (source != null) {
+				source = source.getCanonicalFile();
+				String sourcePath = source.getAbsolutePath();
+				String resourcePath = resource.getAbsolutePath();
+
+				if (resourcePath.startsWith(sourcePath)) {
+					// Make sure that the path name is translated correctly
+					// i.e. on Windows the \ must be translated to /
+					String filePath = resourcePath.substring(sourcePath.length() + 1);
+
+					return filePath.replace(File.separatorChar, '/');
+				}
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * doNotCopy
+	 * 
+	 * The doNotCopy variable maintains a patter for files that should not be
+	 * copied. There is a default {@link #DEFAULT_DO_NOT_COPY} but this ca be
+	 * overridden with the {@link Constants#DONOTCOPY} property.
+	 */
+
+	public boolean doNotCopy(String v) {
+		return getDoNotCopy().matcher(v).matches();
+	}
+
+	public Pattern getDoNotCopy() {
+		if (xdoNotCopy == null) {
+			String string = null;
+			try {
+				string = getProperty(DONOTCOPY, DEFAULT_DO_NOT_COPY);
+				xdoNotCopy = Pattern.compile(string);
+			} catch (Exception e) {
+				error("Invalid value for %s, value is %s", DONOTCOPY, string);
+				xdoNotCopy = Pattern.compile(DEFAULT_DO_NOT_COPY);
+			}
+		}
+		return xdoNotCopy;
+	}
+
+	/**
+	 */
+
+	static MakeBnd			makeBnd				= new MakeBnd();
+	static MakeCopy			makeCopy			= new MakeCopy();
+	static ServiceComponent	serviceComponent	= new ServiceComponent();
+	static DSAnnotations	dsAnnotations		= new DSAnnotations();
+	static MetatypePlugin	metatypePlugin		= new MetatypePlugin();
+
+	@Override protected void setTypeSpecificPlugins(Set<Object> list) {
+		list.add(makeBnd);
+		list.add(makeCopy);
+		list.add(serviceComponent);
+		//list.add(dsAnnotations);
+		list.add(metatypePlugin);
+		super.setTypeSpecificPlugins(list);
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/ClassDataCollector.java b/bundleplugin/src/main/java/aQute/lib/osgi/ClassDataCollector.java
new file mode 100644
index 0000000..12e9a37
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/ClassDataCollector.java
@@ -0,0 +1,88 @@
+package aQute.lib.osgi;
+
+public class ClassDataCollector {
+    public void classBegin(int access, String name) {
+    }
+
+    public boolean classStart(int access, String name) {
+        classBegin(access,name);
+        return true;
+    }
+
+    public void extendsClass(String name) {
+    }
+
+    public void implementsInterfaces(String name[]) {
+    }
+
+    public void addReference(String token) {
+    }
+
+    public void annotation(Annotation annotation) {
+    }
+
+    public void parameter(int p) {
+    }
+
+    public void method(Clazz.MethodDef defined) {
+        if (defined.isConstructor())
+            constructor(defined.access, defined.descriptor);
+        else
+            method(defined.access, defined.name, defined.descriptor);
+    }
+
+    public void field(Clazz.FieldDef defined) {
+        field(defined.access, defined.name, defined.descriptor);
+    }
+
+    public void reference(Clazz.MethodDef referenced) {
+    }
+
+    public void reference(Clazz.FieldDef referenced) {
+    }
+
+    public void classEnd() {
+    }
+
+    @Deprecated // Will really be removed!
+    public void field(int access, String name, String descriptor) {
+    }
+
+    @Deprecated // Will really be removed!
+    public void constructor(int access, String descriptor) {
+    }
+
+    @Deprecated // Will really be removed!
+    public void method(int access, String name, String descriptor) {
+    }
+
+    /**
+     * The EnclosingMethod attribute
+     * 
+     * @param cName The name of the enclosing class, never null. Name is with slashes.
+     * @param mName The name of the enclosing method in the class with cName or null
+     * @param mDescriptor The descriptor of this type
+     */
+	public void enclosingMethod(String cName, String mName, String mDescriptor) {
+		
+	}
+
+	/**
+	 * The InnerClass attribute
+	 * 
+	 * @param innerClass The name of the inner class (with slashes). Can be null.
+	 * @param outerClass The name of the outer class (with slashes) Can be null.
+	 * @param innerName The name inside the outer class, can be null.
+	 * @param modifiers The access flags 
+	 */
+	public void innerClass(String innerClass, String outerClass, String innerName,
+			int innerClassAccessFlags) {		
+	}
+
+	public void signature(String signature) {
+	}
+
+	public void constant(Object object) {
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Clazz.java b/bundleplugin/src/main/java/aQute/lib/osgi/Clazz.java
new file mode 100644
index 0000000..d7ed1ce
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Clazz.java
@@ -0,0 +1,1608 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+import java.lang.annotation.*;
+import java.nio.*;
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.bnd.annotation.*;
+import aQute.libg.generics.*;
+
+public class Clazz {
+
+	public class ClassConstant {
+		int	cname;
+
+		public ClassConstant(int class_index) {
+			this.cname = class_index;
+		}
+
+		public String getName() {
+			return (String) pool[cname];
+		}
+	}
+
+	public static enum JAVA {
+		UNKNOWN(Integer.MAX_VALUE), OpenJDK7(51), J2S6(50), J2SE5(49), JDK1_4(48), JDK1_3(47), JDK1_2(
+				46), JDK1_1(45);
+
+		final int	major;
+
+		JAVA(int major) {
+			this.major = major;
+		}
+
+		static JAVA format(int n) {
+			for (JAVA e : JAVA.values())
+				if (e.major == n)
+					return e;
+			return UNKNOWN;
+		}
+
+		public int getMajor() {
+			return major;
+		}
+
+		public boolean hasAnnotations() {
+			return major >= J2SE5.major;
+		}
+
+		public boolean hasGenerics() {
+			return major >= J2SE5.major;
+		}
+
+		public boolean hasEnums() {
+			return major >= J2SE5.major;
+		}
+	};
+
+	public static enum QUERY {
+		IMPLEMENTS, EXTENDS, IMPORTS, NAMED, ANY, VERSION, CONCRETE, ABSTRACT, PUBLIC, ANNOTATION, RUNTIMEANNOTATIONS, CLASSANNOTATIONS
+	};
+
+	public static EnumSet<QUERY>	HAS_ARGUMENT	= EnumSet.of(QUERY.IMPLEMENTS, QUERY.EXTENDS,
+															QUERY.IMPORTS, QUERY.NAMED,
+															QUERY.VERSION, QUERY.ANNOTATION);
+
+	/**
+	 * <pre>
+	 * ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its
+	 * package. 
+	 * ACC_FINAL 0x0010 Declared final; no subclasses allowed.
+	 * ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the
+	 * invokespecial instruction. 
+	 * ACC_INTERFACE 0x0200 Is an interface, not a
+	 * class. 
+	 * ACC_ABSTRACT 0x0400 Declared abstract; may not be instantiated.
+	 * </pre>
+	 * 
+	 * @param mod
+	 */
+	final static int				ACC_PUBLIC		= 0x0001;									// Declared
+	// public;
+	// may
+	// be
+	// accessed
+	// from outside its package.
+	final static int				ACC_FINAL		= 0x0010;									// Declared
+	// final;
+	// no
+	// subclasses
+	// allowed.
+	final static int				ACC_SUPER		= 0x0020;									// Treat
+	// superclass
+	// methods
+	// specially when invoked by the
+	// invokespecial instruction.
+	final static int				ACC_INTERFACE	= 0x0200;									// Is
+	// an
+	// interface,
+	// not
+	// a
+	// classs
+	final static int				ACC_ABSTRACT	= 0x0400;									// Declared
+
+	// abstract;
+	// may
+	// not
+	// be
+
+	// instantiated.
+
+	final static int				ACC_ENUM		= 0x04000;
+
+	static protected class Assoc {
+		Assoc(byte tag, int a, int b) {
+			this.tag = tag;
+			this.a = a;
+			this.b = b;
+		}
+
+		byte	tag;
+		int		a;
+		int		b;
+	}
+
+	static public class FieldDef implements Comparable<FieldDef> {
+		public FieldDef(int access, String clazz, String name, String descriptor) {
+			this.access = access;
+			this.clazz = clazz.replace('/', '.');
+			this.name = name;
+			this.descriptor = descriptor;
+		}
+
+		final public int	access;
+		final public String	clazz;
+		final public String	name;
+		final public String	descriptor;
+		public String		signature;
+		public Object		constant;
+
+		public boolean equals(Object other) {
+			if (!(other instanceof MethodDef))
+				return false;
+
+			FieldDef m = (FieldDef) other;
+			return clazz.equals(m.clazz) && name.equals(m.name) && descriptor.equals(m.descriptor);
+		}
+
+		public int hashCode() {
+			return clazz.hashCode() ^ name.hashCode() ^ descriptor.hashCode();
+		}
+
+		public int compareTo(FieldDef o) {
+			int result = clazz.compareTo(o.clazz);
+			if (result == 0) {
+				result = name.compareTo(o.name);
+				if (result == 0) {
+					result = descriptor.compareTo(o.descriptor);
+				}
+			}
+			return result;
+		}
+
+		public String getPretty() {
+			return name;
+		}
+
+		public String toString() {
+			return getPretty();
+		}
+
+		public boolean isEnum() {
+			return (access & ACC_ENUM) != 0;
+		}
+	}
+
+	static public class MethodDef extends FieldDef {
+		Pattern	METHOD_DESCRIPTOR	= Pattern.compile("\\((.*)\\)(.+)");
+
+		public MethodDef(int access, String clazz, String method, String descriptor) {
+			super(access, clazz, method, descriptor);
+		}
+
+		public boolean isConstructor() {
+			return name.equals("<init>") || name.equals("<clinit>");
+		}
+
+		public String getReturnType() {
+			String use = descriptor;
+			if (signature != null)
+				use = signature;
+
+			Matcher m = METHOD_DESCRIPTOR.matcher(use);
+			if (!m.matches())
+				throw new IllegalArgumentException("Not a valid method descriptor: " + descriptor);
+
+			String returnType = m.group(2);
+			return objectDescriptorToFQN(returnType);
+		}
+
+		public String getPretty() {
+
+			StringBuilder sb = new StringBuilder();
+			sb.append(descriptor.charAt(0));
+			int index = 1;
+			String del = "";
+			while (index < descriptor.length() && descriptor.charAt(index) != ')') {
+				sb.append(del);
+				index = printParameter(sb, descriptor, index);
+				del = ",";
+			}
+			sb.append(descriptor.charAt(index++));
+			StringBuilder sb2 = new StringBuilder();
+			if (isConstructor()) {
+				sb2.append(getShortName(clazz));
+				index++; // skip the V
+			} else {
+				printParameter(sb2, descriptor, index);
+				sb2.append(" ");
+				sb2.append(getShortName(clazz));
+				sb2.append(".");
+				sb2.append(name);
+			}
+			sb2.append(sb);
+			return sb2.toString();
+		}
+
+		private int printParameter(StringBuilder sb, CharSequence descriptor, int index) {
+			char c = descriptor.charAt(index++);
+			switch (c) {
+			case 'B':
+				sb.append("byte");
+				break;
+			case 'C':
+				sb.append("char");
+				break;
+			case 'D':
+				sb.append("double");
+				break;
+			case 'F':
+				sb.append("float");
+				break;
+			case 'I':
+				sb.append("int");
+				break;
+			case 'J':
+				sb.append("long");
+				break;
+			case 'S':
+				sb.append("short");
+				break;
+			case 'Z':
+				sb.append("boolean");
+				break;
+			case 'V':
+				sb.append("void");
+				break;
+			case 'L':
+				index = reference(sb, descriptor, index);
+				break;
+
+			case '[':
+				index = array(sb, descriptor, index);
+				break;
+			}
+			return index;
+		}
+
+		private int reference(StringBuilder sb, CharSequence descriptor, int index) {
+			int n = sb.length();
+			int lastSlash = n;
+			while (index < descriptor.length() && descriptor.charAt(index) != ';') {
+				char c = descriptor.charAt(index++);
+				if (c == '/') {
+					c = '.';
+					lastSlash = sb.length() + 1;
+				}
+				sb.append(c);
+			}
+			if (lastSlash != n) {
+				sb.delete(n, lastSlash);
+			}
+			return ++index;
+		}
+
+		private int array(StringBuilder sb, CharSequence descriptor, int index) {
+			int n = 1;
+			while (index < descriptor.length() && descriptor.charAt(index) == '[') {
+				index++;
+			}
+			index = printParameter(sb, descriptor, index);
+			while (n-- > 0) {
+				sb.append("[]");
+			}
+			return index;
+		}
+	}
+
+	final static byte	SkipTable[]	= { 0, // 0 non existent
+			-1, // 1 CONSTANT_utf8 UTF 8, handled in
+			// method
+			-1, // 2
+			4, // 3 CONSTANT_Integer
+			4, // 4 CONSTANT_Float
+			8, // 5 CONSTANT_Long (index +=2!)
+			8, // 6 CONSTANT_Double (index +=2!)
+			-1, // 7 CONSTANT_Class
+			2, // 8 CONSTANT_String
+			4, // 9 CONSTANT_FieldRef
+			4, // 10 CONSTANT_MethodRef
+			4, // 11 CONSTANT_InterfaceMethodRef
+			4, // 12 CONSTANT_NameAndType
+									};
+
+	boolean				isAbstract;
+	boolean				isPublic;
+	boolean				isEnum;
+	boolean				hasRuntimeAnnotations;
+	boolean				hasClassAnnotations;
+
+	String				className;
+	Object				pool[];
+	int					intPool[];
+	Set<String>			imports		= Create.set();
+	String				path;
+	int					minor		= 0;
+	int					major		= 0;
+	int					access		= 0;
+	String				sourceFile;
+	Set<String>			xref;
+	Set<Integer>		classes;
+	Set<Integer>		descriptors;
+	Set<String>			annotations;
+	int					forName		= 0;
+	int					class$		= 0;
+	String[]			interfaces;
+	String				zuper;
+	ClassDataCollector	cd			= null;
+	Resource			resource;
+	FieldDef			last		= null;
+
+	public Clazz(String path, Resource resource) {
+		this.path = path;
+		this.resource = resource;
+	}
+
+	public Set<String> parseClassFile() throws Exception {
+		return parseClassFileWithCollector(null);
+	}
+
+	public Set<String> parseClassFile(InputStream in) throws IOException {
+		return parseClassFile(in, null);
+	}
+
+	public Set<String> parseClassFileWithCollector(ClassDataCollector cd) throws Exception {
+		InputStream in = resource.openInputStream();
+		try {
+			return parseClassFile(in, cd);
+		} finally {
+			in.close();
+		}
+	}
+
+	public Set<String> parseClassFile(InputStream in, ClassDataCollector cd) throws IOException {
+		DataInputStream din = new DataInputStream(in);
+		try {
+			this.cd = cd;
+			return parseClassFile(din);
+		} finally {
+			cd = null;
+			din.close();
+		}
+	}
+
+	Set<String> parseClassFile(DataInputStream in) throws IOException {
+
+		xref = new HashSet<String>();
+		classes = new HashSet<Integer>();
+		descriptors = new HashSet<Integer>();
+
+		boolean crawl = cd != null; // Crawl the byte code if we have a
+		// collector
+		int magic = in.readInt();
+		if (magic != 0xCAFEBABE)
+			throw new IOException("Not a valid class file (no CAFEBABE header)");
+
+		minor = in.readUnsignedShort(); // minor version
+		major = in.readUnsignedShort(); // major version
+		int count = in.readUnsignedShort();
+		pool = new Object[count];
+		intPool = new int[count];
+
+		process: for (int poolIndex = 1; poolIndex < count; poolIndex++) {
+			byte tag = in.readByte();
+			switch (tag) {
+			case 0:
+				break process;
+			case 1:
+				constantUtf8(in, poolIndex);
+				break;
+
+			case 3:
+				constantInteger(in, poolIndex);
+				break;
+
+			case 4:
+				constantFloat(in, poolIndex);
+				break;
+
+			// For some insane optimization reason are
+			// the long and the double two entries in the
+			// constant pool. See 4.4.5
+			case 5:
+				constantLong(in, poolIndex);
+				poolIndex++;
+				break;
+
+			case 6:
+				constantDouble(in, poolIndex);
+				poolIndex++;
+				break;
+
+			case 7:
+				constantClass(in, poolIndex);
+				break;
+
+			case 8:
+				constantString(in, poolIndex);
+				break;
+
+			case 10: // Method ref
+			case 11: // Interface Method ref
+				methodRef(in, poolIndex);
+				break;
+
+			// Name and Type
+			case 12:
+				nameAndType(in, poolIndex, tag);
+				break;
+
+			// We get the skip count for each record type
+			// from the SkipTable. This will also automatically
+			// abort when
+			default:
+				if (tag == 2)
+					throw new IOException("Invalid tag " + tag);
+				in.skipBytes(SkipTable[tag]);
+				break;
+			}
+		}
+
+		pool(pool, intPool);
+		/*
+		 * Parse after the constant pool, code thanks to Hans Christian
+		 * Falkenberg
+		 */
+
+		int access_flags = in.readUnsignedShort(); // access
+		isAbstract = (access_flags & ACC_ABSTRACT) != 0;
+		isPublic = (access_flags & ACC_PUBLIC) != 0;
+		isEnum = (access_flags & ACC_ENUM) != 0;
+
+		int this_class = in.readUnsignedShort();
+		className = (String) pool[intPool[this_class]];
+
+		try {
+
+			if (cd != null) {
+				if (!cd.classStart(access_flags, className))
+					return null;
+			}
+
+			int super_class = in.readUnsignedShort();
+			zuper = (String) pool[intPool[super_class]];
+			if (zuper != null) {
+				String pack = getPackage(zuper);
+				packageReference(pack);
+				if (cd != null)
+					cd.extendsClass(zuper);
+			}
+
+			int interfacesCount = in.readUnsignedShort();
+			if (interfacesCount > 0) {
+				interfaces = new String[interfacesCount];
+				for (int i = 0; i < interfacesCount; i++)
+					interfaces[i] = (String) pool[intPool[in.readUnsignedShort()]];
+				if (cd != null)
+					cd.implementsInterfaces(interfaces);
+			}
+
+			int fieldsCount = in.readUnsignedShort();
+			for (int i = 0; i < fieldsCount; i++) {
+				access_flags = in.readUnsignedShort(); // skip access flags
+				int name_index = in.readUnsignedShort();
+				int descriptor_index = in.readUnsignedShort();
+
+				// Java prior to 1.5 used a weird
+				// static variable to hold the com.X.class
+				// result construct. If it did not find it
+				// it would create a variable class$com$X
+				// that would be used to hold the class
+				// object gotten with Class.forName ...
+				// Stupidly, they did not actively use the
+				// class name for the field type, so bnd
+				// would not see a reference. We detect
+				// this case and add an artificial descriptor
+				String name = pool[name_index].toString(); // name_index
+				if (name.startsWith("class$")) {
+					crawl = true;
+				}
+				if (cd != null)
+					cd.field(last = new FieldDef(access_flags, className, name,
+							pool[descriptor_index].toString()));
+				descriptors.add(new Integer(descriptor_index));
+				doAttributes(in, ElementType.FIELD, false);
+			}
+
+			//
+			// Check if we have to crawl the code to find
+			// the ldc(_w) <string constant> invokestatic Class.forName
+			// if so, calculate the method ref index so we
+			// can do this efficiently
+			//
+			if (crawl) {
+				forName = findMethodReference("java/lang/Class", "forName",
+						"(Ljava/lang/String;)Ljava/lang/Class;");
+				class$ = findMethodReference(className, "class$",
+						"(Ljava/lang/String;)Ljava/lang/Class;");
+			} else if (major == 48) {
+				forName = findMethodReference("java/lang/Class", "forName",
+						"(Ljava/lang/String;)Ljava/lang/Class;");
+				if (forName > 0) {
+					crawl = true;
+					class$ = findMethodReference(className, "class$",
+							"(Ljava/lang/String;)Ljava/lang/Class;");
+				}
+			}
+
+			//
+			// Handle the methods
+			//
+			int methodCount = in.readUnsignedShort();
+			for (int i = 0; i < methodCount; i++) {
+				access_flags = in.readUnsignedShort();
+				int name_index = in.readUnsignedShort();
+				int descriptor_index = in.readUnsignedShort();
+				descriptors.add(new Integer(descriptor_index));
+				String name = pool[name_index].toString();
+				String descriptor = pool[descriptor_index].toString();
+				if (cd != null) {
+					MethodDef mdef = new MethodDef(access_flags, className, name, descriptor);
+					last = mdef;
+					cd.method(mdef);
+				}
+
+				if ("<init>".equals(name)) {
+					doAttributes(in, ElementType.CONSTRUCTOR, crawl);
+				} else {
+					doAttributes(in, ElementType.METHOD, crawl);
+				}
+			}
+
+			doAttributes(in, ElementType.TYPE, false);
+
+			//
+			// Now iterate over all classes we found and
+			// parse those as well. We skip duplicates
+			//
+
+			for (int n : classes) {
+				String clazz = (String) pool[n];
+				if (clazz.endsWith(";") || clazz.startsWith("["))
+					parseReference(clazz, 0);
+				else {
+
+					String pack = getPackage(clazz);
+					packageReference(pack);
+				}
+			}
+
+			//
+			// Parse all the descriptors we found
+			//
+
+			for (Iterator<Integer> e = descriptors.iterator(); e.hasNext();) {
+				Integer index = e.next();
+				String prototype = (String) pool[index.intValue()];
+				if (prototype != null)
+					parseDescriptor(prototype);
+				else
+					System.err.println("Unrecognized descriptor: " + index);
+			}
+			Set<String> xref = this.xref;
+			reset();
+			return xref;
+		} finally {
+			if (cd != null)
+				cd.classEnd();
+		}
+	}
+
+	private void constantFloat(DataInputStream in, int poolIndex) throws IOException {
+		if (cd != null)
+			pool[poolIndex] = in.readFloat(); // ALU
+		else
+			in.skipBytes(4);
+	}
+
+	private void constantInteger(DataInputStream in, int poolIndex) throws IOException {
+		intPool[poolIndex] = in.readInt();
+		if (cd != null)
+			pool[poolIndex] = intPool[poolIndex];
+	}
+
+	protected void pool(Object[] pool, int[] intPool) {
+	}
+
+	/**
+	 * @param in
+	 * @param poolIndex
+	 * @param tag
+	 * @throws IOException
+	 */
+	protected void nameAndType(DataInputStream in, int poolIndex, byte tag) throws IOException {
+		int name_index = in.readUnsignedShort();
+		int descriptor_index = in.readUnsignedShort();
+		descriptors.add(new Integer(descriptor_index));
+		pool[poolIndex] = new Assoc(tag, name_index, descriptor_index);
+	}
+
+	/**
+	 * @param in
+	 * @param poolIndex
+	 * @param tag
+	 * @throws IOException
+	 */
+	private void methodRef(DataInputStream in, int poolIndex) throws IOException {
+		int class_index = in.readUnsignedShort();
+		int name_and_type_index = in.readUnsignedShort();
+		pool[poolIndex] = new Assoc((byte) 10, class_index, name_and_type_index);
+	}
+
+	/**
+	 * @param in
+	 * @param poolIndex
+	 * @throws IOException
+	 */
+	private void constantString(DataInputStream in, int poolIndex) throws IOException {
+		int string_index = in.readUnsignedShort();
+		intPool[poolIndex] = string_index;
+	}
+
+	/**
+	 * @param in
+	 * @param poolIndex
+	 * @throws IOException
+	 */
+	protected void constantClass(DataInputStream in, int poolIndex) throws IOException {
+		int class_index = in.readUnsignedShort();
+		classes.add(new Integer(class_index));
+		intPool[poolIndex] = class_index;
+		ClassConstant c = new ClassConstant(class_index);
+		pool[poolIndex] = c;
+	}
+
+	/**
+	 * @param in
+	 * @throws IOException
+	 */
+	protected void constantDouble(DataInputStream in, int poolIndex) throws IOException {
+		if (cd != null)
+			pool[poolIndex] = in.readDouble();
+		else
+			in.skipBytes(8);
+	}
+
+	/**
+	 * @param in
+	 * @throws IOException
+	 */
+	protected void constantLong(DataInputStream in, int poolIndex) throws IOException {
+		if (cd != null) {
+			pool[poolIndex] = in.readLong();
+		} else
+			in.skipBytes(8);
+	}
+
+	/**
+	 * @param in
+	 * @param poolIndex
+	 * @throws IOException
+	 */
+	protected void constantUtf8(DataInputStream in, int poolIndex) throws IOException {
+		// CONSTANT_Utf8
+
+		String name = in.readUTF();
+		xref.add(name);
+		pool[poolIndex] = name;
+	}
+
+	/**
+	 * Find a method reference in the pool that points to the given class,
+	 * methodname and descriptor.
+	 * 
+	 * @param clazz
+	 * @param methodname
+	 * @param descriptor
+	 * @return index in constant pool
+	 */
+	private int findMethodReference(String clazz, String methodname, String descriptor) {
+		for (int i = 1; i < pool.length; i++) {
+			if (pool[i] instanceof Assoc) {
+				Assoc methodref = (Assoc) pool[i];
+				if (methodref.tag == 10) {
+					// Method ref
+					int class_index = methodref.a;
+					int class_name_index = intPool[class_index];
+					if (clazz.equals(pool[class_name_index])) {
+						int name_and_type_index = methodref.b;
+						Assoc name_and_type = (Assoc) pool[name_and_type_index];
+						if (name_and_type.tag == 12) {
+							// Name and Type
+							int name_index = name_and_type.a;
+							int type_index = name_and_type.b;
+							if (methodname.equals(pool[name_index])) {
+								if (descriptor.equals(pool[type_index])) {
+									return i;
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+		return -1;
+	}
+
+	/**
+	 * Called for each attribute in the class, field, or method.
+	 * 
+	 * @param in
+	 *            The stream
+	 * @throws IOException
+	 */
+	private void doAttributes(DataInputStream in, ElementType member, boolean crawl)
+			throws IOException {
+		int attributesCount = in.readUnsignedShort();
+		for (int j = 0; j < attributesCount; j++) {
+			// skip name CONSTANT_Utf8 pointer
+			doAttribute(in, member, crawl);
+		}
+	}
+
+	/**
+	 * Process a single attribute, if not recognized, skip it.
+	 * 
+	 * @param in
+	 *            the data stream
+	 * @throws IOException
+	 */
+	private void doAttribute(DataInputStream in, ElementType member, boolean crawl)
+			throws IOException {
+		int attribute_name_index = in.readUnsignedShort();
+		String attributeName = (String) pool[attribute_name_index];
+		long attribute_length = in.readInt();
+		attribute_length &= 0xFFFFFFFF;
+		if ("RuntimeVisibleAnnotations".equals(attributeName))
+			doAnnotations(in, member, RetentionPolicy.RUNTIME);
+		else if ("RuntimeVisibleParameterAnnotations".equals(attributeName))
+			doParameterAnnotations(in, member, RetentionPolicy.RUNTIME);
+		else if ("RuntimeInvisibleAnnotations".equals(attributeName))
+			doAnnotations(in, member, RetentionPolicy.CLASS);
+		else if ("RuntimeInvisibleParameterAnnotations".equals(attributeName))
+			doParameterAnnotations(in, member, RetentionPolicy.CLASS);
+		else if ("InnerClasses".equals(attributeName))
+			doInnerClasses(in);
+		else if ("EnclosingMethod".equals(attributeName))
+			doEnclosingMethod(in);
+		else if ("SourceFile".equals(attributeName))
+			doSourceFile(in);
+		else if ("Code".equals(attributeName) && crawl)
+			doCode(in);
+		else if ("Signature".equals(attributeName))
+			doSignature(in, member);
+		else if ("ConstantValue".equals(attributeName))
+			doConstantValue(in);
+		else {
+			if (attribute_length > 0x7FFFFFFF) {
+				throw new IllegalArgumentException("Attribute > 2Gb");
+			}
+			in.skipBytes((int) attribute_length);
+		}
+	}
+
+	/**
+	 * <pre>
+	 * EnclosingMethod_attribute { 
+	 * 	u2 attribute_name_index; 
+	 * 	u4 attribute_length; 
+	 * 	u2 class_index
+	 * 	u2 method_index;
+	 * }
+	 * </pre>
+	 * 
+	 * 
+	 * @param in
+	 * @throws IOException
+	 */
+	private void doEnclosingMethod(DataInputStream in) throws IOException {
+		int cIndex = in.readShort();
+		int mIndex = in.readShort();
+
+		if (cd != null) {
+			int nameIndex = intPool[cIndex];
+			String cName = (String) pool[nameIndex];
+
+			String mName = null;
+			String mDescriptor = null;
+
+			if (mIndex != 0) {
+				Assoc nameAndType = (Assoc) pool[mIndex];
+				mName = (String) pool[nameAndType.a];
+				mDescriptor = (String) pool[nameAndType.b];
+			}
+			cd.enclosingMethod(cName, mName, mDescriptor);
+		}
+	}
+
+	/**
+	 * <pre>
+	 * InnerClasses_attribute {
+	 * 	u2 attribute_name_index; 
+	 * 	u4 attribute_length; 
+	 * 	u2 number_of_classes; {	
+	 * 		u2 inner_class_info_index;
+	 * 		u2 outer_class_info_index; 
+	 * 		u2 inner_name_index; 
+	 * 		u2 inner_class_access_flags;
+	 * 	} classes[number_of_classes];
+	 * }
+	 * </pre>
+	 * 
+	 * @param in
+	 * @throws IOException
+	 */
+	private void doInnerClasses(DataInputStream in) throws IOException {
+		int number_of_classes = in.readShort();
+		for (int i = 0; i < number_of_classes; i++) {
+			int inner_class_info_index = in.readShort();
+			int outer_class_info_index = in.readShort();
+			int inner_name_index = in.readShort();
+			int inner_class_access_flags = in.readShort() & 0xFFFF;
+
+			if (cd != null) {
+				String innerClass = null;
+				String outerClass = null;
+				String innerName = null;
+
+				if (inner_class_info_index != 0) {
+					int nameIndex = intPool[inner_class_info_index];
+					innerClass = (String) pool[nameIndex];
+				}
+
+				if (outer_class_info_index != 0) {
+					int nameIndex = intPool[outer_class_info_index];
+					outerClass = (String) pool[nameIndex];
+				}
+
+				if (inner_name_index != 0)
+					innerName = (String) pool[inner_name_index];
+
+				cd.innerClass(innerClass, outerClass, innerName, inner_class_access_flags);
+			}
+		}
+	}
+
+	/**
+	 * Handle a signature
+	 * 
+	 * <pre>
+	 * Signature_attribute { 
+	 *     u2 attribute_name_index; 
+	 *     u4 attribute_length; 
+	 *     u2 signature_index; 
+	 *     }
+	 * </pre>
+	 * 
+	 * @param member
+	 */
+
+	void doSignature(DataInputStream in, ElementType member) throws IOException {
+		int signature_index = in.readUnsignedShort();
+		String signature = (String) pool[signature_index];
+
+		// System.out.println("Signature " + signature );
+
+		// The type signature is kind of weird,
+		// lets skip it for now. Seems to be some kind of
+		// type variable name index but it does not seem to
+		// conform to the language specification.
+		if (member != ElementType.TYPE)
+			parseDescriptor(signature);
+
+		if (last != null)
+			last.signature = signature;
+
+		if (cd != null)
+			cd.signature(signature);
+	}
+
+	/**
+	 * Handle a constant value call the data collector with it
+	 */
+	void doConstantValue(DataInputStream in) throws IOException {
+		int constantValue_index = in.readUnsignedShort();
+		if (cd == null)
+			return;
+
+		Object object = pool[constantValue_index];
+		if (object == null)
+			object = pool[intPool[constantValue_index]];
+
+		last.constant = object;
+		cd.constant(object);
+	}
+
+	/**
+	 * <pre>
+	 * Code_attribute {
+	 * 		u2 attribute_name_index;
+	 * 		u4 attribute_length;
+	 * 		u2 max_stack;
+	 * 		u2 max_locals;
+	 * 		u4 code_length;
+	 * 		u1 code[code_length];
+	 * 		u2 exception_table_length;
+	 * 		{    	u2 start_pc;
+	 * 		      	u2 end_pc;
+	 * 		      	u2  handler_pc;
+	 * 		      	u2  catch_type;
+	 * 		}	exception_table[exception_table_length];
+	 * 		u2 attributes_count;
+	 * 		attribute_info attributes[attributes_count];
+	 * 	}
+	 * </pre>
+	 * 
+	 * @param in
+	 * @param pool
+	 * @throws IOException
+	 */
+	private void doCode(DataInputStream in) throws IOException {
+		/* int max_stack = */in.readUnsignedShort();
+		/* int max_locals = */in.readUnsignedShort();
+		int code_length = in.readInt();
+		byte code[] = new byte[code_length];
+		in.readFully(code);
+		crawl(code);
+		int exception_table_length = in.readUnsignedShort();
+		in.skipBytes(exception_table_length * 8);
+		doAttributes(in, ElementType.METHOD, false);
+	}
+
+	/**
+	 * We must find Class.forName references ...
+	 * 
+	 * @param code
+	 */
+	protected void crawl(byte[] code) {
+		ByteBuffer bb = ByteBuffer.wrap(code);
+		bb.order(ByteOrder.BIG_ENDIAN);
+		int lastReference = -1;
+
+		while (bb.remaining() > 0) {
+			int instruction = 0xFF & bb.get();
+			switch (instruction) {
+			case OpCodes.ldc:
+				lastReference = 0xFF & bb.get();
+				break;
+
+			case OpCodes.ldc_w:
+				lastReference = 0xFFFF & bb.getShort();
+				break;
+
+			case OpCodes.invokespecial: {
+				int mref = 0xFFFF & bb.getShort();
+				if (cd != null)
+					cd.reference(getMethodDef(0, mref));
+				break;
+			}
+
+			case OpCodes.invokevirtual: {
+				int mref = 0xFFFF & bb.getShort();
+				if (cd != null)
+					cd.reference(getMethodDef(0, mref));
+				break;
+			}
+
+			case OpCodes.invokeinterface: {
+				int mref = 0xFFFF & bb.getShort();
+				if (cd != null)
+					cd.reference(getMethodDef(0, mref));
+				break;
+			}
+
+			case OpCodes.invokestatic: {
+				int methodref = 0xFFFF & bb.getShort();
+				if (cd != null)
+					cd.reference(getMethodDef(0, methodref));
+
+				if ((methodref == forName || methodref == class$) && lastReference != -1
+						&& pool[intPool[lastReference]] instanceof String) {
+					String clazz = (String) pool[intPool[lastReference]];
+					if (clazz.startsWith("[") || clazz.endsWith(";"))
+						parseReference(clazz, 0);
+					else {
+						int n = clazz.lastIndexOf('.');
+						if (n > 0)
+							packageReference(clazz.substring(0, n));
+					}
+				}
+				break;
+			}
+
+			case OpCodes.tableswitch:
+				// Skip to place divisible by 4
+				while ((bb.position() & 0x3) != 0)
+					bb.get();
+				/* int deflt = */
+				bb.getInt();
+				int low = bb.getInt();
+				int high = bb.getInt();
+				bb.position(bb.position() + (high - low + 1) * 4);
+				lastReference = -1;
+				break;
+
+			case OpCodes.lookupswitch:
+				// Skip to place divisible by 4
+				while ((bb.position() & 0x3) != 0)
+					bb.get();
+				/* deflt = */
+				bb.getInt();
+				int npairs = bb.getInt();
+				bb.position(bb.position() + npairs * 8);
+				lastReference = -1;
+				break;
+
+			default:
+				lastReference = -1;
+				bb.position(bb.position() + OpCodes.OFFSETS[instruction]);
+			}
+		}
+	}
+
+	private void doSourceFile(DataInputStream in) throws IOException {
+		int sourcefile_index = in.readUnsignedShort();
+		this.sourceFile = pool[sourcefile_index].toString();
+	}
+
+	private void doParameterAnnotations(DataInputStream in, ElementType member,
+			RetentionPolicy policy) throws IOException {
+		int num_parameters = in.readUnsignedByte();
+		for (int p = 0; p < num_parameters; p++) {
+			if (cd != null)
+				cd.parameter(p);
+			doAnnotations(in, member, policy);
+		}
+	}
+
+	private void doAnnotations(DataInputStream in, ElementType member, RetentionPolicy policy)
+			throws IOException {
+		int num_annotations = in.readUnsignedShort(); // # of annotations
+		for (int a = 0; a < num_annotations; a++) {
+			if (cd == null)
+				doAnnotation(in, member, policy, false);
+			else {
+				Annotation annotion = doAnnotation(in, member, policy, true);
+				cd.annotation(annotion);
+			}
+		}
+	}
+
+	private Annotation doAnnotation(DataInputStream in, ElementType member, RetentionPolicy policy,
+			boolean collect) throws IOException {
+		int type_index = in.readUnsignedShort();
+		if (annotations == null)
+			annotations = new HashSet<String>();
+
+		annotations.add(pool[type_index].toString());
+
+		if (policy == RetentionPolicy.RUNTIME) {
+			descriptors.add(new Integer(type_index));
+			hasRuntimeAnnotations = true;
+		} else {
+			hasClassAnnotations = true;
+		}
+		String name = (String) pool[type_index];
+		int num_element_value_pairs = in.readUnsignedShort();
+		Map<String, Object> elements = null;
+		for (int v = 0; v < num_element_value_pairs; v++) {
+			int element_name_index = in.readUnsignedShort();
+			String element = (String) pool[element_name_index];
+			Object value = doElementValue(in, member, policy, collect);
+			if (collect) {
+				if (elements == null)
+					elements = new LinkedHashMap<String, Object>();
+				elements.put(element, value);
+			}
+		}
+		if (collect)
+			return new Annotation(name, elements, member, policy);
+		else
+			return null;
+	}
+
+	private Object doElementValue(DataInputStream in, ElementType member, RetentionPolicy policy,
+			boolean collect) throws IOException {
+		char tag = (char) in.readUnsignedByte();
+		switch (tag) {
+		case 'B': // Byte
+		case 'C': // Character
+		case 'I': // Integer
+		case 'S': // Short
+			int const_value_index = in.readUnsignedShort();
+			return intPool[const_value_index];
+
+		case 'D': // Double
+		case 'F': // Float
+		case 's': // String
+		case 'J': // Long
+			const_value_index = in.readUnsignedShort();
+			return pool[const_value_index];
+
+		case 'Z': // Boolean
+			const_value_index = in.readUnsignedShort();
+			return pool[const_value_index] == null || pool[const_value_index].equals(0) ? false : true;
+
+		case 'e': // enum constant
+			int type_name_index = in.readUnsignedShort();
+			if (policy == RetentionPolicy.RUNTIME)
+				descriptors.add(new Integer(type_name_index));
+			int const_name_index = in.readUnsignedShort();
+			return pool[const_name_index];
+
+		case 'c': // Class
+			int class_info_index = in.readUnsignedShort();
+			if (policy == RetentionPolicy.RUNTIME)
+				descriptors.add(new Integer(class_info_index));
+			return pool[class_info_index];
+
+		case '@': // Annotation type
+			return doAnnotation(in, member, policy, collect);
+
+		case '[': // Array
+			int num_values = in.readUnsignedShort();
+			Object[] result = new Object[num_values];
+			for (int i = 0; i < num_values; i++) {
+				result[i] = doElementValue(in, member, policy, collect);
+			}
+			return result;
+
+		default:
+			throw new IllegalArgumentException("Invalid value for Annotation ElementValue tag "
+					+ tag);
+		}
+	}
+
+	/**
+	 * Add a new package reference.
+	 * 
+	 * @param pack
+	 *            A '.' delimited package name
+	 */
+	void packageReference(String pack) {
+		imports.add(pack);
+	}
+
+	/**
+	 * This method parses a descriptor and adds the package of the descriptor to
+	 * the referenced packages.
+	 * 
+	 * The syntax of the descriptor is:
+	 * 
+	 * <pre>
+	 *   descriptor ::= ( '(' reference * ')' )? reference
+	 *   reference  ::= 'L' classname ( '&lt;' references '&gt;' )? ';' | 'B' | 'Z' | ... | '+' | '-' | '['
+	 * </pre>
+	 * 
+	 * This methods uses heavy recursion to parse the descriptor and a roving
+	 * pointer to limit the creation of string objects.
+	 * 
+	 * @param descriptor
+	 *            The to be parsed descriptor
+	 * @param rover
+	 *            The pointer to start at
+	 */
+	public void parseDescriptor(String descriptor) {
+		// Some descriptors are weird, they start with a generic
+		// declaration that contains ':', not sure what they mean ...
+		if (descriptor.charAt(0) == '<')
+			return;
+
+		int rover = 0;
+		if (descriptor.charAt(rover) == '(') {
+			rover = parseReferences(descriptor, rover + 1, ')');
+			rover++;
+		}
+		parseReferences(descriptor, rover, (char) 0);
+	}
+
+	/**
+	 * Parse a sequence of references. A sequence ends with a given character or
+	 * when the string ends.
+	 * 
+	 * @param descriptor
+	 *            The whole descriptor.
+	 * @param rover
+	 *            The index in the descriptor
+	 * @param delimiter
+	 *            The end character or 0
+	 * @return the last index processed, one character after the delimeter
+	 */
+	int parseReferences(String descriptor, int rover, char delimiter) {
+		while (rover < descriptor.length() && descriptor.charAt(rover) != delimiter) {
+			rover = parseReference(descriptor, rover);
+		}
+		return rover;
+	}
+
+	/**
+	 * Parse a single reference. This can be a single character or an object
+	 * reference when it starts with 'L'.
+	 * 
+	 * @param descriptor
+	 *            The descriptor
+	 * @param rover
+	 *            The place to start
+	 * @return The return index after the reference
+	 */
+	int parseReference(String descriptor, int rover) {
+
+		char c = descriptor.charAt(rover);
+		while (c == '[')
+			c = descriptor.charAt(++rover);
+
+		if (c == '<') {
+			rover = parseReferences(descriptor, rover + 1, '>');
+		} else if (c == 'T') {
+			// Type variable name
+			rover++;
+			while (descriptor.charAt(rover) != ';')
+				rover++;
+		} else if (c == 'L') {
+			StringBuilder sb = new StringBuilder();
+			rover++;
+			int lastSlash = -1;
+			while ((c = descriptor.charAt(rover)) != ';') {
+				if (c == '<') {
+					rover = parseReferences(descriptor, rover + 1, '>');
+				} else if (c == '/') {
+					lastSlash = sb.length();
+					sb.append('.');
+				} else
+					sb.append(c);
+				rover++;
+			}
+			if (cd != null)
+				cd.addReference(sb.toString());
+
+			if (lastSlash > 0)
+				packageReference(sb.substring(0, lastSlash));
+		} else {
+			if ("+-*BCDFIJSZV".indexOf(c) < 0)
+				;// System.out.println("Should not skip: " + c);
+		}
+
+		// this skips a lot of characters
+		// [, *, +, -, B, etc.
+
+		return rover + 1;
+	}
+
+	public static String getPackage(String clazz) {
+		int n = clazz.lastIndexOf('/');
+		if (n < 0) {
+			n = clazz.lastIndexOf('.');
+			if (n < 0)
+				return ".";
+		}
+		return clazz.substring(0, n).replace('/', '.');
+	}
+
+	public Set<String> getReferred() {
+		return imports;
+	}
+
+	String getClassName() {
+		if (className == null)
+			return "NOCLASSNAME";
+		return className;
+	}
+
+	public String getPath() {
+		return path;
+	}
+
+	public String getSourceFile() {
+		return sourceFile;
+	}
+
+	/**
+	 * .class construct for different compilers
+	 * 
+	 * sun 1.1 Detect static variable class$com$acme$MyClass 1.2 " 1.3 " 1.4 "
+	 * 1.5 ldc_w (class) 1.6 "
+	 * 
+	 * eclipse 1.1 class$0, ldc (string), invokestatic Class.forName 1.2 " 1.3 "
+	 * 1.5 ldc (class) 1.6 "
+	 * 
+	 * 1.5 and later is not an issue, sun pre 1.5 is easy to detect the static
+	 * variable that decodes the class name. For eclipse, the class$0 gives away
+	 * we have a reference encoded in a string.
+	 * compilerversions/compilerversions.jar contains test versions of all
+	 * versions/compilers.
+	 */
+
+	public void reset() {
+		pool = null;
+		intPool = null;
+		xref = null;
+		classes = null;
+		descriptors = null;
+	}
+
+	public boolean is(QUERY query, Instruction instr, Analyzer analyzer) throws Exception {
+		switch (query) {
+		case ANY:
+			return true;
+
+		case NAMED:
+			if (instr.matches(getClassName()))
+				return !instr.isNegated();
+			return false;
+
+		case VERSION:
+			String v = major + "/" + minor;
+			if (instr.matches(v))
+				return !instr.isNegated();
+			return false;
+
+		case IMPLEMENTS:
+			for (int i = 0; interfaces != null && i < interfaces.length; i++) {
+				if (instr.matches(interfaces[i]))
+					return !instr.isNegated();
+			}
+			break;
+
+		case EXTENDS:
+			if (zuper == null)
+				return false;
+
+			if (instr.matches(zuper))
+				return !instr.isNegated();
+			break;
+
+		case PUBLIC:
+			return !isPublic;
+
+		case CONCRETE:
+			return !isAbstract;
+
+		case ANNOTATION:
+			if (annotations == null)
+				return false;
+
+			if (annotations.contains(instr.getPattern()))
+				return true;
+
+			for (String annotation : annotations) {
+				if (instr.matches(annotation))
+					return !instr.isNegated();
+			}
+
+			return false;
+
+		case RUNTIMEANNOTATIONS:
+			return hasClassAnnotations;
+		case CLASSANNOTATIONS:
+			return hasClassAnnotations;
+
+		case ABSTRACT:
+			return isAbstract;
+
+		case IMPORTS:
+			for (String imp : imports) {
+				if (instr.matches(imp.replace('.', '/')))
+					return !instr.isNegated();
+			}
+		}
+
+		if (zuper == null)
+			return false;
+
+		Clazz clazz = analyzer.findClass(zuper + ".class");
+		if (clazz == null)
+			return false;
+
+		return clazz.is(query, instr, analyzer);
+	}
+
+	public String toString() {
+		return getFQN();
+	}
+
+	public String getFQN() {
+		String s = getClassName().replace('/', '.');
+		return s;
+	}
+
+	/**
+	 * Return a list of packages implemented by this class.
+	 * 
+	 * @param implemented
+	 * @param classspace
+	 * @param clazz
+	 * @throws Exception
+	 */
+	@SuppressWarnings("deprecation") final static String	USEPOLICY		= toDescriptor(UsePolicy.class);
+	final static String										PROVIDERPOLICY	= toDescriptor(ProviderType.class);
+
+	public static void getImplementedPackages(Set<String> implemented, Analyzer analyzer,
+			Clazz clazz) throws Exception {
+		if (clazz.interfaces != null) {
+			for (String interf : clazz.interfaces) {
+				interf = interf + ".class";
+				Clazz c = analyzer.getClassspace().get(interf);
+
+				// If not found, actually parse the imported
+				// class file to check for implementation policy.
+				if (c == null)
+					c = analyzer.findClass(interf);
+
+				if (c != null) {
+					boolean consumer = false;
+					Set<String> annotations = c.annotations;
+					if (annotations != null)
+						// Override if we marked the interface as a consumer
+						// interface
+						consumer = annotations.contains(USEPOLICY)
+								|| annotations.contains(PROVIDERPOLICY);
+
+					if (!consumer)
+						implemented.add(getPackage(interf));
+					getImplementedPackages(implemented, analyzer, c);
+				} else
+					implemented.add(getPackage(interf));
+
+			}
+		}
+		if (clazz.zuper != null) {
+			Clazz c = analyzer.getClassspace().get(clazz.zuper);
+			if (c != null) {
+				getImplementedPackages(implemented, analyzer, c);
+			}
+		}
+
+	}
+
+	// String RNAME = "LaQute/bnd/annotation/UsePolicy;";
+
+	public static String toDescriptor(Class<?> clazz) {
+		StringBuilder sb = new StringBuilder();
+		sb.append('L');
+		sb.append(clazz.getName().replace('.', '/'));
+		sb.append(';');
+		return sb.toString();
+	}
+
+	MethodDef getMethodDef(int access, int methodRefPoolIndex) {
+		Object o = pool[methodRefPoolIndex];
+		if (o != null && o instanceof Assoc) {
+			Assoc assoc = (Assoc) o;
+			if (assoc.tag == 10) {
+				int string_index = intPool[assoc.a];
+				String className = (String) pool[string_index];
+				int name_and_type_index = assoc.b;
+				Assoc name_and_type = (Assoc) pool[name_and_type_index];
+				if (name_and_type.tag == 12) {
+					// Name and Type
+					int name_index = name_and_type.a;
+					int type_index = name_and_type.b;
+					String method = (String) pool[name_index];
+					String descriptor = (String) pool[type_index];
+					return new MethodDef(access, className, method, descriptor);
+				} else
+					throw new IllegalArgumentException(
+							"Invalid class file (or parsing is wrong), assoc is not type + name (12)");
+			} else
+				throw new IllegalArgumentException(
+						"Invalid class file (or parsing is wrong), Assoc is not method ref! (10)");
+		} else
+			throw new IllegalArgumentException(
+					"Invalid class file (or parsing is wrong), Not an assoc at a method ref");
+	}
+
+	public static String getShortName(String cname) {
+		int n = cname.lastIndexOf('.');
+		if (n < 0)
+			return cname;
+		return cname.substring(n + 1, cname.length());
+	}
+
+	public static String fqnToPath(String dotted) {
+		return dotted.replace('.', '/') + ".class";
+	}
+
+	public static String fqnToBinary(String dotted) {
+		return "L" + dotted.replace('.', '/') + ";";
+	}
+
+	public static String pathToFqn(String path) {
+		return path.replace('/', '.').substring(0, path.length() - 6);
+	}
+
+	public boolean isPublic() {
+		return isPublic;
+	}
+
+	public boolean isEnum() {
+		return isEnum;
+	}
+
+	public JAVA getFormat() {
+		return JAVA.format(major);
+
+	}
+
+	public static String objectDescriptorToFQN(String string) {
+		if (string.startsWith("L") && string.endsWith(";"))
+			return string.substring(1, string.length() - 1).replace('/', '.');
+
+		switch (string.charAt(0)) {
+		case 'V':
+			return "void";
+		case 'B':
+			return "byte";
+		case 'C':
+			return "char";
+		case 'I':
+			return "int";
+		case 'S':
+			return "short";
+		case 'D':
+			return "double";
+		case 'F':
+			return "float";
+		case 'J':
+			return "long";
+		case 'Z':
+			return "boolean";
+		case '[': // Array
+			return objectDescriptorToFQN(string.substring(1)) + "[]";
+		}
+		throw new IllegalArgumentException("Invalid type character in descriptor " + string);
+	}
+
+	public static String internalToFqn(String string) {
+		return string.replace('/', '.');
+	}
+
+	public static String unCamel(String id) {
+		StringBuilder out = new StringBuilder();
+		for (int i = 0; i < id.length(); i++) {
+			char c = id.charAt(i);
+			if (c == '_' || c == '$' || c == '.') {
+				if (out.length() > 0 && !Character.isWhitespace(out.charAt(out.length() - 1)))
+					out.append(' ');
+				continue;
+			}
+
+			int n = i;
+			while (n < id.length() && Character.isUpperCase(id.charAt(n))) {
+				n++;
+			}
+			if (n == i)
+				out.append(id.charAt(i));
+			else {
+				boolean tolower = (n - i) == 1;
+				if (i > 0 && !Character.isWhitespace(out.charAt(out.length() - 1)))
+					out.append(' ');
+
+				for (; i < n;) {
+					if (tolower)
+						out.append(Character.toLowerCase(id.charAt(i)));
+					else
+						out.append(id.charAt(i));
+					i++;
+				}
+				i--;
+			}
+		}
+		if (id.startsWith("."))
+			out.append(" *");
+		out.replace(0, 1, Character.toUpperCase(out.charAt(0)) + "");
+		return out.toString();
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Constants.java b/bundleplugin/src/main/java/aQute/lib/osgi/Constants.java
new file mode 100644
index 0000000..f1afe72
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Constants.java
@@ -0,0 +1,240 @@
+package aQute.lib.osgi;
+
+import java.nio.charset.*;
+import java.util.regex.*;
+
+public interface Constants {
+	/*
+	 * Defined in OSGi
+	 */
+	/**
+	 * @syntax Bundle-ActivationPolicy ::= policy ( ’;’ directive )* policy ::=
+	 *         ’lazy’
+	 */
+	String					BND_ADDXMLTOTEST							= "Bnd-AddXMLToTest";
+	String					BUNDLE_ACTIVATIONPOLICY						= "Bundle-ActivationPolicy";
+	String					BUNDLE_ACTIVATOR							= "Bundle-Activator";
+	String					BUNDLE_BLUEPRINT							= "Bundle-Copyright";
+	String					BUNDLE_CATEGORY								= "Bundle-Category";
+	String					BUNDLE_CLASSPATH							= "Bundle-ClassPath";
+	String					BUNDLE_CONTACTADDRESS						= "Bundle-ContactAddress";
+	String					BUNDLE_COPYRIGHT							= "Bundle-Copyright";
+	String					BUNDLE_DESCRIPTION							= "Bundle-Description";
+	String					BUNDLE_DOCURL								= "Bundle-DocURL";
+	String					BUNDLE_ICON									= "Bundle-Icon";
+	String					BUNDLE_LICENSE								= "Bundle-License";
+	String					BUNDLE_LOCALIZATION							= "Bundle-Localization";
+	String					BUNDLE_MANIFESTVERSION						= "Bundle-ManifestVersion";
+	String					BUNDLE_NAME									= "Bundle-Name";
+	String					BUNDLE_NATIVECODE							= "Bundle-NativeCode";
+	String					BUNDLE_REQUIREDEXECUTIONENVIRONMENT			= "Bundle-RequiredExecutionEnvironment";
+	String					BUNDLE_SYMBOLICNAME							= "Bundle-SymbolicName";
+	String					BUNDLE_UPDATELOCATION						= "Bundle-UpdateLocation";
+	String					BUNDLE_VENDOR								= "Bundle-Vendor";
+	String					BUNDLE_VERSION								= "Bundle-Version";
+	String					DYNAMICIMPORT_PACKAGE						= "DynamicImport-Package";
+	String					EXPORT_PACKAGE								= "Export-Package";
+	String					EXPORT_SERVICE								= "Export-Service";
+	String					FRAGMENT_HOST								= "Fragment-Host";
+	String					IMPORT_PACKAGE								= "Import-Package";
+	String					IMPORT_SERVICE								= "Import-Service";
+	String					REQUIRE_BUNDLE								= "Require-Bundle";
+	String					SERVICE_COMPONENT							= "Service-Component";
+
+	String					PRIVATE_PACKAGE								= "Private-Package";
+	String					IGNORE_PACKAGE								= "Ignore-Package";
+	String					INCLUDE_RESOURCE							= "Include-Resource";
+	String					CONDITIONAL_PACKAGE							= "Conditional-Package";
+	String					BND_LASTMODIFIED							= "Bnd-LastModified";
+	String					CREATED_BY									= "Created-By";
+	String					TOOL										= "Tool";
+	String					TESTCASES									= "Test-Cases";
+	String					SIGNATURE_TEST								= "-signaturetest";
+
+	String					headers[]									= { BUNDLE_ACTIVATOR,
+			BUNDLE_CONTACTADDRESS, BUNDLE_COPYRIGHT, BUNDLE_DESCRIPTION, BUNDLE_DOCURL,
+			BUNDLE_LOCALIZATION, BUNDLE_NATIVECODE, BUNDLE_VENDOR, BUNDLE_VERSION, BUNDLE_LICENSE,
+			BUNDLE_CLASSPATH, SERVICE_COMPONENT, EXPORT_PACKAGE, IMPORT_PACKAGE,
+			BUNDLE_LOCALIZATION, BUNDLE_MANIFESTVERSION, BUNDLE_NAME, BUNDLE_NATIVECODE,
+			BUNDLE_REQUIREDEXECUTIONENVIRONMENT, BUNDLE_SYMBOLICNAME, BUNDLE_VERSION,
+			FRAGMENT_HOST, PRIVATE_PACKAGE, IGNORE_PACKAGE, INCLUDE_RESOURCE, REQUIRE_BUNDLE,
+			IMPORT_SERVICE, EXPORT_SERVICE, CONDITIONAL_PACKAGE, BND_LASTMODIFIED, TESTCASES,
+			SIGNATURE_TEST												};
+
+	String					BUILDPATH									= "-buildpath";
+	String					BUILDPACKAGES								= "-buildpackages";
+	String					BUMPPOLICY									= "-bumppolicy";
+	String					CONDUIT										= "-conduit";
+	String					COMPILER_SOURCE								= "-source";
+	String					COMPILER_TARGET								= "-target";
+	String					DEPENDSON									= "-dependson";
+	String					DEPLOY										= "-deploy";
+	String					DEPLOYREPO									= "-deployrepo";
+	String					DONOTCOPY									= "-donotcopy";
+	String					DEBUG										= "-debug";
+	String					EXPORT_CONTENTS								= "-exportcontents";
+	String					FAIL_OK										= "-failok";
+	String					INCLUDE										= "-include";
+	String					INCLUDERESOURCE								= "-includeresource";
+	String					MAKE										= "-make";
+	String					METATYPE									= "-metatype";
+	String					MANIFEST									= "-manifest";
+	String					SAVEMANIFEST								= "-savemanifest";
+	String					NODEFAULTVERSION							= "-nodefaultversion";
+	String					NOEXTRAHEADERS								= "-noextraheaders";
+	String					NOMANIFEST									= "-nomanifest";
+	String					NOUSES										= "-nouses";
+	@Deprecated String		NOPE										= "-nope";
+	String					NOBUNDLES									= "-nobundles";
+	String					PEDANTIC									= "-pedantic";
+	String					PLUGIN										= "-plugin";
+	String					POM											= "-pom";
+	String					RELEASEREPO									= "-releaserepo";
+	String					REMOVEHEADERS								= "-removeheaders";
+	String					RESOURCEONLY								= "-resourceonly";
+	String					SOURCES										= "-sources";
+	String					SOURCEPATH									= "-sourcepath";
+	String					SUB											= "-sub";
+	String					RUNPROPERTIES								= "-runproperties";
+	String					RUNSYSTEMPACKAGES							= "-runsystempackages";
+	String					RUNBUNDLES									= "-runbundles";
+	String					RUNPATH										= "-runpath";
+	String					RUNSTORAGE									= "-runstorage";
+	String					RUNBUILDS									= "-runbuilds";
+	String					RUNPATH_MAIN_DIRECTIVE						= "main:";
+	String					RUNPATH_LAUNCHER_DIRECTIVE					= "launcher:";
+	String					RUNVM										= "-runvm";
+	String					RUNTRACE									= "-runtrace";
+	String					RUNFRAMEWORK								= "-runframework";
+	String					RUNTIMEOUT									= "-runtimeout";
+	String					SNAPSHOT									= "-snapshot";
+	String					RUNFRAMEWORK_SERVICES						= "services";
+	String					RUNFRAMEWORK_NONE							= "none";
+	String					REPORTNEWER									= "-reportnewer";
+	String					SIGN										= "-sign";
+	String					TESTPACKAGES								= "-testpackages";
+	String					TESTREPORT									= "-testreport";
+	String					TESTPATH									= "-testpath";
+	String					TESTCONTINUOUS								= "-testcontinuous";
+	String					UNDERTEST									= "-undertest";
+	String					VERBOSE										= "-verbose";
+	@Deprecated String		VERSIONPOLICY_IMPL							= "-versionpolicy-impl";
+	@Deprecated String		VERSIONPOLICY_USES							= "-versionpolicy-uses";
+	String					PROVIDER_POLICY								= "-provider-policy";
+	String					CONSUMER_POLICY								= "-consumer-policy";
+	@Deprecated String		VERSIONPOLICY								= "-versionpolicy";
+	String					WAB											= "-wab";
+	String					WABLIB										= "-wablib";
+	String					REQUIRE_BND									= "-require-bnd";
+
+	// Deprecated
+	String					CLASSPATH									= "-classpath";
+
+	String					options[]									= { BUILDPATH, BUMPPOLICY,
+			CONDUIT, CLASSPATH, CONSUMER_POLICY, DEPENDSON, DONOTCOPY, EXPORT_CONTENTS, FAIL_OK,
+			INCLUDE, INCLUDERESOURCE, MAKE, MANIFEST, NOEXTRAHEADERS, NOUSES, NOBUNDLES, PEDANTIC,
+			PLUGIN, POM, PROVIDER_POLICY, REMOVEHEADERS, RESOURCEONLY, SOURCES, SOURCEPATH,
+			SOURCES, SOURCEPATH, SUB, RUNBUNDLES, RUNPATH, RUNSYSTEMPACKAGES, RUNPROPERTIES,
+			REPORTNEWER, UNDERTEST, TESTPATH, TESTPACKAGES, TESTREPORT, VERBOSE, NOMANIFEST,
+			DEPLOYREPO, RELEASEREPO, SAVEMANIFEST, RUNVM, WAB, WABLIB, RUNFRAMEWORK, RUNTRACE,
+			TESTCONTINUOUS, SNAPSHOT												};
+
+	// Ignore bundle specific headers. These bundles do not make
+	// a lot of sense to inherit
+	String[]				BUNDLE_SPECIFIC_HEADERS						= new String[] {
+			INCLUDE_RESOURCE, BUNDLE_ACTIVATOR, BUNDLE_CLASSPATH, BUNDLE_NAME, BUNDLE_NATIVECODE,
+			BUNDLE_SYMBOLICNAME, IMPORT_PACKAGE, EXPORT_PACKAGE, DYNAMICIMPORT_PACKAGE,
+			FRAGMENT_HOST, REQUIRE_BUNDLE, PRIVATE_PACKAGE, EXPORT_CONTENTS, TESTCASES, NOMANIFEST,
+			SIGNATURE_TEST, WAB, WABLIB								};
+
+	char					DUPLICATE_MARKER							= '~';
+	String					SPECIFICATION_VERSION						= "specification-version";
+	String					SPLIT_PACKAGE_DIRECTIVE						= "-split-package:";
+	String					IMPORT_DIRECTIVE							= "-import:";
+	String					NO_IMPORT_DIRECTIVE							= "-noimport:";
+	String					REMOVE_ATTRIBUTE_DIRECTIVE					= "-remove-attribute:";
+	String					LIB_DIRECTIVE								= "lib:";
+	String					NOANNOTATIONS								= "-noannotations";
+	String					COMMAND_DIRECTIVE							= "command:";
+	String					USES_DIRECTIVE								= "uses:";
+	String					MANDATORY_DIRECTIVE							= "mandatory:";
+	String					INCLUDE_DIRECTIVE							= "include:";
+	String					PROVIDE_DIRECTIVE							= "provide:";
+	String					EXCLUDE_DIRECTIVE							= "exclude:";
+	String					PRESENCE_DIRECTIVE							= "presence:";
+	String					PRIVATE_DIRECTIVE							= "private:";
+	String					SINGLETON_DIRECTIVE							= "singleton:";
+	String					EXTENSION_DIRECTIVE							= "extension:";
+	String					VISIBILITY_DIRECTIVE						= "visibility:";
+	String					FRAGMENT_ATTACHMENT_DIRECTIVE				= "fragment-attachment:";
+	String					RESOLUTION_DIRECTIVE						= "resolution:";
+	String					PATH_DIRECTIVE								= "path:";
+	String					SIZE_ATTRIBUTE								= "size";
+	String					LINK_ATTRIBUTE								= "link";
+	String					NAME_ATTRIBUTE								= "name";
+	String					DESCRIPTION_ATTRIBUTE						= "description";
+	String					OSNAME_ATTRIBUTE							= "osname";
+	String					OSVERSION_ATTRIBUTE							= "osversion";
+	String					PROCESSOR_ATTRIBUTE							= "processor";
+	String					LANGUAGE_ATTRIBUTE							= "language";
+	String					SELECTION_FILTER_ATTRIBUTE					= "selection-filter";
+	String					BLUEPRINT_WAIT_FOR_DEPENDENCIES_ATTRIBUTE	= "blueprint.wait-for-dependencies";
+	String					BLUEPRINT_TIMEOUT_ATTRIBUTE					= "blueprint.timeout";
+	String					VERSION_ATTRIBUTE							= "version";
+	String					BUNDLE_SYMBOLIC_NAME_ATTRIBUTE				= "bundle-symbolic-name";
+	String					BUNDLE_VERSION_ATTRIBUTE					= "bundle-version";
+	String					FROM_DIRECTIVE								= "from:";
+
+	String					KEYSTORE_LOCATION_DIRECTIVE					= "keystore:";
+	String					KEYSTORE_PROVIDER_DIRECTIVE					= "provider:";
+	String					KEYSTORE_PASSWORD_DIRECTIVE					= "password:";
+	String					SIGN_PASSWORD_DIRECTIVE						= "sign-password:";
+
+	String					NONE										= "none";
+
+	String					directives[]								= {
+			SPLIT_PACKAGE_DIRECTIVE, NO_IMPORT_DIRECTIVE, IMPORT_DIRECTIVE, RESOLUTION_DIRECTIVE,
+			INCLUDE_DIRECTIVE, USES_DIRECTIVE, EXCLUDE_DIRECTIVE, KEYSTORE_LOCATION_DIRECTIVE,
+			KEYSTORE_PROVIDER_DIRECTIVE, KEYSTORE_PASSWORD_DIRECTIVE, SIGN_PASSWORD_DIRECTIVE,
+			COMMAND_DIRECTIVE, NOANNOTATIONS, LIB_DIRECTIVE, RUNPATH_LAUNCHER_DIRECTIVE,
+			FROM_DIRECTIVE, PRIVATE_DIRECTIVE
+
+																		// TODO
+																		};
+
+	String					USES_USES									= "<<USES>>";
+	String					CURRENT_USES								= "@uses";
+	String					IMPORT_REFERENCE							= "reference";
+	String					IMPORT_PRIVATE								= "private";
+	String[]				importDirectives							= { IMPORT_REFERENCE,
+			IMPORT_PRIVATE												};
+
+	static final Pattern	VALID_PROPERTY_TYPES						= Pattern
+																				.compile("(String|Long|Double|Float|Integer|Byte|Character|Boolean|Short)");
+
+	String					DEFAULT_BND_EXTENSION						= ".bnd";
+	String					DEFAULT_JAR_EXTENSION						= ".jar";
+	String					DEFAULT_BAR_EXTENSION						= ".bar";
+	String					DEFAULT_BNDRUN_EXTENSION					= ".bndrun";
+	String[]				METAPACKAGES								= { "META-INF", "OSGI-INF",
+			"OSGI-OPT"													};
+
+	String					CURRENT_VERSION								= "@";
+	String					CURRENT_PACKAGE								= "@package";
+
+	String					BUILDFILES									= "buildfiles";
+
+	String					EMPTY_HEADER								= "<<EMPTY>>";
+
+	String					EMBEDDED_REPO								= "/embedded-repo.jar";
+	String					LAUNCHER_PLUGIN								= "Launcher-Plugin";
+	String					TESTER_PLUGIN								= "Tester-Plugin";
+
+	String					DEFAULT_LAUNCHER_BSN						= "biz.aQute.launcher";
+	String					DEFAULT_TESTER_BSN							= "biz.aQute.junit";
+
+	String					DEFAULT_DO_NOT_COPY							= "CVS|\\.svn|\\.git|\\.DS_Store";
+
+	Charset					DEFAULT_CHARSET								= Charset.forName("UTF8");
+	String					VERSION_FILTER	= "version";
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/EmbeddedResource.java b/bundleplugin/src/main/java/aQute/lib/osgi/EmbeddedResource.java
new file mode 100644
index 0000000..cb302da
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/EmbeddedResource.java
@@ -0,0 +1,86 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+import java.util.zip.*;
+
+public class EmbeddedResource implements Resource {
+	byte	data[];
+	long 	lastModified;
+	String	extra;
+
+	public EmbeddedResource(byte data[], long lastModified) {
+		this.data = data;
+		this.lastModified = lastModified;
+	}
+
+	public InputStream openInputStream() throws FileNotFoundException {
+		return new ByteArrayInputStream(data);
+	}
+
+	public void write(OutputStream out) throws IOException {
+		out.write(data);
+	}
+
+	public String toString() {
+		return ":" + data.length + ":";
+	}
+
+	public static void build(Jar jar, InputStream in, long lastModified) throws IOException {
+		ZipInputStream jin = new ZipInputStream(in);
+		ZipEntry entry = jin.getNextEntry();
+		while (entry != null) {
+			if (!entry.isDirectory()) {
+				byte data[] = collect(jin);
+				jar.putResource(entry.getName(), new EmbeddedResource(data, lastModified), true);
+			}
+			entry = jin.getNextEntry();
+		}
+		jin.close();
+	}
+
+	/**
+	 * Convenience method to turn an inputstream into a byte array. The method
+	 * uses a recursive algorithm to minimize memory usage.
+	 * 
+	 * @param in stream with data
+	 * @param offset where we are in the stream
+	 * @returns byte array filled with data
+	 */
+    static byte[] collect(InputStream in) throws IOException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        copy(in,out);
+        return out.toByteArray();
+    }
+
+    static void copy(InputStream in, OutputStream out) throws IOException {
+        int available = in.available();
+        if ( available <= 10000)
+            available = 64000;
+        byte [] buffer = new byte[available];
+        int size;
+        while ( (size=in.read(buffer))>0)
+            out.write(buffer,0,size);
+    }
+
+	public long lastModified() {
+		return lastModified;
+	}
+
+	public static void build(Jar sub, Resource resource) throws Exception {
+			InputStream in = resource.openInputStream();
+			build(sub,in, resource.lastModified());
+			in.close();
+	}
+
+	public String getExtra() {
+		return extra;
+	}
+
+	public void setExtra(String extra) {
+		this.extra = extra;
+	}
+
+	public long size() {
+	    return data.length;
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/FileResource.java b/bundleplugin/src/main/java/aQute/lib/osgi/FileResource.java
new file mode 100644
index 0000000..4c57321
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/FileResource.java
@@ -0,0 +1,84 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+import java.util.regex.Pattern;
+
+public class FileResource implements Resource {
+	File	file;
+	String	extra;
+	
+	public FileResource(File file) {
+		this.file = file;
+	}
+
+	public InputStream openInputStream() throws FileNotFoundException {
+		return new FileInputStream(file);
+	}
+
+	public static void build(Jar jar, File directory, Pattern doNotCopy) {
+		traverse(
+				jar,
+				directory.getAbsolutePath().length(),
+				directory,
+				doNotCopy);
+	}
+
+	public String toString() {
+		return ":" + file.getName() + ":";
+	}
+
+	public void write(OutputStream out) throws Exception {
+		copy(this, out);
+	}
+
+	static synchronized void copy(Resource resource, OutputStream out)
+			throws Exception {
+		InputStream in = resource.openInputStream();
+		try {
+			byte buffer[] = new byte[20000];
+			int size = in.read(buffer);
+			while (size > 0) {
+				out.write(buffer, 0, size);
+				size = in.read(buffer);
+			}
+		}
+		finally {
+			in.close();
+		}
+	}
+
+	static void traverse(Jar jar, int rootlength, File directory,
+			Pattern doNotCopy) {
+		if (doNotCopy != null && doNotCopy.matcher(directory.getName()).matches())
+			return;
+
+		File files[] = directory.listFiles();
+		for (int i = 0; i < files.length; i++) {
+			if (files[i].isDirectory())
+				traverse(jar, rootlength, files[i], doNotCopy);
+			else {
+				String path = files[i].getAbsolutePath().substring(
+						rootlength + 1);
+				if (File.separatorChar != '/')
+					path = path.replace(File.separatorChar, '/');
+				jar.putResource(path, new FileResource(files[i]), true);
+			}
+		}
+	}
+
+	public long lastModified() {
+		return file.lastModified();
+	}
+
+	public String getExtra() {
+		return extra;
+	}
+
+	public void setExtra(String extra) {
+		this.extra = extra;
+	}
+	
+	public long size() {
+	    return (int) file.length();
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Instruction.java b/bundleplugin/src/main/java/aQute/lib/osgi/Instruction.java
new file mode 100644
index 0000000..f466f57
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Instruction.java
@@ -0,0 +1,130 @@
+package aQute.lib.osgi;
+
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.libg.generics.*;
+
+public class Instruction {
+    Pattern pattern;
+    String  instruction;
+    boolean negated;
+    boolean optional;
+
+    public Instruction(String instruction, boolean negated) {
+        this.instruction = instruction;
+        this.negated = negated;
+    }
+
+    public boolean matches(String value) {
+        return getMatcher(value).matches();
+    }
+
+    public boolean isNegated() {
+        return negated;
+    }
+
+    public String getPattern() {
+        return instruction;
+    }
+
+    /**
+     * Convert a string based pattern to a regular expression based pattern.
+     * This is called an instruction, this object makes it easier to handle the
+     * different cases
+     * 
+     * @param string
+     * @return
+     */
+    public static Instruction getPattern(String string) {
+        boolean negated = false;
+        if (string.startsWith("!")) {
+            negated = true;
+            string = string.substring(1);
+        }
+        StringBuffer sb = new StringBuffer();
+        for (int c = 0; c < string.length(); c++) {
+            switch (string.charAt(c)) {
+            case '.':
+                sb.append("\\.");
+                break;
+            case '*':
+                sb.append(".*");
+                break;
+            case '?':
+                sb.append(".?");
+                break;
+            default:
+                sb.append(string.charAt(c));
+                break;
+            }
+        }
+        string = sb.toString();
+        if (string.endsWith("\\..*")) {
+            sb.append("|");
+            sb.append(string.substring(0, string.length() - 4));
+        }
+        return new Instruction(sb.toString(), negated);
+    }
+
+    public String toString() {
+        return getPattern();
+    }
+
+    public Matcher getMatcher(String value) {
+        if (pattern == null) {
+            pattern = Pattern.compile(instruction);
+        }
+        return pattern.matcher(value);
+    }
+
+    public int hashCode() {
+        return instruction.hashCode();
+    }
+
+    public boolean equals(Object other) {
+        return other != null && (other instanceof Instruction)
+                && instruction.equals(((Instruction) other).instruction);
+    }
+
+    public void setOptional() {
+        optional = true;
+    }
+
+    public boolean isOptional() {
+        return optional;
+    }
+
+    public static Map<Instruction, Map<String, String>> replaceWithInstruction(
+            Map<String, Map<String, String>> header) {
+        Map<Instruction, Map<String, String>> map = Processor.newMap();
+        for (Iterator<Map.Entry<String, Map<String, String>>> e = header
+                .entrySet().iterator(); e.hasNext();) {
+            Map.Entry<String, Map<String, String>> entry = e.next();
+            String pattern = entry.getKey();
+            Instruction instr = getPattern(pattern);
+            String presence = entry.getValue()
+                    .get(Constants.PRESENCE_DIRECTIVE);
+            if ("optional".equals(presence))
+                instr.setOptional();
+            map.put(instr, entry.getValue());
+        }
+        return map;
+    }
+
+    public static <T> Collection<T> select(Collection<Instruction> matchers,
+            Collection<T> targets) {
+        Collection<T> result = Create.list();
+        outer: for (T t : targets) {
+            String s = t.toString();
+            for (Instruction i : matchers) {
+                if (i.matches(s)) {
+                    if (!i.isNegated())
+                        result.add(t);
+                    continue outer;
+                }
+            }
+        }
+        return result;
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/InstructionFilter.java b/bundleplugin/src/main/java/aQute/lib/osgi/InstructionFilter.java
new file mode 100644
index 0000000..83f85ee
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/InstructionFilter.java
@@ -0,0 +1,37 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+import java.util.regex.*;
+
+public class InstructionFilter implements FileFilter {
+
+	private Instruction instruction;
+	private boolean recursive;
+	private Pattern doNotCopy;
+	
+	public InstructionFilter (Instruction instruction, boolean recursive, Pattern doNotCopy) {
+		this.instruction = instruction;
+		this.recursive = recursive;
+		this.doNotCopy = doNotCopy;
+	}
+	public InstructionFilter (Instruction instruction, boolean recursive) {
+		this(instruction, recursive, Pattern.compile(Constants.DEFAULT_DO_NOT_COPY));
+	}
+	public boolean isRecursive() {
+		return recursive;
+	}
+	public boolean accept(File pathname) {
+		if (doNotCopy != null && doNotCopy.matcher(pathname.getName()).matches()) {
+			return false;
+		}
+
+		if (pathname.isDirectory() && isRecursive()) {
+			return true;
+		}
+		
+		if (instruction == null) {
+			return true;
+		}
+		return !instruction.isNegated() == instruction.matches(pathname.getName());
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Jar.java b/bundleplugin/src/main/java/aQute/lib/osgi/Jar.java
new file mode 100644
index 0000000..c8b2359
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Jar.java
@@ -0,0 +1,690 @@
+package aQute.lib.osgi;
+
+import static aQute.lib.io.IO.*;
+
+import java.io.*;
+import java.security.*;
+import java.util.*;
+import java.util.jar.*;
+import java.util.regex.*;
+import java.util.zip.*;
+
+import aQute.lib.base64.*;
+import aQute.libg.reporter.*;
+
+public class Jar implements Closeable {
+	public static final Object[]		EMPTY_ARRAY	= new Jar[0];
+	Map<String, Resource>				resources	= new TreeMap<String, Resource>();
+	Map<String, Map<String, Resource>>	directories	= new TreeMap<String, Map<String, Resource>>();
+	Manifest							manifest;
+	boolean								manifestFirst;
+	String								name;
+	File								source;
+	ZipFile								zipFile;
+	long								lastModified;
+	String								lastModifiedReason;
+	Reporter							reporter;
+	boolean								doNotTouchManifest;
+	boolean								nomanifest;
+
+	public Jar(String name) {
+		this.name = name;
+	}
+
+	public Jar(String name, File dirOrFile, Pattern doNotCopy) throws ZipException, IOException {
+		this(name);
+		source = dirOrFile;
+		if (dirOrFile.isDirectory())
+			FileResource.build(this, dirOrFile, doNotCopy);
+		else if (dirOrFile.isFile()) {
+			zipFile = ZipResource.build(this, dirOrFile);
+		} else {
+			throw new IllegalArgumentException("A Jar can only accept a valid file or directory: "
+					+ dirOrFile);
+		}
+	}
+
+	public Jar(String name, InputStream in, long lastModified) throws IOException {
+		this(name);
+		EmbeddedResource.build(this, in, lastModified);
+	}
+
+	public Jar(String name, String path) throws IOException {
+		this(name);
+		File f = new File(path);
+		InputStream in = new FileInputStream(f);
+		EmbeddedResource.build(this, in, f.lastModified());
+		in.close();
+	}
+
+	public Jar(File f) throws IOException {
+		this(getName(f), f, null);
+	}
+
+	/**
+	 * Make the JAR file name the project name if we get a src or bin directory.
+	 * 
+	 * @param f
+	 * @return
+	 */
+	private static String getName(File f) {
+		f = f.getAbsoluteFile();
+		String name = f.getName();
+		if (name.equals("bin") || name.equals("src"))
+			return f.getParentFile().getName();
+		else {
+			if (name.endsWith(".jar"))
+				name = name.substring(0, name.length() - 4);
+			return name;
+		}
+	}
+
+	public Jar(String string, InputStream resourceAsStream) throws IOException {
+		this(string, resourceAsStream, 0);
+	}
+
+	public Jar(String string, File file) throws ZipException, IOException {
+		this(string, file, Pattern.compile(Constants.DEFAULT_DO_NOT_COPY));
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String toString() {
+		return "Jar:" + name;
+	}
+
+	public boolean putResource(String path, Resource resource) {
+		return putResource(path, resource, true);
+	}
+
+	public boolean putResource(String path, Resource resource, boolean overwrite) {
+		updateModified(resource.lastModified(), path);
+		while (path.startsWith("/"))
+			path = path.substring(1);
+
+		if (path.equals("META-INF/MANIFEST.MF")) {
+			manifest = null;
+			if (resources.isEmpty())
+				manifestFirst = true;
+		}
+		String dir = getDirectory(path);
+		Map<String, Resource> s = directories.get(dir);
+		if (s == null) {
+			s = new TreeMap<String, Resource>();
+			directories.put(dir, s);
+			int n = dir.lastIndexOf('/');
+			while (n > 0) {
+				String dd = dir.substring(0, n);
+				if (directories.containsKey(dd))
+					break;
+				directories.put(dd, null);
+				n = dd.lastIndexOf('/');
+			}
+		}
+		boolean duplicate = s.containsKey(path);
+		if (!duplicate || overwrite) {
+			resources.put(path, resource);
+			s.put(path, resource);
+		}
+		return duplicate;
+	}
+
+	public Resource getResource(String path) {
+		return resources.get(path);
+	}
+
+	private String getDirectory(String path) {
+		int n = path.lastIndexOf('/');
+		if (n < 0)
+			return "";
+
+		return path.substring(0, n);
+	}
+
+	public Map<String, Map<String, Resource>> getDirectories() {
+		return directories;
+	}
+
+	public Map<String, Resource> getResources() {
+		return resources;
+	}
+
+	public boolean addDirectory(Map<String, Resource> directory, boolean overwrite) {
+		boolean duplicates = false;
+		if (directory == null)
+			return false;
+
+		for (Map.Entry<String, Resource> entry : directory.entrySet()) {
+			String key = entry.getKey();
+			if (!key.endsWith(".java")) {
+				duplicates |= putResource(key, (Resource) entry.getValue(), overwrite);
+			}
+		}
+		return duplicates;
+	}
+
+	public Manifest getManifest() throws Exception {
+		if (manifest == null) {
+			Resource manifestResource = getResource("META-INF/MANIFEST.MF");
+			if (manifestResource != null) {
+				InputStream in = manifestResource.openInputStream();
+				manifest = new Manifest(in);
+				in.close();
+			}
+		}
+		return manifest;
+	}
+
+	public boolean exists(String path) {
+		return resources.containsKey(path);
+	}
+
+	public void setManifest(Manifest manifest) {
+		manifestFirst = true;
+		this.manifest = manifest;
+	}
+
+	public void write(File file) throws Exception {
+		try {
+			OutputStream out = new FileOutputStream(file);
+			write(out);
+			out.close();
+			return;
+
+		} catch (Exception t) {
+			file.delete();
+			throw t;
+		}
+	}
+
+	public void write(String file) throws Exception {
+		write(new File(file));
+	}
+
+	public void write(OutputStream out) throws Exception {
+		ZipOutputStream jout = nomanifest || doNotTouchManifest ? new ZipOutputStream(out)
+				: new JarOutputStream(out);
+		Set<String> done = new HashSet<String>();
+
+		Set<String> directories = new HashSet<String>();
+		if (doNotTouchManifest) {
+			Resource r = getResource("META-INF/MANIFEST.MF");
+			if (r != null) {
+				writeResource(jout, directories, "META-INF/MANIFEST.MF", r);
+				done.add("META-INF/MANIFEST.MF");
+			}
+		} else
+			doManifest(done, jout);
+
+		for (Map.Entry<String, Resource> entry : getResources().entrySet()) {
+			// Skip metainf contents
+			if (!done.contains(entry.getKey()))
+				writeResource(jout, directories, (String) entry.getKey(),
+						(Resource) entry.getValue());
+		}
+		jout.finish();
+	}
+
+	private void doManifest(Set<String> done, ZipOutputStream jout) throws Exception {
+		if (nomanifest)
+			return;
+
+		JarEntry ze = new JarEntry("META-INF/MANIFEST.MF");
+		jout.putNextEntry(ze);
+		writeManifest(jout);
+		jout.closeEntry();
+		done.add(ze.getName());
+	}
+
+	/**
+	 * Cleanup the manifest for writing. Cleaning up consists of adding a space
+	 * after any \n to prevent the manifest to see this newline as a delimiter.
+	 * 
+	 * @param out
+	 *            Output
+	 * @throws IOException
+	 */
+
+	public void writeManifest(OutputStream out) throws Exception {
+		writeManifest(getManifest(), out);
+	}
+
+	public static void writeManifest(Manifest manifest, OutputStream out) throws IOException {
+		if (manifest == null)
+			return;
+
+		manifest = clean(manifest);
+		outputManifest(manifest, out);
+	}
+
+	/**
+	 * Unfortunately we have to write our own manifest :-( because of a stupid
+	 * bug in the manifest code. It tries to handle UTF-8 but the way it does it
+	 * it makes the bytes platform dependent.
+	 * 
+	 * So the following code outputs the manifest.
+	 * 
+	 * A Manifest consists of
+	 * 
+	 * <pre>
+	 *   'Manifest-Version: 1.0\r\n'
+	 *   main-attributes *
+	 *   \r\n
+	 *   name-section
+	 *   
+	 *   main-attributes ::= attributes
+	 *   attributes      ::= key ': ' value '\r\n'
+	 *   name-section    ::= 'Name: ' name '\r\n' attributes
+	 * </pre>
+	 * 
+	 * Lines in the manifest should not exceed 72 bytes (! this is where the
+	 * manifest screwed up as well when 16 bit unicodes were used).
+	 * 
+	 * <p>
+	 * As a bonus, we can now sort the manifest!
+	 */
+	static byte[]	CONTINUE	=  new byte[] {'\r','\n', ' '};
+
+	/**
+	 * Main function to output a manifest properly in UTF-8.
+	 * 
+	 * @param manifest
+	 *            The manifest to output
+	 * @param out
+	 *            The output stream
+	 * @throws IOException
+	 *             when something fails
+	 */
+	public static void outputManifest(Manifest manifest, OutputStream out) throws IOException {
+		writeEntry(out, "Manifest-Version", "1.0");
+		attributes(manifest.getMainAttributes(), out);
+
+		TreeSet<String> keys = new TreeSet<String>();
+		for (Object o : manifest.getEntries().keySet())
+			keys.add(o.toString());
+
+		for (String key : keys) {
+			write(out, 0, "\r\n");
+			writeEntry(out, "Name", key);
+			attributes(manifest.getAttributes(key), out);
+		}
+	}
+
+	/**
+	 * Write out an entry, handling proper unicode and line length constraints
+	 * 
+	 */
+	private static void writeEntry(OutputStream out, String name, String value) throws IOException {
+		int n = write(out, 0, name + ": ");
+		n = write(out, n, value);
+		write(out, 0, "\r\n");
+	}
+
+	/**
+	 * Convert a string to bytes with UTF8 and then output in max 72 bytes
+	 * 
+	 * @param out
+	 *            the output string
+	 * @param i
+	 *            the current width
+	 * @param s
+	 *            the string to output
+	 * @return the new width
+	 * @throws IOException
+	 *             when something fails
+	 */
+	private static int write(OutputStream out, int i, String s) throws IOException {
+		byte[] bytes = s.getBytes("UTF8");
+		return write(out, i, bytes);
+	}
+
+	/**
+	 * Write the bytes but ensure that the line length does not exceed 72
+	 * characters. If it is more than 70 characters, we just put a cr/lf +
+	 * space.
+	 * 
+	 * @param out
+	 *            The output stream
+	 * @param width
+	 *            The nr of characters output in a line before this method
+	 *            started
+	 * @param bytes
+	 *            the bytes to output
+	 * @return the nr of characters in the last line
+	 * @throws IOException
+	 *             if something fails
+	 */
+	private static int write(OutputStream out, int width, byte[] bytes) throws IOException {
+		for (int i = 0; i < bytes.length; i++) {
+			if (width >= 72) { // we need to add the \n\r!
+				out.write(CONTINUE);
+				width = 1;
+			}
+			out.write(bytes[i]);
+			width++;
+		}
+		return width;
+	}
+
+	/**
+	 * Output an Attributes map. We will sort this map before outputing.
+	 * 
+	 * @param value
+	 *            the attrbutes
+	 * @param out
+	 *            the output stream
+	 * @throws IOException
+	 *             when something fails
+	 */
+	private static void attributes(Attributes value, OutputStream out) throws IOException {
+		TreeMap<String, String> map = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
+		for (Map.Entry<Object, Object> entry : value.entrySet()) {
+			map.put(entry.getKey().toString(), entry.getValue().toString());
+		}
+
+		map.remove("Manifest-Version"); // get rid of
+		// manifest
+		// version
+		for (Map.Entry<String, String> entry : map.entrySet()) {
+			writeEntry(out, entry.getKey(), entry.getValue());
+		}
+	}
+
+	private static Manifest clean(Manifest org) {
+
+		Manifest result = new Manifest();
+		for (Map.Entry<?, ?> entry : org.getMainAttributes().entrySet()) {
+			String nice = clean((String) entry.getValue());
+			result.getMainAttributes().put(entry.getKey(), nice);
+		}
+		for (String name : org.getEntries().keySet()) {
+			Attributes attrs = result.getAttributes(name);
+			if (attrs == null) {
+				attrs = new Attributes();
+				result.getEntries().put(name, attrs);
+			}
+
+			for (Map.Entry<?, ?> entry : org.getAttributes(name).entrySet()) {
+				String nice = clean((String) entry.getValue());
+				attrs.put((Attributes.Name) entry.getKey(), nice);
+			}
+		}
+		return result;
+	}
+
+	private static String clean(String s) {
+		if (s.indexOf('\n') < 0)
+			return s;
+
+		StringBuffer sb = new StringBuffer(s);
+		for (int i = 0; i < sb.length(); i++) {
+			if (sb.charAt(i) == '\n')
+				sb.insert(++i, ' ');
+		}
+		return sb.toString();
+	}
+
+	private void writeResource(ZipOutputStream jout, Set<String> directories, String path,
+			Resource resource) throws Exception {
+		if (resource == null)
+			return;
+
+		createDirectories(directories, jout, path);
+		ZipEntry ze = new ZipEntry(path);
+		ze.setMethod(ZipEntry.DEFLATED);
+		long lastModified = resource.lastModified();
+		if (lastModified == 0L) {
+			lastModified = System.currentTimeMillis();
+		}
+		ze.setTime(lastModified);
+		if (resource.getExtra() != null)
+			ze.setExtra(resource.getExtra().getBytes());
+		jout.putNextEntry(ze);
+		resource.write(jout);
+		jout.closeEntry();
+	}
+
+	void createDirectories(Set<String> directories, ZipOutputStream zip, String name)
+			throws IOException {
+		int index = name.lastIndexOf('/');
+		if (index > 0) {
+			String path = name.substring(0, index);
+			if (directories.contains(path))
+				return;
+			createDirectories(directories, zip, path);
+			ZipEntry ze = new ZipEntry(path + '/');
+			zip.putNextEntry(ze);
+			zip.closeEntry();
+			directories.add(path);
+		}
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Add all the resources in the given jar that match the given filter.
+	 * 
+	 * @param sub
+	 *            the jar
+	 * @param filter
+	 *            a pattern that should match the resoures in sub to be added
+	 */
+	public boolean addAll(Jar sub, Instruction filter) {
+		return addAll(sub, filter, "");
+	}
+
+	/**
+	 * Add all the resources in the given jar that match the given filter.
+	 * 
+	 * @param sub
+	 *            the jar
+	 * @param filter
+	 *            a pattern that should match the resoures in sub to be added
+	 */
+	public boolean addAll(Jar sub, Instruction filter, String destination) {
+		boolean dupl = false;
+		for (String name : sub.getResources().keySet()) {
+			if ("META-INF/MANIFEST.MF".equals(name))
+				continue;
+
+			if (filter == null || filter.matches(name) != filter.isNegated())
+				dupl |= putResource(Processor.appendPath(destination, name), sub.getResource(name),
+						true);
+		}
+		return dupl;
+	}
+
+	public void close() {
+		if (zipFile != null)
+			try {
+				zipFile.close();
+			} catch (IOException e) {
+				// Ignore
+			}
+		resources = null;
+		directories = null;
+		manifest = null;
+		source = null;
+	}
+
+	public long lastModified() {
+		return lastModified;
+	}
+
+	public void updateModified(long time, String reason) {
+		if (time > lastModified) {
+			lastModified = time;
+			lastModifiedReason = reason;
+		}
+	}
+
+	public void setReporter(Reporter reporter) {
+		this.reporter = reporter;
+	}
+
+	public boolean hasDirectory(String path) {
+		return directories.get(path) != null;
+	}
+
+	public List<String> getPackages() {
+		List<String> list = new ArrayList<String>(directories.size());
+
+		for (Map.Entry<String, Map<String, Resource>> i : directories.entrySet()) {
+			if (i.getValue() != null) {
+				String path = i.getKey();
+				String pack = path.replace('/', '.');
+				list.add(pack);
+			}
+		}
+		return list;
+	}
+
+	public File getSource() {
+		return source;
+	}
+
+	public boolean addAll(Jar src) {
+		return addAll(src, null);
+	}
+
+	public boolean rename(String oldPath, String newPath) {
+		Resource resource = remove(oldPath);
+		if (resource == null)
+			return false;
+
+		return putResource(newPath, resource);
+	}
+
+	public Resource remove(String path) {
+		Resource resource = resources.remove(path);
+		String dir = getDirectory(path);
+		Map<String, Resource> mdir = directories.get(dir);
+		// must be != null
+		mdir.remove(path);
+		return resource;
+	}
+
+	/**
+	 * Make sure nobody touches the manifest! If the bundle is signed, we do not
+	 * want anybody to touch the manifest after the digests have been
+	 * calculated.
+	 */
+	public void setDoNotTouchManifest() {
+		doNotTouchManifest = true;
+	}
+
+	/**
+	 * Calculate the checksums and set them in the manifest.
+	 */
+
+	public void calcChecksums(String algorithms[]) throws Exception {
+		if (algorithms == null)
+			algorithms = new String[] { "SHA", "MD5" };
+
+		Manifest m = getManifest();
+		if ( m == null) {
+			m= new Manifest();
+			setManifest(m);
+		}
+		
+		MessageDigest digests[] = new MessageDigest[algorithms.length];
+		int n = 0;
+		for (String algorithm : algorithms)
+			digests[n++] = MessageDigest.getInstance(algorithm);
+
+		byte buffer[] = new byte[30000];
+
+		for (Map.Entry<String, Resource> entry : resources.entrySet()) {
+			
+			// Skip the manifest
+			if (entry.getKey().equals("META-INF/MANIFEST.MF"))
+				continue;
+
+			Resource r = entry.getValue();
+			Attributes attributes = m.getAttributes(entry.getKey());
+			if (attributes == null) {
+				attributes = new Attributes();
+				getManifest().getEntries().put(entry.getKey(), attributes);
+			}
+			InputStream in = r.openInputStream();
+			try {
+				for (MessageDigest d : digests)
+					d.reset();
+				int size = in.read(buffer);
+				while (size > 0) {
+					for (MessageDigest d : digests)
+						d.update(buffer, 0, size);
+					size = in.read(buffer);
+				}
+			} finally {
+				in.close();
+			}
+			for (MessageDigest d : digests)
+				attributes.putValue(d.getAlgorithm() + "-Digest", Base64.encodeBase64(d.digest()));
+		}
+	}
+
+	Pattern	BSN	= Pattern.compile("\\s*([-\\w\\d\\._]+)\\s*;?.*");
+
+	public String getBsn() throws Exception {
+		Manifest m = getManifest();
+		if (m == null)
+			return null;
+
+		String s = m.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+		Matcher matcher = BSN.matcher(s);
+		if (matcher.matches()) {
+			return matcher.group(1);
+		}
+		return null;
+	}
+
+	public String getVersion() throws Exception {
+		Manifest m = getManifest();
+		if (m == null)
+			return null;
+
+		String s = m.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+		if (s == null)
+			return null;
+
+		return s.trim();
+	}
+
+	/**
+	 * Expand the JAR file to a directory.
+	 * 
+	 * @param dir
+	 *            the dst directory, is not required to exist
+	 * @throws Exception
+	 *             if anything does not work as expected.
+	 */
+	public void expand(File dir) throws Exception {
+		dir = dir.getAbsoluteFile();
+		dir.mkdirs();
+		if (!dir.isDirectory()) {
+			throw new IllegalArgumentException("Not a dir: " + dir.getAbsolutePath());
+		}
+
+		for (Map.Entry<String, Resource> entry : getResources().entrySet()) {
+			File f = getFile(dir, entry.getKey());
+			f.getParentFile().mkdirs();
+			copy(entry.getValue().openInputStream(), f);
+		}
+	}
+
+	/**
+	 * Make sure we have a manifest
+	 * @throws Exception
+	 */
+	public void ensureManifest() throws Exception {
+		if ( getManifest() != null)
+			return;
+		manifest = new Manifest();
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/JarResource.java b/bundleplugin/src/main/java/aQute/lib/osgi/JarResource.java
new file mode 100644
index 0000000..706094f
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/JarResource.java
@@ -0,0 +1,46 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+
+public class JarResource implements Resource {
+	Jar		jar;
+	String extra;
+	
+	public JarResource(Jar jar ) {
+		this.jar = jar;
+	}
+	
+	public String getExtra() {
+		return extra;
+	}
+
+	public long lastModified() {
+		return jar.lastModified();
+	}
+
+
+	public void write(OutputStream out) throws Exception {
+		jar.write(out);
+	}
+	
+	public InputStream openInputStream() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		write(out);
+		out.close();
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		return in;
+	}
+
+	public void setExtra(String extra) {
+		this.extra = extra;
+	}
+	
+	public Jar getJar() { 
+	    return jar;
+	}
+	
+	public String toString() {
+	    return ":" + jar.getName() + ":";
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Macro.java b/bundleplugin/src/main/java/aQute/lib/osgi/Macro.java
new file mode 100644
index 0000000..d18a2a2
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Macro.java
@@ -0,0 +1,952 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.net.*;
+import java.text.*;
+import java.util.*;
+import java.util.regex.*;
+
+import aQute.lib.io.*;
+import aQute.libg.sed.*;
+import aQute.libg.version.*;
+
+/**
+ * Provide a macro processor. This processor can replace variables in strings
+ * based on a properties and a domain. The domain can implement functions that
+ * start with a "_" and take args[], the names of these functions are available
+ * as functions in the macro processor (without the _). Macros can nest to any
+ * depth but may not contain loops.
+ * 
+ * Add POSIX macros:
+ * ${#parameter}
+    String length.
+
+${parameter%word}
+    Remove smallest suffix pattern.
+
+${parameter%%word}
+    Remove largest suffix pattern.
+
+${parameter#word}
+    Remove smallest prefix pattern.
+
+${parameter##word}
+    Remove largest prefix pattern. 
+ */
+public class Macro implements Replacer {
+	Processor	domain;
+	Object		targets[];
+	boolean		flattening;
+
+	public Macro(Processor domain, Object... targets) {
+		this.domain = domain;
+		this.targets = targets;
+		if (targets != null) {
+			for (Object o : targets) {
+				assert o != null;
+			}
+		}
+	}
+
+	public String process(String line, Processor source) {
+		return process(line, new Link(source,null,line));
+	}
+
+	String process(String line, Link link) {
+		StringBuffer sb = new StringBuffer();
+		process(line, 0, '\u0000', '\u0000', sb, link);
+		return sb.toString();
+	}
+
+	int process(CharSequence org, int index, char begin, char end, StringBuffer result, Link link) {
+		StringBuilder line = new StringBuilder(org);
+		int nesting = 1;
+
+		StringBuffer variable = new StringBuffer();
+		outer: while (index < line.length()) {
+			char c1 = line.charAt(index++);
+			if (c1 == end) {
+				if (--nesting == 0) {
+					result.append(replace(variable.toString(), link));
+					return index;
+				}
+			} else if (c1 == begin)
+				nesting++;
+			else if (c1 == '\\' && index < line.length() - 1 && line.charAt(index) == '$') {
+				// remove the escape backslash and interpret the dollar as a
+				// literal
+				index++;
+				variable.append('$');
+				continue outer;
+			} else if (c1 == '$' && index < line.length() - 2) {
+				char c2 = line.charAt(index);
+				char terminator = getTerminator(c2);
+				if (terminator != 0) {
+					index = process(line, index + 1, c2, terminator, variable, link);
+					continue outer;
+				}
+			}
+			variable.append(c1);
+		}
+		result.append(variable);
+		return index;
+	}
+
+	public static char getTerminator(char c) {
+		switch (c) {
+		case '(':
+			return ')';
+		case '[':
+			return ']';
+		case '{':
+			return '}';
+		case '<':
+			return '>';
+		case '\u00ab': // Guillemet double << >>
+			return '\u00bb';
+		case '\u2039': // Guillemet single
+			return '\u203a';
+		}
+		return 0;
+	}
+
+	protected String replace(String key, Link link) {
+		if (link != null && link.contains(key))
+			return "${infinite:" + link.toString() + "}";
+
+		if (key != null) {
+			key = key.trim();
+			if (key.length() > 0) {
+				Processor source = domain;
+				String value = null;
+				while( source != null) {
+					value = source.getProperties().getProperty(key);
+					if ( value != null )
+						break;
+					
+					source = source.getParent();
+				}
+				
+				if (value != null)
+					return process(value, new Link(source,link, key));
+
+				value = doCommands(key, link);
+				if (value != null)
+					return process(value, new Link(source, link, key));
+
+				if (key != null && key.trim().length() > 0) {
+					value = System.getProperty(key);
+					if (value != null)
+						return value;
+				}
+				if (!flattening)
+					domain.warning("No translation found for macro: " + key);
+			} else {
+				domain.warning("Found empty macro key");
+			}
+		} else {
+			domain.warning("Found null macro key");
+		}
+		return "${" + key + "}";
+	}
+
+	/**
+	 * Parse the key as a command. A command consist of parameters separated by
+	 * ':'.
+	 * 
+	 * @param key
+	 * @return
+	 */
+	static Pattern	commands	= Pattern.compile("(?<!\\\\);");
+
+	private String doCommands(String key, Link source) {
+		String[] args = commands.split(key);
+		if (args == null || args.length == 0)
+			return null;
+
+		for (int i = 0; i < args.length; i++)
+			if (args[i].indexOf('\\') >= 0)
+				args[i] = args[i].replaceAll("\\\\;", ";");
+
+		
+		if ( args[0].startsWith("^")) {
+			String varname = args[0].substring(1).trim();
+			
+			Processor parent = source.start.getParent();
+			if ( parent != null)
+				return parent.getProperty(varname);
+			else
+				return null;
+		}
+		
+		
+		Processor rover = domain;
+		while (rover != null) {
+			String result = doCommand(rover, args[0], args);
+			if (result != null)
+				return result;
+
+			rover = rover.getParent();
+		}
+
+		for (int i = 0; targets != null && i < targets.length; i++) {
+			String result = doCommand(targets[i], args[0], args);
+			if (result != null)
+				return result;
+		}
+
+		return doCommand(this, args[0], args);
+	}
+
+	private String doCommand(Object target, String method, String[] args) {
+		if (target == null)
+			; // System.out.println("Huh? Target should never be null " +
+		// domain);
+		else {
+			String cname = "_" + method.replaceAll("-", "_");
+			try {
+				Method m = target.getClass().getMethod(cname, new Class[] { String[].class });
+				return (String) m.invoke(target, new Object[] { args });
+			} catch (NoSuchMethodException e) {
+				// Ignore
+			} catch (InvocationTargetException e) {
+				if ( e.getCause() instanceof IllegalArgumentException ) {
+					domain.error("%s, for cmd: %s, arguments; %s", e.getMessage(), method, Arrays.toString(args));
+				} else {
+					domain.warning("Exception in replace: " + e.getCause());
+					e.getCause().printStackTrace();
+				}
+			} catch (Exception e) {
+				domain.warning("Exception in replace: " + e + " method=" + method);
+				e.printStackTrace();
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Return a unique list where the duplicates are removed.
+	 * 
+	 * @param args
+	 * @return
+	 */
+	static String	_uniqHelp	= "${uniq;<list> ...}";
+
+	public String _uniq(String args[]) {
+		verifyCommand(args, _uniqHelp, null, 1, Integer.MAX_VALUE);
+		Set<String> set = new LinkedHashSet<String>();
+		for (int i = 1; i < args.length; i++) {
+			Processor.split(args[i], set);
+		}
+		return Processor.join(set, ",");
+	}
+
+	public String _pathseparator(String args[]) {
+		return File.pathSeparator;
+	}
+
+	public String _separator(String args[]) {
+		return File.separator;
+	}
+
+	public String _filter(String args[]) {
+		return filter(args, false);
+	}
+
+	public String _filterout(String args[]) {
+		return filter(args, true);
+
+	}
+
+	static String	_filterHelp	= "${%s;<list>;<regex>}";
+
+	String filter(String[] args, boolean include) {
+		verifyCommand(args, String.format(_filterHelp, args[0]), null, 3, 3);
+
+		Collection<String> list = new ArrayList<String>(Processor.split(args[1]));
+		Pattern pattern = Pattern.compile(args[2]);
+
+		for (Iterator<String> i = list.iterator(); i.hasNext();) {
+			if (pattern.matcher(i.next()).matches() == include)
+				i.remove();
+		}
+		return Processor.join(list);
+	}
+
+	static String	_sortHelp	= "${sort;<list>...}";
+
+	public String _sort(String args[]) {
+		verifyCommand(args, _sortHelp, null, 2, Integer.MAX_VALUE);
+
+		List<String> result = new ArrayList<String>();
+		for (int i = 1; i < args.length; i++) {
+			Processor.split(args[i], result);
+		}
+		Collections.sort(result);
+		return Processor.join(result);
+	}
+
+	static String	_joinHelp	= "${join;<list>...}";
+
+	public String _join(String args[]) {
+
+		verifyCommand(args, _joinHelp, null, 1, Integer.MAX_VALUE);
+
+		List<String> result = new ArrayList<String>();
+		for (int i = 1; i < args.length; i++) {
+			Processor.split(args[i], result);
+		}
+		return Processor.join(result);
+	}
+
+	static String	_ifHelp	= "${if;<condition>;<iftrue> [;<iffalse>] }";
+
+	public String _if(String args[]) {
+		verifyCommand(args, _ifHelp, null, 3, 4);
+		String condition = args[1].trim();
+		if (condition.length() != 0)
+			return args[2];
+		if (args.length > 3)
+			return args[3];
+		else
+			return "";
+	}
+
+	public String _now(String args[]) {
+		return new Date().toString();
+	}
+
+	public static String	_fmodifiedHelp	= "${fmodified;<list of filenames>...}, return latest modification date";
+
+	public String _fmodified(String args[]) throws Exception {
+		verifyCommand(args, _fmodifiedHelp, null, 2, Integer.MAX_VALUE);
+
+		long time = 0;
+		Collection<String> names = new ArrayList<String>();
+		for (int i = 1; i < args.length; i++) {
+			Processor.split(args[i], names);
+		}
+		for (String name : names) {
+			File f = new File(name);
+			if (f.exists() && f.lastModified() > time)
+				time = f.lastModified();
+		}
+		return "" + time;
+	}
+
+	public String _long2date(String args[]) {
+		try {
+			return new Date(Long.parseLong(args[1])).toString();
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return "not a valid long";
+	}
+
+	public String _literal(String args[]) {
+		if (args.length != 2)
+			throw new RuntimeException("Need a value for the ${literal;<value>} macro");
+		return "${" + args[1] + "}";
+	}
+
+	public String _def(String args[]) {
+		if (args.length != 2)
+			throw new RuntimeException("Need a value for the ${def;<value>} macro");
+
+		return domain.getProperty(args[1], "");
+	}
+
+	/**
+	 * 
+	 * replace ; <list> ; regex ; replace
+	 * 
+	 * @param args
+	 * @return
+	 */
+	public String _replace(String args[]) {
+		if (args.length != 4) {
+			domain.warning("Invalid nr of arguments to replace " + Arrays.asList(args));
+			return null;
+		}
+
+		String list[] = args[1].split("\\s*,\\s*");
+		StringBuffer sb = new StringBuffer();
+		String del = "";
+		for (int i = 0; i < list.length; i++) {
+			String element = list[i].trim();
+			if (!element.equals("")) {
+				sb.append(del);
+				sb.append(element.replaceAll(args[2], args[3]));
+				del = ", ";
+			}
+		}
+
+		return sb.toString();
+	}
+
+	public String _warning(String args[]) {
+		for (int i = 1; i < args.length; i++) {
+			domain.warning(process(args[i]));
+		}
+		return "";
+	}
+
+	public String _error(String args[]) {
+		for (int i = 1; i < args.length; i++) {
+			domain.error(process(args[i]));
+		}
+		return "";
+	}
+
+	/**
+	 * toclassname ; <path>.class ( , <path>.class ) *
+	 * 
+	 * @param args
+	 * @return
+	 */
+	static String	_toclassnameHelp	= "${classname;<list of class names>}, convert class paths to FQN class names ";
+
+	public String _toclassname(String args[]) {
+		verifyCommand(args, _toclassnameHelp, null, 2, 2);
+		Collection<String> paths = Processor.split(args[1]);
+
+		List<String> names = new ArrayList<String>(paths.size());
+		for (String path : paths) {
+			if (path.endsWith(".class")) {
+				String name = path.substring(0, path.length() - 6).replace('/', '.');
+				names.add(name);
+			} else if (path.endsWith(".java")) {
+				String name = path.substring(0, path.length() - 5).replace('/', '.');
+				names.add(name);
+			} else {
+				domain.warning("in toclassname, " + args[1]
+						+ " is not a class path because it does not end in .class");
+			}
+		}
+		return Processor.join(names, ",");
+	}
+
+	/**
+	 * toclassname ; <path>.class ( , <path>.class ) *
+	 * 
+	 * @param args
+	 * @return
+	 */
+
+	static String	_toclasspathHelp	= "${toclasspath;<list>[;boolean]}, convert a list of class names to paths";
+
+	public String _toclasspath(String args[]) {
+		verifyCommand(args, _toclasspathHelp, null, 2, 3);
+		boolean cl = true;
+		if (args.length > 2)
+			cl = new Boolean(args[2]);
+
+		Collection<String> names = Processor.split(args[1]);
+		Collection<String> paths = new ArrayList<String>(names.size());
+		for (String name : names) {
+			String path = name.replace('.', '/') + (cl ? ".class" : "");
+			paths.add(path);
+		}
+		return Processor.join(paths, ",");
+	}
+
+	public String _dir(String args[]) {
+		if (args.length < 2) {
+			domain.warning("Need at least one file name for ${dir;...}");
+			return null;
+		} else {
+			String del = "";
+			StringBuffer sb = new StringBuffer();
+			for (int i = 1; i < args.length; i++) {
+				File f = domain.getFile(args[i]);
+				if (f.exists() && f.getParentFile().exists()) {
+					sb.append(del);
+					sb.append(f.getParentFile().getAbsolutePath());
+					del = ",";
+				}
+			}
+			return sb.toString();
+		}
+
+	}
+
+	public String _basename(String args[]) {
+		if (args.length < 2) {
+			domain.warning("Need at least one file name for ${basename;...}");
+			return null;
+		} else {
+			String del = "";
+			StringBuffer sb = new StringBuffer();
+			for (int i = 1; i < args.length; i++) {
+				File f = domain.getFile(args[i]);
+				if (f.exists() && f.getParentFile().exists()) {
+					sb.append(del);
+					sb.append(f.getName());
+					del = ",";
+				}
+			}
+			return sb.toString();
+		}
+
+	}
+
+	public String _isfile(String args[]) {
+		if (args.length < 2) {
+			domain.warning("Need at least one file name for ${isfile;...}");
+			return null;
+		} else {
+			boolean isfile = true;
+			for (int i = 1; i < args.length; i++) {
+				File f = new File(args[i]).getAbsoluteFile();
+				isfile &= f.isFile();
+			}
+			return isfile ? "true" : "false";
+		}
+
+	}
+
+	public String _isdir(String args[]) {
+		if (args.length < 2) {
+			domain.warning("Need at least one file name for ${isdir;...}");
+			return null;
+		} else {
+			boolean isdir = true;
+			for (int i = 1; i < args.length; i++) {
+				File f = new File(args[i]).getAbsoluteFile();
+				isdir &= f.isDirectory();
+			}
+			return isdir ? "true" : "false";
+		}
+
+	}
+
+	public String _tstamp(String args[]) {
+		String format = "yyyyMMddHHmm";
+		long now = System.currentTimeMillis();
+
+		if (args.length > 1) {
+			format = args[1];
+			if (args.length > 2) {
+				now = Long.parseLong(args[2]);
+				if (args.length > 3) {
+					domain.warning("Too many arguments for tstamp: " + Arrays.toString(args));
+				}
+			}
+		}
+		SimpleDateFormat sdf = new SimpleDateFormat(format);
+		return sdf.format(new Date(now));
+	}
+
+	/**
+	 * Wildcard a directory. The lists can contain Instruction that are matched
+	 * against the given directory
+	 * 
+	 * ${lsr;<dir>;<list>(;<list>)*} ${lsa;<dir>;<list>(;<list>)*}
+	 * 
+	 * @author aqute
+	 * 
+	 */
+
+	public String _lsr(String args[]) {
+		return ls(args, true);
+	}
+
+	public String _lsa(String args[]) {
+		return ls(args, false);
+	}
+
+	String ls(String args[], boolean relative) {
+		if (args.length < 2)
+			throw new IllegalArgumentException(
+					"the ${ls} macro must at least have a directory as parameter");
+
+		File dir = domain.getFile(args[1]);
+		if (!dir.isAbsolute())
+			throw new IllegalArgumentException(
+					"the ${ls} macro directory parameter is not absolute: " + dir);
+
+		if (!dir.exists())
+			throw new IllegalArgumentException(
+					"the ${ls} macro directory parameter does not exist: " + dir);
+
+		if (!dir.isDirectory())
+			throw new IllegalArgumentException(
+					"the ${ls} macro directory parameter points to a file instead of a directory: "
+							+ dir);
+
+		String[] files = dir.list();
+		List<String> result;
+
+		if (args.length < 3) {
+			result = Arrays.asList(files);
+		} else
+			result = new ArrayList<String>();
+
+		for (int i = 2; i < args.length; i++) {
+			String parts[] = args[i].split("\\s*,\\s*");
+			for (String pattern : parts) {
+				// So make it in to an instruction
+				Instruction instr = Instruction.getPattern(pattern);
+
+				// For each project, match it against the instruction
+				for (int f = 0; f < files.length; f++) {
+					if (files[f] != null) {
+						if (instr.matches(files[f])) {
+							if (!instr.isNegated()) {
+								if (relative)
+									result.add(files[f]);
+								else
+									result.add(new File(dir, files[f]).getAbsolutePath());
+							}
+							files[f] = null;
+						}
+					}
+				}
+			}
+		}
+		return Processor.join(result, ",");
+	}
+
+	public String _currenttime(String args[]) {
+		return Long.toString(System.currentTimeMillis());
+	}
+
+	/**
+	 * Modify a version to set a version policy. Thed policy is a mask that is
+	 * mapped to a version.
+	 * 
+	 * <pre>
+	 * +           increment
+	 * -           decrement
+	 * =           maintain
+	 * &tilde;           discard
+	 * 
+	 * ==+      = maintain major, minor, increment micro, discard qualifier
+	 * &tilde;&tilde;&tilde;=     = just get the qualifier
+	 * version=&quot;[${version;==;${@}},${version;=+;${@}})&quot;
+	 * </pre>
+	 * 
+	 * 
+	 * 
+	 * 
+	 * @param args
+	 * @return
+	 */
+	final static String		MASK_STRING			= "[\\-+=~0123456789]{0,3}[=~]?";
+	final static Pattern	MASK				= Pattern.compile(MASK_STRING);
+	final static String		_versionHelp		= "${version;<mask>;<version>}, modify a version\n"
+														+ "<mask> ::= [ M [ M [ M [ MQ ]]]\n"
+														+ "M ::= '+' | '-' | MQ\n"
+														+ "MQ ::= '~' | '='";
+	final static Pattern	_versionPattern[]	= new Pattern[] { null, null, MASK,
+			Verifier.VERSION					};
+
+	public String _version(String args[]) {
+		verifyCommand(args, _versionHelp, null, 2, 3);
+
+		String mask = args[1];
+
+		Version version = null;
+		if (args.length >= 3)
+			version = new Version(args[2]);
+
+		return version(version, mask);
+	}
+
+	String version(Version version, String mask) {
+		if (version == null) {
+			String v = domain.getProperty("@");
+			if (v == null) {
+				domain
+						.error(
+								"No version specified for ${version} or ${range} and no implicit version ${@} either, mask=%s",
+								mask);
+				v = "0";
+			}
+			version = new Version(v);
+		}
+
+		StringBuilder sb = new StringBuilder();
+		String del = "";
+
+		for (int i = 0; i < mask.length(); i++) {
+			char c = mask.charAt(i);
+			String result = null;
+			if (c != '~') {
+				if (i == 3) {
+					result = version.getQualifier();
+				} else if (Character.isDigit(c)) {
+					// Handle masks like +00, =+0
+					result = String.valueOf(c);
+				} else {
+					int x = version.get(i);
+					switch (c) {
+					case '+':
+						x++;
+						break;
+					case '-':
+						x--;
+						break;
+					case '=':
+						break;
+					}
+					result = Integer.toString(x);
+				}
+				if (result != null) {
+					sb.append(del);
+					del = ".";
+					sb.append(result);
+				}
+			}
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * Schortcut for version policy
+	 * 
+	 * <pre>
+	 * -provide-policy : ${policy;[==,=+)}
+	 * -consume-policy : ${policy;[==,+)}
+	 * </pre>
+	 * 
+	 * @param args
+	 * @return
+	 */
+
+	static Pattern	RANGE_MASK		= Pattern.compile("(\\[|\\()(" + MASK_STRING + "),(" + MASK_STRING +")(\\]|\\))");
+	static String	_rangeHelp		= "${range;<mask>[;<version>]}, range for version, if version not specified lookyp ${@}\n"
+											+ "<mask> ::= [ M [ M [ M [ MQ ]]]\n"
+											+ "M ::= '+' | '-' | MQ\n" + "MQ ::= '~' | '='";
+	static Pattern	_rangePattern[]	= new Pattern[] { null, RANGE_MASK };
+
+	public String _range(String args[]) {
+		verifyCommand(args, _rangeHelp, _rangePattern, 2, 3);
+		Version version = null;
+		if (args.length >= 3)
+			version = new Version(args[2]);
+
+		String spec = args[1];
+
+		Matcher m = RANGE_MASK.matcher(spec);
+		m.matches();
+		String floor = m.group(1);
+		String floorMask = m.group(2);
+		String ceilingMask = m.group(3);
+		String ceiling = m.group(4);
+
+		StringBuilder sb = new StringBuilder();
+		sb.append(floor);
+		sb.append(version(version, floorMask));
+		sb.append(",");
+		sb.append(version(version, ceilingMask));
+		sb.append(ceiling);
+
+		String s = sb.toString();
+		VersionRange vr = new VersionRange(s);
+		if (!(vr.includes(vr.getHigh()) || vr.includes(vr.getLow()))) {
+			domain.error("${range} macro created an invalid range %s from %s and mask %s", s,
+					version, spec);
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * System command. Execute a command and insert the result.
+	 * 
+	 * @param args
+	 * @param help
+	 * @param patterns
+	 * @param low
+	 * @param high
+	 */
+	public String _system(String args[]) throws Exception {
+		verifyCommand(args, "${system;<command>[;<in>]}, execute a system command", null, 2, 3);
+		String command = args[1];
+		String input = null;
+
+		if (args.length > 2) {
+			input = args[2];
+		}
+
+		Process process = Runtime.getRuntime().exec(command, null, domain.getBase());
+		if (input != null) {
+			process.getOutputStream().write(input.getBytes("UTF-8"));
+		}
+		process.getOutputStream().close();
+
+		String s = IO.collect(process.getInputStream(), "UTF-8");
+		int exitValue = process.waitFor();
+		if (exitValue != 0) {
+			domain.error("System command " + command + " failed with " + exitValue);
+		}
+		return s.trim();
+	}
+
+	public String _env(String args[]) {
+		verifyCommand(args, "${env;<name>}, get the environmet variable", null, 2, 2);
+
+		try {
+			return System.getenv(args[1]);
+		} catch (Throwable t) {
+			return null;
+		}
+	}
+
+	/**
+	 * Get the contents of a file.
+	 * 
+	 * @param in
+	 * @return
+	 * @throws IOException
+	 */
+
+	public String _cat(String args[]) throws IOException {
+		verifyCommand(args, "${cat;<in>}, get the content of a file", null, 2, 2);
+		File f = domain.getFile(args[1]);
+		if (f.isFile()) {
+			return IO.collect(f);
+		} else if (f.isDirectory()) {
+			return Arrays.toString(f.list());
+		} else {
+			try {
+				URL url = new URL(args[1]);
+				return IO.collect(url, "UTF-8");
+			} catch (MalformedURLException mfue) {
+				// Ignore here
+			}
+			return null;
+		}
+	}
+
+	public static void verifyCommand(String args[], String help, Pattern[] patterns, int low,
+			int high) {
+		String message = "";
+		if (args.length > high) {
+			message = "too many arguments";
+		} else if (args.length < low) {
+			message = "too few arguments";
+		} else {
+			for (int i = 0; patterns != null && i < patterns.length && i < args.length; i++) {
+				if (patterns[i] != null) {
+					Matcher m = patterns[i].matcher(args[i]);
+					if (!m.matches())
+						message += String.format("Argument %s (%s) does not match %s\n", i,
+								args[i], patterns[i].pattern());
+				}
+			}
+		}
+		if (message.length() != 0) {
+			StringBuilder sb = new StringBuilder();
+			String del = "${";
+			for (String arg : args) {
+				sb.append(del);
+				sb.append(arg);
+				del = ";";
+			}
+			sb.append("}, is not understood. ");
+			sb.append(message);
+			throw new IllegalArgumentException(sb.toString());
+		}
+	}
+
+	// Helper class to track expansion of variables
+	// on the stack.
+	static class Link {
+		Link	previous;
+		String	key;
+		Processor start;
+
+		public Link(Processor start, Link previous, String key) {
+			this.previous = previous;
+			this.key = key;
+			this.start = start;
+		}
+
+		public boolean contains(String key) {
+			if (this.key.equals(key))
+				return true;
+
+			if (previous == null)
+				return false;
+
+			return previous.contains(key);
+		}
+
+		public String toString() {
+			StringBuffer sb = new StringBuffer();
+			String del = "[";
+			for (Link r = this; r != null; r = r.previous) {
+				sb.append(del);
+				sb.append(r.key);
+				del = ",";
+			}
+			sb.append("]");
+			return sb.toString();
+		}
+	}
+
+	/**
+	 * Take all the properties and translate them to actual values. This method
+	 * takes the set properties and traverse them over all entries, including
+	 * the default properties for that properties. The values no longer contain
+	 * macros.
+	 * 
+	 * @return A new Properties with the flattened values
+	 */
+	public Properties getFlattenedProperties() {
+		// Some macros only work in a lower processor, so we
+		// do not report unknown macros while flattening
+		flattening = true;
+		try {
+			Properties flattened = new Properties();
+			Properties source = domain.getProperties();
+			for (Enumeration<?> e = source.propertyNames(); e.hasMoreElements();) {
+				String key = (String) e.nextElement();
+				if (!key.startsWith("_"))
+					if (key.startsWith("-"))
+						flattened.put(key, source.getProperty(key));
+					else
+						flattened.put(key, process(source.getProperty(key)));
+			}
+			return flattened;
+		} finally {
+			flattening = false;
+		}
+	};
+
+	public static String	_fileHelp	= "${file;<base>;<paths>...}, create correct OS dependent path";
+
+	public String _osfile(String args[]) {
+		verifyCommand(args, _fileHelp, null, 3, 3);
+		File base = new File(args[1]);
+		File f = Processor.getFile(base, args[2]);
+		return f.getAbsolutePath();
+	}
+
+	public String _path(String args[]) {
+		List<String> list = new ArrayList<String>();
+		for (int i = 1; i < args.length; i++) {
+			list.addAll(Processor.split(args[i]));
+		}
+		return Processor.join(list, File.pathSeparator);
+	}
+
+	public static Properties getParent(Properties p) {
+		try {
+			Field f = Properties.class.getDeclaredField("defaults");
+			f.setAccessible(true);
+			return (Properties) f.get(p);
+		} catch (Exception e) {
+			Field[] fields = Properties.class.getFields();
+			System.out.println(Arrays.toString(fields));
+			return null;
+		}
+	}
+
+	public String process(String line) {
+		return process(line,domain);
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/OpCodes.java b/bundleplugin/src/main/java/aQute/lib/osgi/OpCodes.java
new file mode 100644
index 0000000..f0d3134
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/OpCodes.java
@@ -0,0 +1,1196 @@
+package aQute.lib.osgi;
+
+public class OpCodes {
+	final static short	nop				= 0x00;			// [No change] performs
+														// no
+	// operation
+	final static short	aconst_null		= 0x01;			// ? null pushes a null
+	// reference onto the stack
+	final static short	iconst_m1		= 0x02;			// ? -1 loads the int
+														// value -1
+	// onto the stack
+	final static short	iconst_0		= 0x03;			// ? 0 loads the int
+														// value 0
+	// onto the stack
+	final static short	iconst_1		= 0x04;			// ? 1 loads the int
+														// value 1
+	// onto the stack
+	final static short	iconst_2		= 0x05;			// ? 2 loads the int
+														// value 2
+	// onto the stack
+	final static short	iconst_3		= 0x06;			// ? 3 loads the int
+														// value 3
+	// onto the stack
+	final static short	iconst_4		= 0x07;			// ? 4 loads the int
+														// value 4
+	// onto the stack
+	final static short	iconst_5		= 0x08;			// ? 5 loads the int
+														// value 5
+	// onto the stack
+	final static short	lconst_0		= 0x09;			// ? 0L pushes the long
+														// 0 onto
+	// the stack
+	final static short	bipush			= 0x10;			// byte ? value pushes a
+														// byte
+	// onto the stack as an integer
+	// value
+	final static short	sipush			= 0x11;			// byte1, byte2 ? value
+														// pushes a
+	// signed integer (byte1 << 8 +
+	// byte2) onto the stack
+	final static short	ldc				= 0x12;			// index ? value pushes
+														// a
+	// constant #index from a
+	// constant pool (String, int,
+	// float or class type) onto the
+	// stack
+	final static short	ldc_w			= 0x13;			// indexbyte1,
+														// indexbyte2 ?
+	// value pushes a constant
+	// #index from a constant pool
+	// (String, int, float or class
+	// type) onto the stack (wide
+	// index is constructed as
+	// indexbyte1 << 8 + indexbyte2)
+	final static short	ldc2_w			= 0x14;			// indexbyte1,
+														// indexbyte2 ?
+	// value pushes a constant
+	// #index from a constant pool
+	// (double or long) onto the
+	// stack (wide index is
+	// constructed as indexbyte1 <<
+	// 8 + indexbyte2)
+	final static short	iload			= 0x15;			// index ? value loads
+														// an int
+	// value from a variable #index
+	final static short	lload			= 0x16;			// index ? value load a
+														// long
+	// value from a local variable
+	// #index
+	final static short	fload			= 0x17;			// index ? value loads a
+														// float
+	// value from a local variable
+	// #index
+	final static short	dload			= 0x18;			// index ? value loads a
+														// double
+	// value from a local variable
+	// #index
+	final static short	aload			= 0x19;			// index ? objectref
+														// loads a
+	// reference onto the stack from
+	// a local variable #index
+	final static short	lload_2			= 0x20;			// ? value load a long
+														// value
+	// from a local variable 2
+	final static short	lload_3			= 0x21;			// ? value load a long
+														// value
+	// from a local variable 3
+	final static short	fload_0			= 0x22;			// ? value loads a float
+														// value
+	// from local variable 0
+	final static short	fload_1			= 0x23;			// ? value loads a float
+														// value
+	// from local variable 1
+	final static short	fload_2			= 0x24;			// ? value loads a float
+														// value
+	// from local variable 2
+	final static short	fload_3			= 0x25;			// ? value loads a float
+														// value
+	// from local variable 3
+	final static short	dload_0			= 0x26;			// ? value loads a
+														// double from
+	// local variable 0
+	final static short	dload_1			= 0x27;			// ? value loads a
+														// double from
+	// local variable 1
+	final static short	dload_2			= 0x28;			// ? value loads a
+														// double from
+	// local variable 2
+	final static short	dload_3			= 0x29;			// ? value loads a
+														// double from
+	// local variable 3
+	final static short	faload			= 0x30;			// arrayref, index ?
+														// value loads
+	// a float from an array
+	final static short	daload			= 0x31;			// arrayref, index ?
+														// value loads
+	// a double from an array
+	final static short	aaload			= 0x32;			// arrayref, index ?
+														// value loads
+	// onto the stack a reference
+	// from an array
+	final static short	baload			= 0x33;			// arrayref, index ?
+														// value loads
+	// a byte or Boolean value from
+	// an array
+	final static short	caload			= 0x34;			// arrayref, index ?
+														// value loads
+	// a char from an array
+	final static short	saload			= 0x35;			// arrayref, index ?
+														// value load
+	// short from array
+	final static short	istore			= 0x36;			// index value ? store
+														// int value
+	// into variable #index
+	final static short	lstore			= 0x37;			// index value ? store a
+														// long
+	// value in a local variable
+	// #index
+	final static short	fstore			= 0x38;			// index value ? stores
+														// a float
+	// value into a local variable
+	// #index
+	final static short	dstore			= 0x39;			// index value ? stores
+														// a double
+	// value into a local variable
+	// #index
+	final static short	lstore_1		= 0x40;			// value ? store a long
+														// value in
+	// a local variable 1
+	final static short	lstore_2		= 0x41;			// value ? store a long
+														// value in
+	// a local variable 2
+	final static short	lstore_3		= 0x42;			// value ? store a long
+														// value in
+	// a local variable 3
+	final static short	fstore_0		= 0x43;			// value ? stores a
+														// float value
+	// into local variable 0
+	final static short	fstore_1		= 0x44;			// value ? stores a
+														// float value
+	// into local variable 1
+	final static short	fstore_2		= 0x45;			// value ? stores a
+														// float value
+	// into local variable 2
+	final static short	fstore_3		= 0x46;			// value ? stores a
+														// float value
+	// into local variable 3
+	final static short	dstore_0		= 0x47;			// value ? stores a
+														// double into
+	// local variable 0
+	final static short	dstore_1		= 0x48;			// value ? stores a
+														// double into
+	// local variable 1
+	final static short	dstore_2		= 0x49;			// value ? stores a
+														// double into
+	// local variable 2
+	final static short	lastore			= 0x50;			// arrayref, index,
+														// value ?
+	// store a long to an array
+	final static short	fastore			= 0x51;			// arreyref, index,
+														// value ?
+	// stores a float in an array
+	final static short	dastore			= 0x52;			// arrayref, index,
+														// value ?
+	// stores a double into an array
+	final static short	aastore			= 0x53;			// arrayref, index,
+														// value ?
+	// stores into a reference to an
+	// array
+	final static short	bastore			= 0x54;			// arrayref, index,
+														// value ?
+	// stores a byte or Boolean
+	// value into an array
+	final static short	castore			= 0x55;			// arrayref, index,
+														// value ?
+	// stores a char into an array
+	final static short	sastore			= 0x56;			// arrayref, index,
+														// value ?
+	// store short to array
+	final static short	pop				= 0x57;			// value ? discards the
+														// top
+	// value on the stack
+	final static short	pop2			= 0x58;			// {value2, value1} ?
+														// discards
+	// the top two values on the
+	// stack (or one value, if it is
+	// a double or long)
+	final static short	dup				= 0x59;			// value ? value, value
+	// duplicates the value on top
+	// of the stack
+	final static short	iadd			= 0x60;			// value1, value2 ?
+														// result adds
+	// two ints together
+	final static short	ladd			= 0x61;			// value1, value2 ?
+														// result add
+	// two longs
+	final static short	fadd			= 0x62;			// value1, value2 ?
+														// result adds
+	// two floats
+	final static short	dadd			= 0x63;			// value1, value2 ?
+														// result adds
+	// two doubles
+	final static short	isub			= 0x64;			// value1, value2 ?
+														// result int
+	// subtract
+	final static short	lsub			= 0x65;			// value1, value2 ?
+														// result
+	// subtract two longs
+	final static short	fsub			= 0x66;			// value1, value2 ?
+														// result
+	// subtracts two floats
+	final static short	dsub			= 0x67;			// value1, value2 ?
+														// result
+	// subtracts a double from
+	// another
+	final static short	imul			= 0x68;			// value1, value2 ?
+														// result
+	// multiply two integers
+	final static short	lmul			= 0x69;			// value1, value2 ?
+														// result
+	// multiplies two longs
+	final static short	irem			= 0x70;			// value1, value2 ?
+														// result
+	// logical int remainder
+	final static short	lrem			= 0x71;			// value1, value2 ?
+														// result
+	// remainder of division of two
+	// longs
+	final static short	frem			= 0x72;			// value1, value2 ?
+														// result gets
+	// the remainder from a division
+	// between two floats
+	final static short	drem			= 0x73;			// value1, value2 ?
+														// result gets
+	// the remainder from a division
+	// between two doubles
+	final static short	ineg			= 0x74;			// value ? result negate
+														// int
+	final static short	lneg			= 0x75;			// value ? result
+														// negates a long
+	final static short	fneg			= 0x76;			// value ? result
+														// negates a
+	// float
+	final static short	dneg			= 0x77;			// value ? result
+														// negates a
+	// double
+	final static short	ishl			= 0x78;			// value1, value2 ?
+														// result int
+	// shift left
+	final static short	lshl			= 0x79;			// value1, value2 ?
+														// result
+	// bitwise shift left of a long
+	// value1 by value2 positions
+	final static short	ior				= 0x80;			// value1, value2 ?
+														// result
+	// logical int or
+	final static short	lor				= 0x81;			// value1, value2 ?
+														// result
+	// bitwise or of two longs
+	final static short	ixor			= 0x82;			// value1, value2 ?
+														// result int
+	// xor
+	final static short	lxor			= 0x83;			// value1, value2 ?
+														// result
+	// bitwise exclusive or of two
+	// longs
+	final static short	iinc			= 0x84;			// index, const [No
+														// change]
+	// increment local variable
+	// #index by signed byte const
+	final static short	i2l				= 0x85;			// value ? result
+														// converts an
+	// int into a long
+	final static short	i2f				= 0x86;			// value ? result
+														// converts an
+	// int into a float
+	final static short	i2d				= 0x87;			// value ? result
+														// converts an
+	// int into a double
+	final static short	l2i				= 0x88;			// value ? result
+														// converts a
+	// long to an int
+	final static short	l2f				= 0x89;			// value ? result
+														// converts a
+	// long to a float
+	final static short	d2f				= 0x90;			// value ? result
+														// converts a
+	// double to a float
+	final static short	i2b				= 0x91;			// value ? result
+														// converts an
+	// int into a byte
+	final static short	i2c				= 0x92;			// value ? result
+														// converts an
+	// int into a character
+	final static short	i2s				= 0x93;			// value ? result
+														// converts an
+	// int into a short
+	final static short	lcmp			= 0x94;			// value1, value2 ?
+														// result
+	// compares two longs values
+	final static short	fcmpl			= 0x95;			// value1, value2 ?
+														// result
+	// compares two floats
+	final static short	fcmpg			= 0x96;			// value1, value2 ?
+														// result
+	// compares two floats
+	final static short	dcmpl			= 0x97;			// value1, value2 ?
+														// result
+	// compares two doubles
+	final static short	dcmpg			= 0x98;			// value1, value2 ?
+														// result
+	// compares two doubles
+	final static short	ifeq			= 0x99;			// branchbyte1,
+														// branchbyte2
+	// value ? if value is 0, branch
+	// to instruction at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	lconst_1		= 0x0a;			// ? 1L pushes the long
+														// 1 onto
+	// the stack
+	final static short	fconst_0		= 0x0b;			// ? 0.0f pushes 0.0f on
+														// the
+	// stack
+	final static short	fconst_1		= 0x0c;			// ? 1.0f pushes 1.0f on
+														// the
+	// stack
+	final static short	fconst_2		= 0x0d;			// ? 2.0f pushes 2.0f on
+														// the
+	// stack
+	final static short	dconst_0		= 0x0e;			// ? 0.0 pushes the
+														// constant 0.0
+	// onto the stack
+	final static short	dconst_1		= 0x0f;			// ? 1.0 pushes the
+														// constant 1.0
+	// onto the stack
+	final static short	iload_0			= 0x1a;			// ? value loads an int
+														// value
+	// from variable 0
+	final static short	iload_1			= 0x1b;			// ? value loads an int
+														// value
+	// from variable 1
+	final static short	iload_2			= 0x1c;			// ? value loads an int
+														// value
+	// from variable 2
+	final static short	iload_3			= 0x1d;			// ? value loads an int
+														// value
+	// from variable 3
+	final static short	lload_0			= 0x1e;			// ? value load a long
+														// value
+	// from a local variable 0
+	final static short	lload_1			= 0x1f;			// ? value load a long
+														// value
+	// from a local variable 1
+	final static short	aload_0			= 0x2a;			// ? objectref loads a
+														// reference
+	// onto the stack from local
+	// variable 0
+	final static short	aload_1			= 0x2b;			// ? objectref loads a
+														// reference
+	// onto the stack from local
+	// variable 1
+	final static short	aload_2			= 0x2c;			// ? objectref loads a
+														// reference
+	// onto the stack from local
+	// variable 2
+	final static short	aload_3			= 0x2d;			// ? objectref loads a
+														// reference
+	// onto the stack from local
+	// variable 3
+	final static short	iaload			= 0x2e;			// arrayref, index ?
+														// value loads
+	// an int from an array
+	final static short	laload			= 0x2f;			// arrayref, index ?
+														// value load
+	// a long from an array
+	final static short	astore			= 0x3a;			// index objectref ?
+														// stores a
+	// reference into a local
+	// variable #index
+	final static short	istore_0		= 0x3b;			// value ? store int
+														// value into
+	// variable 0
+	final static short	istore_1		= 0x3c;			// value ? store int
+														// value into
+	// variable 1
+	final static short	istore_2		= 0x3d;			// value ? store int
+														// value into
+	// variable 2
+	final static short	istore_3		= 0x3e;			// value ? store int
+														// value into
+	// variable 3
+	final static short	lstore_0		= 0x3f;			// value ? store a long
+														// value in
+	// a local variable 0
+	final static short	dstore_3		= 0x4a;			// value ? stores a
+														// double into
+	// local variable 3
+	final static short	astore_0		= 0x4b;			// objectref ? stores a
+	// reference into local variable
+	// 0
+	final static short	astore_1		= 0x4c;			// objectref ? stores a
+	// reference into local variable
+	// 1
+	final static short	astore_2		= 0x4d;			// objectref ? stores a
+	// reference into local variable
+	// 2
+	final static short	astore_3		= 0x4e;			// objectref ? stores a
+	// reference into local variable
+	// 3
+	final static short	iastore			= 0x4f;			// arrayref, index,
+														// value ?
+	// stores an int into an array
+	final static short	dup_x1			= 0x5a;			// value2, value1 ?
+														// value1,
+	// value2, value1 inserts a copy
+	// of the top value into the
+	// stack two values from the top
+	final static short	dup_x2			= 0x5b;			// value3, value2,
+														// value1 ?
+	// value1, value3, value2,
+	// value1 inserts a copy of the
+	// top value into the stack two
+	// (if value2 is double or long
+	// it takes up the entry of
+	// value3, too) or three values
+	// (if value2 is neither double
+	// nor long) from the top
+	final static short	dup2			= 0x5c;			// {value2, value1} ?
+														// {value2,
+	// value1}, {value2, value1}
+	// duplicate top two stack words
+	// (two values, if value1 is not
+	// double nor long; a single
+	// value, if value1 is double or
+	// long)
+	final static short	dup2_x1			= 0x5d;			// value3, {value2,
+														// value1} ?
+	// {value2, value1}, value3,
+	// {value2, value1} duplicate
+	// two words and insert beneath
+	// third word (see explanation
+	// above)
+	final static short	dup2_x2			= 0x5e;			// {value4, value3},
+														// {value2,
+	// value1} ? {value2, value1},
+	// {value4, value3}, {value2,
+	// value1} duplicate two words
+	// and insert beneath fourth
+	// word
+	final static short	swap			= 0x5f;			// value2, value1 ?
+														// value1,
+	// value2 swaps two top words on
+	// the stack (note that value1
+	// and value2 must not be double
+	// or long)
+	final static short	fmul			= 0x6a;			// value1, value2 ?
+														// result
+	// multiplies two floats
+	final static short	dmul			= 0x6b;			// value1, value2 ?
+														// result
+	// multiplies two doubles
+	final static short	idiv			= 0x6c;			// value1, value2 ?
+														// result
+	// divides two integers
+	final static short	ldiv			= 0x6d;			// value1, value2 ?
+														// result
+	// divide two longs
+	final static short	fdiv			= 0x6e;			// value1, value2 ?
+														// result
+	// divides two floats
+	final static short	ddiv			= 0x6f;			// value1, value2 ?
+														// result
+	// divides two doubles
+	final static short	ishr			= 0x7a;			// value1, value2 ?
+														// result int
+	// shift right
+	final static short	lshr			= 0x7b;			// value1, value2 ?
+														// result
+	// bitwise shift right of a long
+	// value1 by value2 positions
+	final static short	iushr			= 0x7c;			// value1, value2 ?
+														// result int
+	// shift right
+	final static short	lushr			= 0x7d;			// value1, value2 ?
+														// result
+	// bitwise shift right of a long
+	// value1 by value2 positions,
+	// unsigned
+	final static short	iand			= 0x7e;			// value1, value2 ?
+														// result
+	// performs a logical and on two
+	// integers
+	final static short	land			= 0x7f;			// value1, value2 ?
+														// result
+	// bitwise and of two longs
+	final static short	l2d				= 0x8a;			// value ? result
+														// converts a
+	// long to a double
+	final static short	f2i				= 0x8b;			// value ? result
+														// converts a
+	// float to an int
+	final static short	f2l				= 0x8c;			// value ? result
+														// converts a
+	// float to a long
+	final static short	f2d				= 0x8d;			// value ? result
+														// converts a
+	// float to a double
+	final static short	d2i				= 0x8e;			// value ? result
+														// converts a
+	// double to an int
+	final static short	d2l				= 0x8f;			// value ? result
+														// converts a
+	// double to a long
+	final static short	ifne			= 0x9a;			// branchbyte1,
+														// branchbyte2
+	// value ? if value is not 0,
+	// branch to instruction at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	iflt			= 0x9b;			// branchbyte1,
+														// branchbyte2
+	// value ? if value is less than
+	// 0, branch to instruction at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	ifge			= 0x9c;			// branchbyte1,
+														// branchbyte2
+	// value ? if value is greater
+	// than or equal to 0, branch to
+	// instruction at branchoffset
+	// (signed short constructed
+	// from unsigned bytes
+	// branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	ifgt			= 0x9d;			// branchbyte1,
+														// branchbyte2
+	// value ? if value is greater
+	// than 0, branch to instruction
+	// at branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	ifle			= 0x9e;			// branchbyte1,
+														// branchbyte2
+	// value ? if value is less than
+	// or equal to 0, branch to
+	// instruction at branchoffset
+	// (signed short constructed
+	// from unsigned bytes
+	// branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	if_icmpeq		= 0x9f;			// branchbyte1,
+														// branchbyte2
+	// value1, value2 ? if ints are
+	// equal, branch to instruction
+	// at branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	if_icmpne		= 0xa0;			// branchbyte1,
+														// branchbyte2
+	// value1, value2 ? if ints are
+	// not equal, branch to
+	// instruction at branchoffset
+	// (signed short constructed
+	// from unsigned bytes
+	// branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	if_icmplt		= 0xa1;			// branchbyte1,
+														// branchbyte2
+	// value1, value2 ? if value1 is
+	// less than value2, branch to
+	// instruction at branchoffset
+	// (signed short constructed
+	// from unsigned bytes
+	// branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	if_icmpge		= 0xa2;			// branchbyte1,
+														// branchbyte2
+	// value1, value2 ? if value1 is
+	// greater than or equal to
+	// value2, branch to instruction
+	// at branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	if_icmpgt		= 0xa3;			// branchbyte1,
+														// branchbyte2
+	// value1, value2 ? if value1 is
+	// greater than value2, branch
+	// to instruction at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	if_icmple		= 0xa4;			// branchbyte1,
+														// branchbyte2
+	// value1, value2 ? if value1 is
+	// less than or equal to value2,
+	// branch to instruction at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	if_acmpeq		= 0xa5;			// branchbyte1,
+														// branchbyte2
+	// value1, value2 ? if
+	// references are equal, branch
+	// to instruction at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	if_acmpne		= 0xa6;			// branchbyte1,
+														// branchbyte2
+	// value1, value2 ? if
+	// references are not equal,
+	// branch to instruction at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	goto_			= 0xa7;			// branchbyte1,
+														// branchbyte2 [no
+	// change] goes to another
+	// instruction at branchoffset
+	// (signed short constructed
+	// from unsigned bytes
+	// branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	jsr				= 0xa8;			// branchbyte1,
+														// branchbyte2 ?
+	// address jump to subroutine at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2) and place the
+	// return address on the stack
+	final static short	ret				= 0xa9;			// index [No change]
+														// continue
+	// execution from address taken
+	// from a local variable #index
+	// (the asymmetry with jsr is
+	// intentional)
+	final static short	tableswitch		= 0xaa;			// [0-3 bytes padding],
+	// defaultbyte1, defaultbyte2,
+	// defaultbyte3, defaultbyte4,
+	// lowbyte1, lowbyte2, lowbyte3,
+	// lowbyte4, highbyte1,
+	// highbyte2, highbyte3,
+	// highbyte4, jump offsets...
+	// index ? continue execution
+	// from an address in the table
+	// at offset index
+	final static short	lookupswitch	= 0xab;			// <0-3 bytes padding>,
+	// defaultbyte1, defaultbyte2,
+	// defaultbyte3, defaultbyte4,
+	// npairs1, npairs2, npairs3,
+	// npairs4, match-offset
+	// pairs... key ? a target
+	// address is looked up from a
+	// table using a key and
+	// execution continues from the
+	// instruction at that address
+	final static short	ireturn			= 0xac;			// value ? [empty]
+														// returns an
+	// integer from a method
+	final static short	lreturn			= 0xad;			// value ? [empty]
+														// returns a
+	// long value
+	final static short	freturn			= 0xae;			// value ? [empty]
+														// returns a
+	// float
+	final static short	dreturn			= 0xaf;			// value ? [empty]
+														// returns a
+	// double from a method
+	final static short	areturn			= 0xb0;			// objectref ? [empty]
+														// returns a
+	// reference from a method
+	final static short	return_			= 0xb1;			// ? [empty] return void
+														// from
+	// method
+	final static short	getstatic		= 0xb2;			// index1, index2 ?
+														// value gets a
+	// static field value of a
+	// class, where the field is
+	// identified by field reference
+	// in the constant pool index
+	// (index1 << 8 + index2)
+	final static short	putstatic		= 0xb3;			// indexbyte1,
+														// indexbyte2 value
+	// ? set static field to value
+	// in a class, where the field
+	// is identified by a field
+	// reference index in constant
+	// pool (indexbyte1 << 8 +
+	// indexbyte2)
+	final static short	getfield		= 0xb4;			// index1, index2
+														// objectref ?
+	// value gets a field value of
+	// an object objectref, where
+	// the field is identified by
+	// field reference in the
+	// constant pool index (index1
+	// << 8 + index2)
+	final static short	putfield		= 0xb5;			// indexbyte1,
+														// indexbyte2
+	// objectref, value ? set field
+	// to value in an object
+	// objectref, where the field is
+	// identified by a field
+	// reference index in constant
+	// pool (indexbyte1 << 8 +
+	// indexbyte2)
+	final static short	invokevirtual	= 0xb6;			// indexbyte1,
+														// indexbyte2
+	// objectref, [arg1, arg2, ...]
+	// ? invoke virtual method on
+	// object objectref, where the
+	// method is identified by
+	// method reference index in
+	// constant pool (indexbyte1 <<
+	// 8 + indexbyte2)
+	final static short	invokespecial	= 0xb7;			// indexbyte1,
+														// indexbyte2
+	// objectref, [arg1, arg2, ...]
+	// ? invoke instance method on
+	// object objectref, where the
+	// method is identified by
+	// method reference index in
+	// constant pool (indexbyte1 <<
+	// 8 + indexbyte2)
+	final static short	invokestatic	= 0xb8;			// indexbyte1,
+														// indexbyte2 [arg1,
+	// arg2, ...] ? invoke a static
+	// method, where the method is
+	// identified by method
+	// reference index in constant
+	// pool (indexbyte1 << 8 +
+	// indexbyte2)
+	final static short	invokeinterface	= 0xb9;			// indexbyte1,
+														// indexbyte2,
+	// count, 0 objectref, [arg1,
+	// arg2, ...] ? invokes an
+	// interface method on object
+	// objectref, where the
+	// interface method is
+	// identified by method
+	// reference index in constant
+	// pool (indexbyte1 << 8 +
+	// indexbyte2)
+	final static short	xxxunusedxxx	= 0xba;			// this opcode is
+														// reserved "for
+	// historical reasons"
+	final static short	new_			= 0xbb;			// indexbyte1,
+														// indexbyte2 ?
+	// objectref creates new object
+	// of type identified by class
+	// reference in constant pool
+	// index (indexbyte1 << 8 +
+	// indexbyte2)
+	final static short	newarray		= 0xbc;			// atype count ?
+														// arrayref
+	// creates new array with count
+	// elements of primitive type
+	// identified by atype
+	final static short	anewarray		= 0xbd;			// indexbyte1,
+														// indexbyte2 count
+	// ? arrayref creates a new
+	// array of references of length
+	// count and component type
+	// identified by the class
+	// reference index (indexbyte1
+	// << 8 + indexbyte2) in the
+	// constant pool
+	final static short	arraylength		= 0xbe;			// arrayref ? length
+														// gets the
+	// length of an array
+	final static short	athrow			= 0xbf;			// objectref ? [empty],
+	// objectref throws an error or
+	// exception (notice that the
+	// rest of the stack is cleared,
+	// leaving only a reference to
+	// the Throwable)
+	final static short	checkcast		= 0xc0;			// indexbyte1,
+														// indexbyte2
+	// objectref ? objectref checks
+	// whether an objectref is of a
+	// certain type, the class
+	// reference of which is in the
+	// constant pool at index
+	// (indexbyte1 << 8 +
+	// indexbyte2)
+	final static short	instanceof_		= 0xc1;			// indexbyte1,
+														// indexbyte2
+	// objectref ? result determines
+	// if an object objectref is of
+	// a given type, identified by
+	// class reference index in
+	// constant pool (indexbyte1 <<
+	// 8 + indexbyte2)
+	final static short	monitorenter	= 0xc2;			// objectref ? enter
+														// monitor for
+	// object ("grab the lock" -
+	// start of synchronized()
+	// section)
+	final static short	monitorexit		= 0xc3;			// objectref ? exit
+														// monitor for
+	// object ("release the lock" -
+	// end of synchronized()
+	// section)
+	final static short	wide			= 0xc4;			// opcode, indexbyte1,
+	// indexbyte2
+	final static short	multianewarray	= 0xc5;			// indexbyte1,
+														// indexbyte2,
+	// dimensions count1,
+	// [count2,...] ? arrayref
+	// create a new array of
+	// dimensions dimensions with
+	// elements of type identified
+	// by class reference in
+	// constant pool index
+	// (indexbyte1 << 8 +
+	// indexbyte2); the sizes of
+	// each dimension is identified
+	// by count1, [count2, etc]
+	final static short	ifnull			= 0xc6;			// branchbyte1,
+														// branchbyte2
+	// value ? if value is null,
+	// branch to instruction at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	ifnonnull		= 0xc7;			// branchbyte1,
+														// branchbyte2
+	// value ? if value is not null,
+	// branch to instruction at
+	// branchoffset (signed short
+	// constructed from unsigned
+	// bytes branchbyte1 << 8 +
+	// branchbyte2)
+	final static short	goto_w			= 0xc8;			// branchbyte1,
+														// branchbyte2,
+	// branchbyte3, branchbyte4 [no
+	// change] goes to another
+	// instruction at branchoffset
+	// (signed int constructed from
+	// unsigned bytes branchbyte1 <<
+	// 24 + branchbyte2 << 16 +
+	// branchbyte3 << 8 +
+	// branchbyte4)
+	final static short	jsr_w			= 0xc9;			// branchbyte1,
+														// branchbyte2,
+	// branchbyte3, branchbyte4 ?
+	// address jump to subroutine at
+	// branchoffset (signed int
+	// constructed from unsigned
+	// bytes branchbyte1 << 24 +
+	// branchbyte2 << 16 +
+	// branchbyte3 << 8 +
+	// branchbyte4) and place the
+	// return address on the stack
+	final static short	breakpoint		= 0xca;			// reserved for
+														// breakpoints in
+	// Java debuggers; should not
+	// appear in any class file
+	final static short	impdep1			= 0xfe;			// reserved for
+	// implementation-dependent
+	// operations within debuggers;
+	// should not appear in any
+	// class file
+	final static short	impdep2			= 0xff;			// reserved for
+	// implementation-dependent
+	// operations within debuggers;
+	// should not appear in any
+	// class file
+
+	final static byte	OFFSETS[]		= new byte[256];
+
+	static {
+		OFFSETS[bipush] = 1; // byte ? value pushes a byte onto the
+		// stack as an integer value
+		OFFSETS[sipush] = 2; // byte1, byte2 ? value pushes a signed
+		// integer (byte1 << 8 + byte2) onto the
+		// stack
+		OFFSETS[ldc] = 1; // index ? value pushes a constant
+		// #index from a constant pool (String,
+		// int, float or class type) onto the
+		// stack
+		OFFSETS[ldc_w] = 2; // indexbyte1, indexbyte2 ? value pushes
+		// a constant #index from a constant
+		// pool (String, int, float or class
+		// type) onto the stack (wide index is
+		// constructed as indexbyte1 << 8 +
+		// indexbyte2)
+		OFFSETS[ldc2_w] = 2; // indexbyte1, indexbyte2 ? value pushes
+		// a constant #index from a constant
+		// pool (double or long) onto the stack
+		// (wide index is constructed as
+		// indexbyte1 << 8 + indexbyte2)
+		OFFSETS[iload] = 1; // index ? value loads an int value from
+		// a variable #index
+		OFFSETS[lload] = 1; // index ? value load a long value from
+		// a local variable #index
+		OFFSETS[fload] = 1; // index ? value loads a float value
+		// from a local variable #index
+		OFFSETS[dload] = 1; // index ? value loads a double value
+		// from a local variable #index
+		OFFSETS[aload] = 1; // index ? objectref loads a reference
+		// onto the stack from a local variable
+		// #index
+		OFFSETS[istore] = 1; // index value ? store int value into
+		// variable #index
+		OFFSETS[lstore] = 1; // index value ? store a long value in a
+		// local variable #index
+		OFFSETS[fstore] = 1; // index value ? stores a float value
+		// into a local variable #index
+		OFFSETS[dstore] = 1; // index value ? stores a double value
+		// into a local variable #index
+		OFFSETS[iinc] = 2; // index, const [No change] increment
+		// local variable #index by signed byte
+		// const
+		OFFSETS[ifeq] = 2; // branchbyte1, branchbyte2 value ? if
+		// value is 0, branch to instruction at
+		// branchoffset (signed short
+		// constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[astore] = 1; // index objectref ? stores a reference
+		// into a local variable #index
+		OFFSETS[ifne] = 2; // branchbyte1, branchbyte2 value ? if
+		// value is not 0, branch to instruction
+		// at branchoffset (signed short
+		// constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[iflt] = 2; // branchbyte1, branchbyte2 value ? if
+		// value is less than 0, branch to
+		// instruction at branchoffset (signed
+		// short constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[ifge] = 2; // branchbyte1, branchbyte2 value ? if
+		// value is greater than or equal to 0,
+		// branch to instruction at branchoffset
+		// (signed short constructed from
+		// unsigned bytes branchbyte1 << 8 +
+		// branchbyte2)
+		OFFSETS[ifgt] = 2; // branchbyte1, branchbyte2 value ? if
+		// value is greater than 0, branch to
+		// instruction at branchoffset (signed
+		// short constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[ifle] = 2; // branchbyte1, branchbyte2 value ? if
+		// value is less than or equal to 0,
+		// branch to instruction at branchoffset
+		// (signed short constructed from
+		// unsigned bytes branchbyte1 << 8 +
+		// branchbyte2)
+		OFFSETS[if_icmpeq] = 2; // branchbyte1, branchbyte2 value1,
+		// value2 ? if ints are equal,
+		// branch to instruction at
+		// branchoffset (signed short
+		// constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[if_icmpne] = 2; // branchbyte1, branchbyte2 value1,
+		// value2 ? if ints are not equal,
+		// branch to instruction at
+		// branchoffset (signed short
+		// constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[if_icmplt] = 2; // branchbyte1, branchbyte2 value1,
+		// value2 ? if value1 is less than
+		// value2, branch to instruction at
+		// branchoffset (signed short
+		// constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[if_icmpge] = 2; // branchbyte1, branchbyte2 value1,
+		// value2 ? if value1 is greater
+		// than or equal to value2, branch
+		// to instruction at branchoffset
+		// (signed short constructed from
+		// unsigned bytes branchbyte1 << 8 +
+		// branchbyte2)
+		OFFSETS[if_icmpgt] = 2; // branchbyte1, branchbyte2 value1,
+		// value2 ? if value1 is greater
+		// than value2, branch to
+		// instruction at branchoffset
+		// (signed short constructed from
+		// unsigned bytes branchbyte1 << 8 +
+		// branchbyte2)
+		OFFSETS[if_icmple] = 2; // branchbyte1, branchbyte2 value1,
+		// value2 ? if value1 is less than
+		// or equal to value2, branch to
+		// instruction at branchoffset
+		// (signed short constructed from
+		// unsigned bytes branchbyte1 << 8 +
+		// branchbyte2)
+		OFFSETS[if_acmpeq] = 2; // branchbyte1, branchbyte2 value1,
+		// value2 ? if references are equal,
+		// branch to instruction at
+		// branchoffset (signed short
+		// constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[if_acmpne] = 2; // branchbyte1, branchbyte2 value1,
+		// value2 ? if references are not
+		// equal, branch to instruction at
+		// branchoffset (signed short
+		// constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[goto_] = 2; // branchbyte1, branchbyte2 [no change]
+		// goes to another instruction at
+		// branchoffset (signed short
+		// constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[jsr] = 2; // branchbyte1, branchbyte2 ? address
+		// jump to subroutine at branchoffset
+		// (signed short constructed from
+		// unsigned bytes branchbyte1 << 8 +
+		// branchbyte2) and place the return
+		// address on the stack
+		OFFSETS[ret] = 1; // index [No change] continue execution
+		// from address taken from a local
+		// variable #index (the asymmetry with
+		// jsr is intentional)
+		OFFSETS[tableswitch] = -1; // [0-3 bytes padding],
+		// defaultbyte1, defaultbyte2,
+		// defaultbyte3, defaultbyte4,
+		// lowbyte1, lowbyte2, lowbyte3,
+		// lowbyte4, highbyte1,
+		// highbyte2, highbyte3,
+		// highbyte4, jump offsets...
+		// index ? continue execution
+		// from an address in the table
+		// at offset index
+		OFFSETS[lookupswitch] = -1; // <0-3 bytes padding>,
+		// defaultbyte1, defaultbyte2,
+		// defaultbyte3, defaultbyte4,
+		// npairs1, npairs2, npairs3,
+		// npairs4, match-offset
+		// pairs... key ? a target
+		// address is looked up from a
+		// table using a key and
+		// execution continues from the
+		// instruction at that address
+		OFFSETS[getstatic] = 2; // index1, index2 ? value gets a
+		// static field value of a class,
+		// where the field is identified by
+		// field reference in the constant
+		// pool index (index1 << 8 + index2)
+		OFFSETS[putstatic] = 2; // indexbyte1, indexbyte2 value ?
+		// set static field to value in a
+		// class, where the field is
+		// identified by a field reference
+		// index in constant pool
+		// (indexbyte1 << 8 + indexbyte2)
+		OFFSETS[getfield] = 2; // index1, index2 objectref ? value
+		// gets a field value of an object
+		// objectref, where the field is
+		// identified by field reference in
+		// the constant pool index (index1
+		// << 8 + index2)
+		OFFSETS[putfield] = 2; // indexbyte1, indexbyte2 objectref,
+		// value ? set field to value in an
+		// object objectref, where the field
+		// is identified by a field
+		// reference index in constant pool
+		// (indexbyte1 << 8 + indexbyte2)
+		OFFSETS[invokevirtual] = 2; // indexbyte1, indexbyte2
+		// objectref, [arg1, arg2, ...]
+		// ? invoke virtual method on
+		// object objectref, where the
+		// method is identified by
+		// method reference index in
+		// constant pool (indexbyte1 <<
+		// 8 + indexbyte2)
+		OFFSETS[invokespecial] = 2; // indexbyte1, indexbyte2
+		// objectref, [arg1, arg2, ...]
+		// ? invoke instance method on
+		// object objectref, where the
+		// method is identified by
+		// method reference index in
+		// constant pool (indexbyte1 <<
+		// 8 + indexbyte2)
+		OFFSETS[invokestatic] = 2; // indexbyte1, indexbyte2 [arg1,
+		// arg2, ...] ? invoke a static
+		// method, where the method is
+		// identified by method
+		// reference index in constant
+		// pool (indexbyte1 << 8 +
+		// indexbyte2)
+		OFFSETS[invokeinterface] = 2; // indexbyte1, indexbyte2,
+		// count, 0 objectref,
+		// [arg1, arg2, ...] ?
+		// invokes an interface
+		// method on object
+		// objectref, where the
+		// interface method is
+		// identified by method
+		// reference index in
+		// constant pool (indexbyte1
+		// << 8 + indexbyte2)
+		OFFSETS[new_] = 2; // indexbyte1, indexbyte2 ? objectref
+		// creates new object of type identified
+		// by class reference in constant pool
+		// index (indexbyte1 << 8 + indexbyte2)
+		OFFSETS[newarray] = 1; // atype count ? arrayref creates
+		// new array with count elements of
+		// primitive type identified by
+		// atype
+		OFFSETS[anewarray] = 2; // indexbyte1, indexbyte2 count ?
+		// arrayref creates a new array of
+		// references of length count and
+		// component type identified by the
+		// class reference index (indexbyte1
+		// << 8 + indexbyte2) in the
+		// constant pool
+		OFFSETS[checkcast] = 2; // indexbyte1, indexbyte2 objectref
+		// ? objectref checks whether an
+		// objectref is of a certain type,
+		// the class reference of which is
+		// in the constant pool at index
+		// (indexbyte1 << 8 + indexbyte2)
+		OFFSETS[instanceof_] = 2; // indexbyte1, indexbyte2 objectref
+		// ? result determines if an object
+		// objectref is of a given type,
+		// identified by class reference
+		// index in constant pool
+		// (indexbyte1 << 8 + indexbyte2)
+		OFFSETS[wide] = 3; // opcode, indexbyte1, indexbyte2
+		OFFSETS[multianewarray] = 3; // indexbyte1, indexbyte2,
+		// dimensions count1,
+		// [count2,...] ? arrayref
+		// create a new array of
+		// dimensions dimensions with
+		// elements of type identified
+		// by class reference in
+		// constant pool index
+		// (indexbyte1 << 8 +
+		// indexbyte2); the sizes of
+		// each dimension is identified
+		// by count1, [count2, etc]
+		OFFSETS[ifnull] = 2; // branchbyte1, branchbyte2 value ? if
+		// value is null, branch to instruction
+		// at branchoffset (signed short
+		// constructed from unsigned bytes
+		// branchbyte1 << 8 + branchbyte2)
+		OFFSETS[ifnonnull] = 2; // branchbyte1, branchbyte2 value ?
+		// if value is not null, branch to
+		// instruction at branchoffset
+		// (signed short constructed from
+		// unsigned bytes branchbyte1 << 8 +
+		// branchbyte2)
+		OFFSETS[goto_w] = 4; // branchbyte1, branchbyte2,
+		// branchbyte3, branchbyte4 [no change]
+		// goes to another instruction at
+		// branchoffset (signed int constructed
+		// from unsigned bytes branchbyte1 << 24
+		// + branchbyte2 << 16 + branchbyte3 <<
+		// 8 + branchbyte4)
+		OFFSETS[jsr_w] = 4; // branchbyte1, branchbyte2,
+		// branchbyte3, branchbyte4 ? address
+		// jump to subroutine at branchoffset
+		// (signed int constructed from unsigned
+		// bytes branchbyte1 << 24 + branchbyte2
+		// << 16 + branchbyte3 << 8 +
+		// branchbyte4) and place the return
+		// address on the stack
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/PreprocessResource.java b/bundleplugin/src/main/java/aQute/lib/osgi/PreprocessResource.java
new file mode 100644
index 0000000..e77f811
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/PreprocessResource.java
@@ -0,0 +1,37 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+
+public class PreprocessResource extends AbstractResource {
+    final Resource  resource;
+    final Processor processor;
+
+    public PreprocessResource(Processor processor, Resource r) {
+        super(r.lastModified());
+        this.processor = processor;
+        this.resource = r;
+        extra = resource.getExtra();
+    }
+
+    protected byte[] getBytes() throws Exception {
+        ByteArrayOutputStream bout = new ByteArrayOutputStream(2000);
+        OutputStreamWriter osw = new OutputStreamWriter(bout, Constants.DEFAULT_CHARSET);
+        PrintWriter pw = new PrintWriter(osw);
+        InputStream in = resource.openInputStream();
+        try {
+            BufferedReader rdr = new BufferedReader(new InputStreamReader(in,"UTF8"));
+            String line = rdr.readLine();
+            while (line != null) {
+                line = processor.getReplacer().process(line);
+                pw.println(line);
+                line = rdr.readLine();
+            }
+            pw.flush();
+            byte [] data= bout.toByteArray();
+            return data;
+                
+        } finally {
+            in.close();
+        }        
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Processor.java b/bundleplugin/src/main/java/aQute/lib/osgi/Processor.java
new file mode 100644
index 0000000..b2352f0
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Processor.java
@@ -0,0 +1,1373 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.jar.*;
+import java.util.regex.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.io.*;
+import aQute.libg.generics.*;
+import aQute.libg.header.*;
+import aQute.libg.reporter.*;
+
+public class Processor implements Reporter, Registry, Constants, Closeable {
+	static ThreadLocal<Processor>	current			= new ThreadLocal<Processor>();
+	static ExecutorService			executor		= Executors.newCachedThreadPool();
+	static Random					random			= new Random();
+
+	// TODO handle include files out of date
+	// TODO make splitter skip eagerly whitespace so trim is not necessary
+	public static String			LIST_SPLITTER	= "\\s*,\\s*";
+	final List<String>				errors			= new ArrayList<String>();
+	final List<String>				warnings		= new ArrayList<String>();
+	final Set<Object>				basicPlugins	= new HashSet<Object>();
+	final Set<Closeable>			toBeClosed		= new HashSet<Closeable>();
+	Set<Object>						plugins;
+
+	boolean							pedantic;
+	boolean							trace;
+	boolean							exceptions;
+	boolean							fileMustExist	= true;
+
+	private File					base			= new File("").getAbsoluteFile();
+
+	Properties						properties;
+	private Macro					replacer;
+	private long					lastModified;
+	private File					propertiesFile;
+	private boolean					fixup			= true;
+	long							modified;
+	Processor						parent;
+	Set<File>						included;
+	CL								pluginLoader;
+	Collection<String>				filter;
+	HashSet<String>					missingCommand;
+
+	public Processor() {
+		properties = new Properties();
+	}
+
+	public Processor(Properties parent) {
+		properties = new Properties(parent);
+	}
+
+	public Processor(Processor parent) {
+		this(parent.properties);
+		this.parent = parent;
+	}
+
+	public void setParent(Processor processor) {
+		this.parent = processor;
+		Properties ext = new Properties(processor.properties);
+		ext.putAll(this.properties);
+		this.properties = ext;
+	}
+
+	public Processor getParent() {
+		return parent;
+	}
+
+	public Processor getTop() {
+		if (parent == null)
+			return this;
+		else
+			return parent.getTop();
+	}
+
+	public void getInfo(Processor processor, String prefix) {
+		if (isFailOk())
+			addAll(warnings, processor.getErrors(), prefix);
+		else
+			addAll(errors, processor.getErrors(), prefix);
+		addAll(warnings, processor.getWarnings(), prefix);
+
+		processor.errors.clear();
+		processor.warnings.clear();
+	}
+
+	public void getInfo(Processor processor) {
+		getInfo(processor, "");
+	}
+
+	private <T> void addAll(List<String> to, List<? extends T> from, String prefix) {
+		for (T x : from) {
+			to.add(prefix + x);
+		}
+	}
+
+	/**
+	 * A processor can mark itself current for a thread.
+	 * 
+	 * @return
+	 */
+	private Processor current() {
+		Processor p = current.get();
+		if (p == null)
+			return this;
+		else
+			return p;
+	}
+
+	public void warning(String string, Object... args) {
+		Processor p = current();
+		String s = String.format(string, args);
+		if (!p.warnings.contains(s))
+			p.warnings.add(s);
+	}
+
+	public void error(String string, Object... args) {
+		Processor p = current();
+		if (p.isFailOk())
+			p.warning(string, args);
+		else {
+			String s = String.format(string, args);
+			if (!p.errors.contains(s))
+				p.errors.add(s);
+		}
+	}
+
+	public void error(String string, Throwable t, Object... args) {
+		Processor p = current();
+
+		if (p.isFailOk())
+			p.warning(string + ": " + t, args);
+		else {
+			p.errors.add("Exception: " + t.getMessage());
+			String s = String.format(string, args);
+			if (!p.errors.contains(s))
+				p.errors.add(s);
+		}
+		if (p.exceptions)
+			t.printStackTrace();
+	}
+
+	public List<String> getWarnings() {
+		return warnings;
+	}
+
+	public List<String> getErrors() {
+		return errors;
+	}
+
+	public Map<String, Map<String, String>> parseHeader(String value) {
+		return parseHeader(value, this);
+	}
+
+	/**
+	 * Standard OSGi header parser.
+	 * 
+	 * @param value
+	 * @return
+	 */
+	static public Map<String, Map<String, String>> parseHeader(String value, Processor logger) {
+		return OSGiHeader.parseHeader(value, logger);
+	}
+
+	Map<String, Map<String, String>> getClauses(String header) {
+		return parseHeader(getProperty(header));
+	}
+
+	public void addClose(Closeable jar) {
+		toBeClosed.add(jar);
+	}
+
+	/**
+	 * Remove all entries from a map that start with a specific prefix
+	 * 
+	 * @param <T>
+	 * @param source
+	 * @param prefix
+	 * @return
+	 */
+	static <T> Map<String, T> removeKeys(Map<String, T> source, String prefix) {
+		Map<String, T> temp = new TreeMap<String, T>(source);
+		for (Iterator<String> p = temp.keySet().iterator(); p.hasNext();) {
+			String pack = (String) p.next();
+			if (pack.startsWith(prefix))
+				p.remove();
+		}
+		return temp;
+	}
+
+	public void progress(String s, Object... args) {
+		trace(s, args);
+	}
+
+	public boolean isPedantic() {
+		return current().pedantic;
+	}
+
+	public void setPedantic(boolean pedantic) {
+		this.pedantic = pedantic;
+	}
+
+	public static File getFile(File base, String file) {
+		return IO.getFile(base, file);
+	}
+
+	public File getFile(String file) {
+		return getFile(base, file);
+	}
+
+	/**
+	 * Return a list of plugins that implement the given class.
+	 * 
+	 * @param clazz
+	 *            Each returned plugin implements this class/interface
+	 * @return A list of plugins
+	 */
+	public <T> List<T> getPlugins(Class<T> clazz) {
+		List<T> l = new ArrayList<T>();
+		Set<Object> all = getPlugins();
+		for (Object plugin : all) {
+			if (clazz.isInstance(plugin))
+				l.add(clazz.cast(plugin));
+		}
+		return l;
+	}
+
+	/**
+	 * Returns the first plugin it can find of the given type.
+	 * 
+	 * @param <T>
+	 * @param clazz
+	 * @return
+	 */
+	public <T> T getPlugin(Class<T> clazz) {
+		Set<Object> all = getPlugins();
+		for (Object plugin : all) {
+			if (clazz.isInstance(plugin))
+				return clazz.cast(plugin);
+		}
+		return null;
+	}
+
+	/**
+	 * Return a list of plugins. Plugins are defined with the -plugin command.
+	 * They are class names, optionally associated with attributes. Plugins can
+	 * implement the Plugin interface to see these attributes.
+	 * 
+	 * Any object can be a plugin.
+	 * 
+	 * @return
+	 */
+	protected synchronized Set<Object> getPlugins() {
+		if (this.plugins != null)
+			return this.plugins;
+
+		missingCommand = new HashSet<String>();
+		Set<Object> list = new LinkedHashSet<Object>();
+
+		// The owner of the plugin is always in there.
+		list.add(this);
+		setTypeSpecificPlugins(list);
+
+		if (parent != null)
+			list.addAll(parent.getPlugins());
+
+		// We only use plugins now when they are defined on our level
+		// and not if it is in our parent. We inherit from our parent
+		// through the previous block.
+
+		if (properties.containsKey(PLUGIN)) {
+			String spe = getProperty(PLUGIN);
+			if (spe.equals(NONE))
+				return new LinkedHashSet<Object>();
+
+			loadPlugins(list, spe);
+		}
+
+		return this.plugins = list;
+	}
+
+	/**
+	 * @param list
+	 * @param spe
+	 */
+	protected void loadPlugins(Set<Object> list, String spe) {
+		Map<String, Map<String, String>> plugins = parseHeader(spe);
+		for (Map.Entry<String, Map<String, String>> entry : plugins.entrySet()) {
+			String key = (String) entry.getKey();
+
+			try {
+				CL loader = getLoader();
+				String path = entry.getValue().get(PATH_DIRECTIVE);
+				if (path != null) {
+					String parts[] = path.split("\\s*,\\s*");
+					for (String p : parts) {
+						File f = getFile(p).getAbsoluteFile();
+						loader.add(f.toURI().toURL());
+					}
+				}
+
+				trace("Using plugin %s", key);
+
+				// Plugins could use the same class with different
+				// parameters so we could have duplicate names Remove
+				// the ! added by the parser to make each name unique.
+				key = removeDuplicateMarker(key);
+
+				try {
+					Class<?> c = (Class<?>) loader.loadClass(key);
+					Object plugin = c.newInstance();
+					customize(plugin, entry.getValue());
+					list.add(plugin);
+				} catch (Throwable t) {
+					// We can defer the error if the plugin specifies
+					// a command name. In that case, we'll verify that
+					// a bnd file does not contain any references to a
+					// plugin
+					// command. The reason this feature was added was
+					// to compile plugin classes with the same build.
+					String commands = entry.getValue().get(COMMAND_DIRECTIVE);
+					if (commands == null)
+						error("Problem loading the plugin: %s exception: (%s)", key, t);
+					else {
+						Collection<String> cs = split(commands);
+						missingCommand.addAll(cs);
+					}
+				}
+			} catch (Throwable e) {
+				error("Problem loading the plugin: " + key + " exception: " + e);
+			}
+		}
+	}
+
+	protected void setTypeSpecificPlugins(Set<Object> list) {
+		list.add(executor);
+		list.add(random);
+		list.addAll(basicPlugins);
+	}
+
+	/**
+	 * @param plugin
+	 * @param entry
+	 */
+	protected <T> T customize(T plugin, Map<String, String> map) {
+		if (plugin instanceof Plugin) {
+			if (map != null)
+				((Plugin) plugin).setProperties(map);
+
+			((Plugin) plugin).setReporter(this);
+		}
+		if (plugin instanceof RegistryPlugin) {
+			((RegistryPlugin) plugin).setRegistry(this);
+		}
+		return plugin;
+	}
+
+	public boolean isFailOk() {
+		String v = getProperty(Analyzer.FAIL_OK, null);
+		return v != null && v.equalsIgnoreCase("true");
+	}
+
+	public File getBase() {
+		return base;
+	}
+
+	public void setBase(File base) {
+		this.base = base;
+	}
+
+	public void clear() {
+		errors.clear();
+		warnings.clear();
+	}
+
+	public void trace(String msg, Object... parms) {
+		Processor p = current();
+		if (p.trace) {
+			System.out.printf("# " + msg + "\n", parms);
+		}
+	}
+
+	public <T> List<T> newList() {
+		return new ArrayList<T>();
+	}
+
+	public <T> Set<T> newSet() {
+		return new TreeSet<T>();
+	}
+
+	public static <K, V> Map<K, V> newMap() {
+		return new LinkedHashMap<K, V>();
+	}
+
+	public static <K, V> Map<K, V> newHashMap() {
+		return new HashMap<K, V>();
+	}
+
+	public <T> List<T> newList(Collection<T> t) {
+		return new ArrayList<T>(t);
+	}
+
+	public <T> Set<T> newSet(Collection<T> t) {
+		return new TreeSet<T>(t);
+	}
+
+	public <K, V> Map<K, V> newMap(Map<K, V> t) {
+		return new LinkedHashMap<K, V>(t);
+	}
+
+	public void close() {
+		for (Closeable c : toBeClosed) {
+			try {
+				c.close();
+			} catch (IOException e) {
+				// Who cares?
+			}
+		}
+		toBeClosed.clear();
+	}
+
+	public String _basedir(String args[]) {
+		if (base == null)
+			throw new IllegalArgumentException("No base dir set");
+
+		return base.getAbsolutePath();
+	}
+
+	/**
+	 * Property handling ...
+	 * 
+	 * @return
+	 */
+
+	public Properties getProperties() {
+		if (fixup) {
+			fixup = false;
+			begin();
+		}
+
+		return properties;
+	}
+
+	public String getProperty(String key) {
+		return getProperty(key, null);
+	}
+
+	public void mergeProperties(File file, boolean override) {
+		if (file.isFile()) {
+			try {
+				Properties properties = loadProperties(file);
+				mergeProperties(properties, override);
+			} catch (Exception e) {
+				error("Error loading properties file: " + file);
+			}
+		} else {
+			if (!file.exists())
+				error("Properties file does not exist: " + file);
+			else
+				error("Properties file must a file, not a directory: " + file);
+		}
+	}
+
+	public void mergeProperties(Properties properties, boolean override) {
+		for (Enumeration<?> e = properties.propertyNames(); e.hasMoreElements();) {
+			String key = (String) e.nextElement();
+			String value = properties.getProperty(key);
+			if (override || !getProperties().containsKey(key))
+				setProperty(key, value);
+		}
+	}
+
+	public void setProperties(Properties properties) {
+		doIncludes(getBase(), properties);
+		this.properties.putAll(properties);
+	}
+
+	public void addProperties(File file) throws Exception {
+		addIncluded(file);
+		Properties p = loadProperties(file);
+		setProperties(p);
+	}
+
+	public synchronized void addIncluded(File file) {
+		if (included == null)
+			included = new HashSet<File>();
+		included.add(file);
+	}
+
+	/**
+	 * Inspect the properties and if you find -includes parse the line included
+	 * manifest files or properties files. The files are relative from the given
+	 * base, this is normally the base for the analyzer.
+	 * 
+	 * @param ubase
+	 * @param p
+	 * @param done
+	 * @throws IOException
+	 * @throws IOException
+	 */
+
+	private void doIncludes(File ubase, Properties p) {
+		String includes = p.getProperty(INCLUDE);
+		if (includes != null) {
+			includes = getReplacer().process(includes);
+			p.remove(INCLUDE);
+			Collection<String> clauses = parseHeader(includes).keySet();
+
+			for (String value : clauses) {
+				boolean fileMustExist = true;
+				boolean overwrite = true;
+				while (true) {
+					if (value.startsWith("-")) {
+						fileMustExist = false;
+						value = value.substring(1).trim();
+					} else if (value.startsWith("~")) {
+						// Overwrite properties!
+						overwrite = false;
+						value = value.substring(1).trim();
+					} else
+						break;
+				}
+				try {
+					File file = getFile(ubase, value).getAbsoluteFile();
+					if (!file.isFile() && fileMustExist) {
+						error("Included file " + file
+								+ (file.exists() ? " does not exist" : " is directory"));
+					} else
+						doIncludeFile(file, overwrite, p);
+				} catch (Exception e) {
+					if (fileMustExist)
+						error("Error in processing included file: " + value, e);
+				}
+			}
+		}
+	}
+
+	/**
+	 * @param file
+	 * @param parent
+	 * @param done
+	 * @param overwrite
+	 * @throws FileNotFoundException
+	 * @throws IOException
+	 */
+	public void doIncludeFile(File file, boolean overwrite, Properties target) throws Exception {
+		if (included != null && included.contains(file)) {
+			error("Cyclic or multiple include of " + file);
+		} else {
+			addIncluded(file);
+			updateModified(file.lastModified(), file.toString());
+			InputStream in = new FileInputStream(file);
+			Properties sub;
+			if (file.getName().toLowerCase().endsWith(".mf")) {
+				sub = getManifestAsProperties(in);
+			} else
+				sub = loadProperties(in, file.getAbsolutePath());
+
+			in.close();
+
+			doIncludes(file.getParentFile(), sub);
+			// make sure we do not override properties
+			for (Map.Entry<?, ?> entry : sub.entrySet()) {
+				if (overwrite || !target.containsKey(entry.getKey()))
+					target.setProperty((String) entry.getKey(), (String) entry.getValue());
+			}
+		}
+	}
+
+	public void unsetProperty(String string) {
+		getProperties().remove(string);
+
+	}
+
+	public boolean refresh() {
+		plugins = null; // We always refresh our plugins
+
+		if (propertiesFile == null)
+			return false;
+
+		updateModified(propertiesFile.lastModified(), "properties file");
+		boolean changed = false;
+		if (included != null) {
+			for (File file : included) {
+
+				if (file.exists() == false || file.lastModified() > modified) {
+					updateModified(file.lastModified(), "include file: " + file);
+					changed = true;
+				}
+			}
+		}
+
+		// System.out.println("Modified " + modified + " file: "
+		// + propertiesFile.lastModified() + " diff "
+		// + (modified - propertiesFile.lastModified()));
+
+		// Date last = new Date(propertiesFile.lastModified());
+		// Date current = new Date(modified);
+		changed |= modified < propertiesFile.lastModified();
+		if (changed) {
+			included = null;
+			properties.clear();
+			setProperties(propertiesFile, base);
+			propertiesChanged();
+			return true;
+		}
+		return false;
+	}
+
+	public void propertiesChanged() {
+	}
+
+	/**
+	 * Set the properties by file. Setting the properties this way will also set
+	 * the base for this analyzer. After reading the properties, this will call
+	 * setProperties(Properties) which will handle the includes.
+	 * 
+	 * @param propertiesFile
+	 * @throws FileNotFoundException
+	 * @throws IOException
+	 */
+	public void setProperties(File propertiesFile) throws IOException {
+		propertiesFile = propertiesFile.getAbsoluteFile();
+		setProperties(propertiesFile, propertiesFile.getParentFile());
+	}
+
+	public void setProperties(File propertiesFile, File base) {
+		this.propertiesFile = propertiesFile.getAbsoluteFile();
+		setBase(base);
+		try {
+			if (propertiesFile.isFile()) {
+				// System.out.println("Loading properties " + propertiesFile);
+				long modified = propertiesFile.lastModified();
+				if (modified > System.currentTimeMillis() + 100) {
+					System.out.println("Huh? This is in the future " + propertiesFile);
+					this.modified = System.currentTimeMillis();
+				} else
+					this.modified = modified;
+
+				included = null;
+				Properties p = loadProperties(propertiesFile);
+				setProperties(p);
+			} else {
+				if (fileMustExist) {
+					error("No such properties file: " + propertiesFile);
+				}
+			}
+		} catch (IOException e) {
+			error("Could not load properties " + propertiesFile);
+		}
+	}
+
+	protected void begin() {
+		if (isTrue(getProperty(PEDANTIC)))
+			setPedantic(true);
+	}
+
+	public static boolean isTrue(String value) {
+		if (value == null)
+			return false;
+
+		return !"false".equalsIgnoreCase(value);
+	}
+
+	/**
+	 * Get a property with a proper default
+	 * 
+	 * @param headerName
+	 * @param deflt
+	 * @return
+	 */
+	public String getProperty(String key, String deflt) {
+		String value = null;
+		Processor source = this;
+
+		if (filter != null && filter.contains(key)) {
+			value = (String) getProperties().get(key);
+		} else {
+			while (source != null) {
+				value = (String) source.getProperties().get(key);
+				if (value != null)
+					break;
+
+				source = source.getParent();
+			}
+		}
+
+		if (value != null)
+			return getReplacer().process(value, source);
+		else if (deflt != null)
+			return getReplacer().process(deflt, this);
+		else
+			return null;
+	}
+
+	/**
+	 * Helper to load a properties file from disk.
+	 * 
+	 * @param file
+	 * @return
+	 * @throws IOException
+	 */
+	public Properties loadProperties(File file) throws IOException {
+		updateModified(file.lastModified(), "Properties file: " + file);
+		InputStream in = new FileInputStream(file);
+		Properties p = loadProperties(in, file.getAbsolutePath());
+		in.close();
+		return p;
+	}
+
+	Properties loadProperties(InputStream in, String name) throws IOException {
+		int n = name.lastIndexOf('/');
+		if (n > 0)
+			name = name.substring(0, n);
+		if (name.length() == 0)
+			name = ".";
+
+		try {
+			Properties p = new Properties();
+			p.load(in);
+			return replaceAll(p, "\\$\\{\\.\\}", name);
+		} catch (Exception e) {
+			error("Error during loading properties file: " + name + ", error:" + e);
+			return new Properties();
+		}
+	}
+
+	/**
+	 * Replace a string in all the values of the map. This can be used to
+	 * preassign variables that change. I.e. the base directory ${.} for a
+	 * loaded properties
+	 */
+
+	public static Properties replaceAll(Properties p, String pattern, String replacement) {
+		Properties result = new Properties();
+		for (Iterator<Map.Entry<Object, Object>> i = p.entrySet().iterator(); i.hasNext();) {
+			Map.Entry<Object, Object> entry = i.next();
+			String key = (String) entry.getKey();
+			String value = (String) entry.getValue();
+			value = value.replaceAll(pattern, replacement);
+			result.put(key, value);
+		}
+		return result;
+	}
+
+	/**
+	 * Merge the attributes of two maps, where the first map can contain
+	 * wildcarded names. The idea is that the first map contains patterns (for
+	 * example *) with a set of attributes. These patterns are matched against
+	 * the found packages in actual. If they match, the result is set with the
+	 * merged set of attributes. It is expected that the instructions are
+	 * ordered so that the instructor can define which pattern matches first.
+	 * Attributes in the instructions override any attributes from the actual.<br/>
+	 * 
+	 * A pattern is a modified regexp so it looks like globbing. The * becomes a
+	 * .* just like the ? becomes a .?. '.' are replaced with \\. Additionally,
+	 * if the pattern starts with an exclamation mark, it will remove that
+	 * matches for that pattern (- the !) from the working set. So the following
+	 * patterns should work:
+	 * <ul>
+	 * <li>com.foo.bar</li>
+	 * <li>com.foo.*</li>
+	 * <li>com.foo.???</li>
+	 * <li>com.*.[^b][^a][^r]</li>
+	 * <li>!com.foo.* (throws away any match for com.foo.*)</li>
+	 * </ul>
+	 * Enough rope to hang the average developer I would say.
+	 * 
+	 * 
+	 * @param instructions
+	 *            the instructions with patterns. A
+	 * @param actual
+	 *            the actual found packages
+	 */
+
+	public static Map<String, Map<String, String>> merge(String type,
+			Map<String, Map<String, String>> instructions, Map<String, Map<String, String>> actual,
+			Set<String> superfluous, Map<String, Map<String, String>> ignored) {
+		Map<String, Map<String, String>> toVisit = new HashMap<String, Map<String, String>>(actual); // we
+		// do
+		// not
+		// want
+		// to
+		// ruin
+		// our
+		// original
+		Map<String, Map<String, String>> result = newMap();
+		for (Iterator<String> i = instructions.keySet().iterator(); i.hasNext();) {
+			String instruction = i.next();
+			String originalInstruction = instruction;
+
+			Map<String, String> instructedAttributes = instructions.get(instruction);
+
+			// Check if we have a fixed (starts with '=') or a
+			// duplicate name. A fixed name is added to the output without
+			// checking against the contents. Duplicates are marked
+			// at the end. In that case we do not pick up any contained
+			// information but just add them to the output including the
+			// marker.
+			if (instruction.startsWith("=")) {
+				result.put(instruction.substring(1), instructedAttributes);
+				superfluous.remove(originalInstruction);
+				continue;
+			}
+			if (isDuplicate(instruction)) {
+				result.put(instruction, instructedAttributes);
+				superfluous.remove(originalInstruction);
+				continue;
+			}
+
+			Instruction instr = Instruction.getPattern(instruction);
+
+			for (Iterator<String> p = toVisit.keySet().iterator(); p.hasNext();) {
+				String packageName = p.next();
+
+				if (instr.matches(packageName)) {
+					superfluous.remove(originalInstruction);
+					if (!instr.isNegated()) {
+						Map<String, String> newAttributes = new HashMap<String, String>();
+						newAttributes.putAll(actual.get(packageName));
+						newAttributes.putAll(instructedAttributes);
+						result.put(packageName, newAttributes);
+					} else if (ignored != null) {
+						ignored.put(packageName, new HashMap<String, String>());
+					}
+					p.remove(); // Can never match again for another pattern
+				}
+			}
+
+		}
+		return result;
+	}
+
+	/**
+	 * Print a standard Map based OSGi header.
+	 * 
+	 * @param exports
+	 *            map { name => Map { attribute|directive => value } }
+	 * @return the clauses
+	 */
+	public static String printClauses(Map<String, Map<String, String>> exports) {
+		return printClauses(exports, false);
+	}
+
+	public static String printClauses(Map<String, Map<String, String>> exports,boolean checkMultipleVersions) {
+		StringBuffer sb = new StringBuffer();
+		String del = "";
+		for (Iterator<String> i = exports.keySet().iterator(); i.hasNext();) {
+			String name = i.next();
+			Map<String, String> clause = exports.get(name);
+
+			// We allow names to be duplicated in the input
+			// by ending them with '~'. This is necessary to use
+			// the package names as keys. However, we remove these
+			// suffixes in the output so that you can set multiple
+			// exports with different attributes.
+			String outname = removeDuplicateMarker(name);
+			sb.append(del);
+			sb.append(outname);
+			printClause(clause,  sb);
+			del = ",";
+		}
+		return sb.toString();
+	}
+
+	public static void printClause(Map<String, String> map, 
+			StringBuffer sb) {
+
+		for (Iterator<String> j = map.keySet().iterator(); j.hasNext();) {
+			String key = j.next();
+
+			// Skip directives we do not recognize
+			if (key.equals(NO_IMPORT_DIRECTIVE) || key.equals(PROVIDE_DIRECTIVE) || key.equals(SPLIT_PACKAGE_DIRECTIVE) || key.equals(FROM_DIRECTIVE))
+				continue;
+
+			String value = ((String) map.get(key)).trim();
+			sb.append(";");
+			sb.append(key);
+			sb.append("=");
+
+			boolean clean = (value.length() >= 2 && value.charAt(0) == '"' && value.charAt(value
+					.length() - 1) == '"') || Verifier.TOKEN.matcher(value).matches();
+			if (!clean)
+				sb.append("\"");
+			sb.append(value);
+			if (!clean)
+				sb.append("\"");
+		}
+	}
+
+	public Macro getReplacer() {
+		if (replacer == null)
+			return replacer = new Macro(this, getMacroDomains());
+		else
+			return replacer;
+	}
+
+	/**
+	 * This should be overridden by subclasses to add extra macro command
+	 * domains on the search list.
+	 * 
+	 * @return
+	 */
+	protected Object[] getMacroDomains() {
+		return new Object[] {};
+	}
+
+	/**
+	 * Return the properties but expand all macros. This always returns a new
+	 * Properties object that can be used in any way.
+	 * 
+	 * @return
+	 */
+	public Properties getFlattenedProperties() {
+		return getReplacer().getFlattenedProperties();
+
+	}
+
+	public void updateModified(long time, String reason) {
+		if (time > lastModified) {
+			lastModified = time;
+		}
+	}
+
+	public long lastModified() {
+		return lastModified;
+	}
+
+	/**
+	 * Add or override a new property.
+	 * 
+	 * @param key
+	 * @param value
+	 */
+	public void setProperty(String key, String value) {
+		checkheader: for (int i = 0; i < headers.length; i++) {
+			if (headers[i].equalsIgnoreCase(value)) {
+				value = headers[i];
+				break checkheader;
+			}
+		}
+		getProperties().put(key, value);
+	}
+
+	/**
+	 * Read a manifest but return a properties object.
+	 * 
+	 * @param in
+	 * @return
+	 * @throws IOException
+	 */
+	public static Properties getManifestAsProperties(InputStream in) throws IOException {
+		Properties p = new Properties();
+		Manifest manifest = new Manifest(in);
+		for (Iterator<Object> it = manifest.getMainAttributes().keySet().iterator(); it.hasNext();) {
+			Attributes.Name key = (Attributes.Name) it.next();
+			String value = manifest.getMainAttributes().getValue(key);
+			p.put(key.toString(), value);
+		}
+		return p;
+	}
+
+	public File getPropertiesFile() {
+		return propertiesFile;
+	}
+
+	public void setFileMustExist(boolean mustexist) {
+		fileMustExist = mustexist;
+	}
+
+	static public String read(InputStream in) throws Exception {
+		InputStreamReader ir = new InputStreamReader(in, "UTF8");
+		StringBuilder sb = new StringBuilder();
+
+		try {
+			char chars[] = new char[1000];
+			int size = ir.read(chars);
+			while (size > 0) {
+				sb.append(chars, 0, size);
+				size = ir.read(chars);
+			}
+		} finally {
+			ir.close();
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * Join a list.
+	 * 
+	 * @param args
+	 * @return
+	 */
+	public static String join(Collection<?> list, String delimeter) {
+		return join(delimeter, list);
+	}
+
+	public static String join(String delimeter, Collection<?>... list) {
+		StringBuilder sb = new StringBuilder();
+		String del = "";
+		for (Collection<?> l : list) {
+			if (list != null) {
+				for (Object item : l) {
+					sb.append(del);
+					sb.append(item);
+					del = delimeter;
+				}
+			}
+		}
+		return sb.toString();
+	}
+
+	public static String join(Object[] list, String delimeter) {
+		if (list == null)
+			return "";
+		StringBuilder sb = new StringBuilder();
+		String del = "";
+		for (Object item : list) {
+			sb.append(del);
+			sb.append(item);
+			del = delimeter;
+		}
+		return sb.toString();
+	}
+
+	public static String join(Collection<?>... list) {
+		return join(",", list);
+	}
+
+	public static <T> String join(T list[]) {
+		return join(list, ",");
+	}
+
+	public static void split(String s, Collection<String> set) {
+
+		String elements[] = s.trim().split(LIST_SPLITTER);
+		for (String element : elements) {
+			if (element.length() > 0)
+				set.add(element);
+		}
+	}
+
+	public static Collection<String> split(String s) {
+		return split(s, LIST_SPLITTER);
+	}
+
+	public static Collection<String> split(String s, String splitter) {
+		if (s != null)
+			s = s.trim();
+		if (s == null || s.trim().length() == 0)
+			return Collections.emptyList();
+
+		return Arrays.asList(s.split(splitter));
+	}
+
+	public static String merge(String... strings) {
+		ArrayList<String> result = new ArrayList<String>();
+		for (String s : strings) {
+			if (s != null)
+				split(s, result);
+		}
+		return join(result);
+	}
+
+	public boolean isExceptions() {
+		return exceptions;
+	}
+
+	public void setExceptions(boolean exceptions) {
+		this.exceptions = exceptions;
+	}
+
+	/**
+	 * Make the file short if it is inside our base directory, otherwise long.
+	 * 
+	 * @param f
+	 * @return
+	 */
+	public String normalize(String f) {
+		if (f.startsWith(base.getAbsolutePath() + "/"))
+			return f.substring(base.getAbsolutePath().length() + 1);
+		else
+			return f;
+	}
+
+	public String normalize(File f) {
+		return normalize(f.getAbsolutePath());
+	}
+
+	public static String removeDuplicateMarker(String key) {
+		int i = key.length() - 1;
+		while (i >= 0 && key.charAt(i) == DUPLICATE_MARKER)
+			--i;
+
+		return key.substring(0, i + 1);
+	}
+
+	public static boolean isDuplicate(String name) {
+		return name.length() > 0 && name.charAt(name.length() - 1) == DUPLICATE_MARKER;
+	}
+
+	public void setTrace(boolean x) {
+		trace = x;
+	}
+
+	static class CL extends URLClassLoader {
+
+		CL() {
+			super(new URL[0], Processor.class.getClassLoader());
+		}
+
+		void add(URL url) {
+			URL urls[] = getURLs();
+			for (URL u : urls) {
+				if (u.equals(url))
+					return;
+			}
+			super.addURL(url);
+		}
+
+		public Class<?> loadClass(String name) throws NoClassDefFoundError {
+			try {
+				Class<?> c = super.loadClass(name);
+				return c;
+			} catch (Throwable t) {
+				StringBuilder sb = new StringBuilder();
+				sb.append(name);
+				sb.append(" not found, parent:  ");
+				sb.append(getParent());
+				sb.append(" urls:");
+				sb.append(Arrays.toString(getURLs()));
+				sb.append(" exception:");
+				sb.append(t);
+				throw new NoClassDefFoundError(sb.toString());
+			}
+		}
+	}
+
+	private CL getLoader() {
+		if (pluginLoader == null) {
+			pluginLoader = new CL();
+		}
+		return pluginLoader;
+	}
+
+	/*
+	 * Check if this is a valid project.
+	 */
+	public boolean exists() {
+		return base != null && base.isDirectory() && propertiesFile != null
+				&& propertiesFile.isFile();
+	}
+
+	public boolean isOk() {
+		return isFailOk() || (getErrors().size() == 0);
+	}
+
+	public boolean isPerfect() {
+		return getErrors().size() == 0 && getWarnings().size() == 0;
+	}
+
+	public void setForceLocal(Collection<String> local) {
+		filter = local;
+	}
+
+	/**
+	 * Answer if the name is a missing plugin's command name. If a bnd file
+	 * contains the command name of a plugin, and that plugin is not available,
+	 * then an error is reported during manifest calculation. This allows the
+	 * plugin to fail to load when it is not needed.
+	 * 
+	 * We first get the plugins to ensure it is properly initialized.
+	 * 
+	 * @param name
+	 * @return
+	 */
+	public boolean isMissingPlugin(String name) {
+		getPlugins();
+		return missingCommand != null && missingCommand.contains(name);
+	}
+
+	/**
+	 * Append two strings to for a path in a ZIP or JAR file. It is guaranteed
+	 * to return a string that does not start, nor ends with a '/', while it is
+	 * properly separated with slashes. Double slashes are properly removed.
+	 * 
+	 * <pre>
+	 *  &quot;/&quot; + &quot;abc/def/&quot; becomes &quot;abc/def&quot;
+	 *  
+	 * &#064;param prefix
+	 * &#064;param suffix
+	 * &#064;return
+	 * 
+	 */
+	public static String appendPath(String... parts) {
+		StringBuilder sb = new StringBuilder();
+		boolean lastSlash = true;
+		for (String part : parts) {
+			for (int i = 0; i < part.length(); i++) {
+				char c = part.charAt(i);
+				if (c == '/') {
+					if (!lastSlash)
+						sb.append('/');
+					lastSlash = true;
+				} else {
+					sb.append(c);
+					lastSlash = false;
+				}
+			}
+			if (!lastSlash & sb.length() > 0) {
+				sb.append('/');
+				lastSlash = true;
+			}
+		}
+		if (lastSlash && sb.length() > 0)
+			sb.deleteCharAt(sb.length() - 1);
+
+		return sb.toString();
+	}
+
+	/**
+	 * Parse the a=b strings and return a map of them.
+	 * 
+	 * @param attrs
+	 * @param clazz
+	 * @return
+	 */
+	public static Map<String, String> doAttrbutes(Object[] attrs, Clazz clazz, Macro macro) {
+		if (attrs == null || attrs.length == 0)
+			return Collections.emptyMap();
+
+		Map<String, String> map = newMap();
+		for (Object a : attrs) {
+			String attr = (String) a;
+			int n = attr.indexOf("=");
+			if (n > 0) {
+				map.put(attr.substring(0, n), macro.process(attr.substring(n + 1)));
+			} else
+				throw new IllegalArgumentException(String.format(
+						"Invalid attribute on package-info.java in %s , %s. Must be <key>=<name> ",
+						clazz, attr));
+		}
+		return map;
+	}
+
+	public static String append(String... strings) {
+		List<String> result = Create.list();
+		for (String s : strings) {
+			result.addAll(split(s));
+		}
+		return join(result);
+	}
+
+	public synchronized Class<?> getClass(String type, File jar) throws Exception {
+		CL cl = getLoader();
+		cl.add(jar.toURI().toURL());
+		return cl.loadClass(type);
+	}
+
+	public boolean isTrace() {
+		return current().trace;
+	}
+
+	public static long getDuration(String tm, long dflt) {
+		if (tm == null)
+			return dflt;
+
+		tm = tm.toUpperCase();
+		TimeUnit unit = TimeUnit.MILLISECONDS;
+		Matcher m = Pattern
+				.compile(
+						"\\s*(\\d+)\\s*(NANOSECONDS|MICROSECONDS|MILLISECONDS|SECONDS|MINUTES|HOURS|DAYS)?")
+				.matcher(tm);
+		if (m.matches()) {
+			long duration = Long.parseLong(tm);
+			String u = m.group(2);
+			if (u != null)
+				unit = TimeUnit.valueOf(u);
+			duration = TimeUnit.MILLISECONDS.convert(duration, unit);
+			return duration;
+		}
+		return dflt;
+	}
+
+	/**
+	 * Generate a random string, which is guaranteed to be a valid Java
+	 * identifier (first character is an ASCII letter, subsequent characters are
+	 * ASCII letters or numbers). Takes an optional parameter for the length of
+	 * string to generate; default is 8 characters.
+	 */
+	public String _random(String[] args) {
+		int numchars = 8;
+		if (args.length > 1) {
+			try {
+				numchars = Integer.parseInt(args[1]);
+			} catch (NumberFormatException e) {
+				throw new IllegalArgumentException(
+						"Invalid character count parameter in ${random} macro.");
+			}
+		}
+
+		if (random == null)
+			random = new Random();
+
+		char[] letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
+		char[] alphanums = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+				.toCharArray();
+
+		char[] array = new char[numchars];
+		for (int i = 0; i < numchars; i++) {
+			char c;
+			if (i == 0)
+				c = letters[random.nextInt(letters.length)];
+			else
+				c = alphanums[random.nextInt(alphanums.length)];
+			array[i] = c;
+		}
+
+		return new String(array);
+	}
+
+	/**
+	 * Set the current command thread. This must be balanced with the
+	 * {@link #end(Processor)} method. The method returns the previous command
+	 * owner or null.
+	 * 
+	 * The command owner will receive all warnings and error reports.
+	 */
+
+	protected Processor beginHandleErrors(String message) {
+		trace("begin %s", message);
+		Processor previous = current.get();
+		current.set(this);
+		return previous;
+	}
+
+	/**
+	 * End a command. Will restore the previous command owner.
+	 * 
+	 * @param previous
+	 */
+	protected void endHandleErrors(Processor previous) {
+		trace("end");
+		current.set(previous);
+	}
+
+	public static Executor getExecutor() {
+		return executor;
+	}
+
+	/**
+	 * These plugins are added to the total list of plugins. The separation
+	 * is necessary because the list of plugins is refreshed now and then
+	 * so we need to be able to add them at any moment in time.
+	 * 
+	 * @param plugin
+	 */
+	public synchronized void addBasicPlugin(Object plugin) {
+		basicPlugins.add(plugin);
+		if (plugins != null)
+			plugins.add(plugin);
+	}
+
+	public synchronized void removeBasicPlugin(Object plugin) {
+		basicPlugins.remove(plugin);
+		if (plugins != null)
+			plugins.remove(plugin);
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Resource.java b/bundleplugin/src/main/java/aQute/lib/osgi/Resource.java
new file mode 100644
index 0000000..85756d5
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Resource.java
@@ -0,0 +1,11 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+
+public interface Resource {
+	InputStream openInputStream() throws Exception ;
+	void write(OutputStream out) throws Exception;
+	long lastModified();
+	void setExtra(String extra);
+	String getExtra();
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/TagResource.java b/bundleplugin/src/main/java/aQute/lib/osgi/TagResource.java
new file mode 100644
index 0000000..a793326
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/TagResource.java
@@ -0,0 +1,56 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+
+import aQute.lib.io.*;
+import aQute.lib.tag.*;
+
+public class TagResource implements Resource {
+	final Tag	tag;
+	String		extra;
+
+	public TagResource(Tag tag) {
+		this.tag = tag;
+	}
+
+	public InputStream openInputStream() throws Exception {
+		final PipedInputStream pin = new PipedInputStream();
+		final PipedOutputStream pout = new PipedOutputStream(pin);
+		Processor.getExecutor().execute(new Runnable() {
+			public void run() {
+				try {
+					write(pout);
+				} catch (Exception e) {
+					e.printStackTrace();
+					// ignore
+				}
+				IO.close(pout);
+			}
+		});
+		return pin;
+	}
+
+	public void write(OutputStream out) throws UnsupportedEncodingException {
+		OutputStreamWriter ow = new OutputStreamWriter(out, "UTF-8");
+		PrintWriter pw = new PrintWriter(ow);
+		pw.println("<?xml version='1.1'?>");
+		try {
+			tag.print(0, pw);
+		} finally {
+			pw.flush();
+		}
+	}
+
+	public long lastModified() {
+		return 0;
+	}
+
+	public void setExtra(String extra) {
+		this.extra = extra;
+	}
+
+	public String getExtra() {
+		return extra;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/URLResource.java b/bundleplugin/src/main/java/aQute/lib/osgi/URLResource.java
new file mode 100644
index 0000000..9f0af59
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/URLResource.java
@@ -0,0 +1,37 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+import java.net.*;
+
+public class URLResource implements Resource {
+	URL	url;
+	String	extra;
+	
+	public URLResource(URL url) {
+		this.url = url;
+	}
+
+	public InputStream openInputStream() throws IOException {
+		return url.openStream();
+	}
+
+	public String toString() {
+		return ":" + url.getPath() + ":";
+	}
+
+	public void write(OutputStream out) throws Exception {
+		FileResource.copy(this, out);
+	}
+
+	public long lastModified() {
+		return -1;
+	}
+
+	public String getExtra() {
+		return extra;
+	}
+
+	public void setExtra(String extra) {
+		this.extra = extra;
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/Verifier.java b/bundleplugin/src/main/java/aQute/lib/osgi/Verifier.java
new file mode 100644
index 0000000..00cd67e
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Verifier.java
@@ -0,0 +1,959 @@
+package aQute.lib.osgi;
+
+import java.util.*;
+import java.util.jar.*;
+import java.util.regex.*;
+
+import aQute.libg.qtokens.*;
+
+public class Verifier extends Analyzer {
+
+    Jar                              dot;
+    Manifest                         manifest;
+    Map<String, Map<String, String>> referred  = newHashMap();
+    Map<String, Map<String, String>> contained = newHashMap();
+    Map<String, Set<String>>         uses      = newHashMap();
+    Map<String, Map<String, String>> mimports;
+    Map<String, Map<String, String>> mdynimports;
+    Map<String, Map<String, String>> mexports;
+    List<Jar>                        bundleClasspath;
+    Map<String, Map<String, String>> ignore    = newHashMap();                                    // Packages
+    // to
+    // ignore
+
+    Map<String, Clazz>               classSpace;
+    boolean                          r3;
+    boolean                          usesRequire;
+    boolean                          fragment;
+    Attributes                       main;
+
+    final static Pattern             EENAME    = Pattern
+                                                       .compile("CDC-1\\.0/Foundation-1\\.0"
+                                                               + "|CDC-1\\.1/Foundation-1\\.1"
+                                                               + "|OSGi/Minimum-1\\.[1-9]"
+                                                               + "|JRE-1\\.1"
+                                                               + "|J2SE-1\\.2"
+                                                               + "|J2SE-1\\.3"
+                                                               + "|J2SE-1\\.4"
+                                                               + "|J2SE-1\\.5"
+                                                               + "|JavaSE-1\\.6"
+                                                               + "|JavaSE-1\\.7"
+                                                               + "|PersonalJava-1\\.1"
+                                                               + "|PersonalJava-1\\.2"
+                                                               + "|CDC-1\\.0/PersonalBasis-1\\.0"
+                                                               + "|CDC-1\\.0/PersonalJava-1\\.0");
+
+    final static int                 V1_1      = 45;
+    final static int                 V1_2      = 46;
+    final static int                 V1_3      = 47;
+    final static int                 V1_4      = 48;
+    final static int                 V1_5      = 49;
+    final static int                 V1_6      = 50;
+    final static int                 V1_7      = 51;
+
+    static class EE {
+        String name;
+        int    target;
+
+        EE(String name, int source, int target) {
+            this.name = name;
+            this.target = target;
+        }
+    }
+
+    final static EE[]           ees                            = {
+            new EE("CDC-1.0/Foundation-1.0", V1_3, V1_1),
+            new EE("CDC-1.1/Foundation-1.1", V1_3, V1_2),
+            new EE("OSGi/Minimum-1.0", V1_3, V1_1),
+            new EE("OSGi/Minimum-1.1", V1_3, V1_2),
+            new EE("JRE-1.1", V1_1, V1_1), //
+            new EE("J2SE-1.2", V1_2, V1_1), //
+            new EE("J2SE-1.3", V1_3, V1_1), //
+            new EE("J2SE-1.4", V1_3, V1_2), //
+            new EE("J2SE-1.5", V1_5, V1_5), //
+            new EE("JavaSE-1.6", V1_6, V1_6),
+            new EE("PersonalJava-1.1", V1_1, V1_1),
+            new EE("PersonalJava-1.2", V1_1, V1_1),
+            new EE("CDC-1.0/PersonalBasis-1.0", V1_3, V1_1),
+            new EE("CDC-1.0/PersonalJava-1.0", V1_3, V1_1),
+            new EE("CDC-1.1/PersonalBasis-1.1", V1_3, V1_2),
+            new EE("CDC-1.1/PersonalJava-1.1", V1_3, V1_2)    };
+
+    final static Pattern        BUNDLEMANIFESTVERSION          = Pattern
+                                                                       .compile("2");
+    public final static String  SYMBOLICNAME_STRING            = "[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*";
+    public final static Pattern SYMBOLICNAME                   = Pattern
+                                                                       .compile(SYMBOLICNAME_STRING);
+
+    public final static String  VERSION_STRING                 = "[0-9]+(\\.[0-9]+(\\.[0-9]+(\\.[0-9A-Za-z_-]+)?)?)?";
+    public final static Pattern VERSION                        = Pattern
+                                                                       .compile(VERSION_STRING);
+    final static Pattern        FILTEROP                       = Pattern
+                                                                       .compile("=|<=|>=|~=");
+    public final static Pattern VERSIONRANGE                   = Pattern
+                                                                       .compile("((\\(|\\[)"
+                                                                               + VERSION_STRING
+                                                                               + ","
+                                                                               + VERSION_STRING
+                                                                               + "(\\]|\\)))|"
+                                                                               + VERSION_STRING);
+    final static Pattern        FILE                           = Pattern
+                                                                       .compile("/?[^/\"\n\r\u0000]+(/[^/\"\n\r\u0000]+)*");
+    final static Pattern        WILDCARDPACKAGE                = Pattern
+                                                                       .compile("((\\p{Alnum}|_)+(\\.(\\p{Alnum}|_)+)*(\\.\\*)?)|\\*");
+    public final static Pattern ISO639                         = Pattern
+                                                                       .compile("[A-Z][A-Z]");
+    public final static Pattern HEADER_PATTERN                 = Pattern
+                                                                       .compile("[A-Za-z0-9][-a-zA-Z0-9_]+");
+    public final static Pattern TOKEN                          = Pattern
+                                                                       .compile("[-a-zA-Z0-9_]+");
+
+    public final static Pattern NUMBERPATTERN                  = Pattern
+                                                                       .compile("\\d+");
+    public final static Pattern PATHPATTERN                    = Pattern
+                                                                       .compile(".*");
+    public final static Pattern FQNPATTERN                     = Pattern
+                                                                       .compile(".*");
+    public final static Pattern URLPATTERN                     = Pattern
+                                                                       .compile(".*");
+    public final static Pattern ANYPATTERN                     = Pattern
+                                                                       .compile(".*");
+    public final static Pattern FILTERPATTERN                  = Pattern
+                                                                       .compile(".*");
+    public final static Pattern TRUEORFALSEPATTERN             = Pattern
+                                                                       .compile("true|false|TRUE|FALSE");
+    public static final Pattern WILDCARDNAMEPATTERN            = Pattern
+                                                                       .compile(".*");
+    public static final Pattern BUNDLE_ACTIVATIONPOLICYPATTERN = Pattern
+                                                                       .compile("lazy");
+
+    public final static String  EES[]                          = {
+            "CDC-1.0/Foundation-1.0", "CDC-1.1/Foundation-1.1",
+            "OSGi/Minimum-1.0", "OSGi/Minimum-1.1", "OSGi/Minimum-1.2",
+            "JRE-1.1", "J2SE-1.2", "J2SE-1.3", "J2SE-1.4", "J2SE-1.5",
+            "JavaSE-1.6", "JavaSE-1.7", "PersonalJava-1.1", "PersonalJava-1.2",
+            "CDC-1.0/PersonalBasis-1.0", "CDC-1.0/PersonalJava-1.0" };
+
+    public final static String  OSNAMES[]                      = {
+            "AIX", // IBM
+            "DigitalUnix", // Compaq
+            "Embos", // Segger Embedded Software Solutions
+            "Epoc32", // SymbianOS Symbian OS
+            "FreeBSD", // Free BSD
+            "HPUX", // hp-ux Hewlett Packard
+            "IRIX", // Silicon Graphics
+            "Linux", // Open source
+            "MacOS", // Apple
+            "NetBSD", // Open source
+            "Netware", // Novell
+            "OpenBSD", // Open source
+            "OS2", // OS/2 IBM
+            "QNX", // procnto QNX
+            "Solaris", // Sun (almost an alias of SunOS)
+            "SunOS", // Sun Microsystems
+            "VxWorks", // WindRiver Systems
+            "Windows95", "Win32", "Windows98", "WindowsNT", "WindowsCE",
+            "Windows2000", // Win2000
+            "Windows2003", // Win2003
+            "WindowsXP", "WindowsVista",                      };
+
+    public final static String  PROCESSORNAMES[]               = { "68k", // Motorola
+            // 68000
+            "ARM_LE", // Intel Strong ARM. Deprecated because it does not
+            // specify the endianness. See the following two rows.
+            "arm_le", // Intel Strong ARM Little Endian mode
+            "arm_be", // Intel String ARM Big Endian mode
+            "Alpha", //
+            "ia64n",// Hewlett Packard 32 bit
+            "ia64w",// Hewlett Packard 64 bit mode
+            "Ignite", // psc1k PTSC
+            "Mips", // SGI
+            "PArisc", // Hewlett Packard
+            "PowerPC", // power ppc Motorola/IBM Power PC
+            "Sh4", // Hitachi
+            "Sparc", // SUN
+            "Sparcv9", // SUN
+            "S390", // IBM Mainframe 31 bit
+            "S390x", // IBM Mainframe 64-bit
+            "V850E", // NEC V850E
+            "x86", // pentium i386
+            "i486", // i586 i686 Intel& AMD 32 bit
+            "x86-64",                                         };
+
+    Properties                  properties;
+
+    public Verifier(Jar jar) throws Exception {
+        this(jar, null);
+    }
+
+    public Verifier(Jar jar, Properties properties) throws Exception {
+        this.dot = jar;
+        this.properties = properties;
+        this.manifest = jar.getManifest();
+        if (manifest == null) {
+            manifest = new Manifest();
+            error("This file contains no manifest and is therefore not a bundle");
+        }
+        main = this.manifest.getMainAttributes();
+        verifyHeaders(main);
+        r3 = getHeader(Analyzer.BUNDLE_MANIFESTVERSION) == null;
+        usesRequire = getHeader(Analyzer.REQUIRE_BUNDLE) != null;
+        fragment = getHeader(Analyzer.FRAGMENT_HOST) != null;
+
+        bundleClasspath = getBundleClassPath();
+        mimports = parseHeader(manifest.getMainAttributes().getValue(
+                Analyzer.IMPORT_PACKAGE));
+        mdynimports = parseHeader(manifest.getMainAttributes().getValue(
+                Analyzer.DYNAMICIMPORT_PACKAGE));
+        mexports = parseHeader(manifest.getMainAttributes().getValue(
+                Analyzer.EXPORT_PACKAGE));
+
+        ignore = parseHeader(manifest.getMainAttributes().getValue(
+                Analyzer.IGNORE_PACKAGE));
+    }
+
+    public Verifier() {
+        // TODO Auto-generated constructor stub
+    }
+
+    private void verifyHeaders(Attributes main) {
+        for (Object element : main.keySet()) {
+            Attributes.Name header = (Attributes.Name) element;
+            String h = header.toString();
+            if (!HEADER_PATTERN.matcher(h).matches())
+                error("Invalid Manifest header: " + h + ", pattern="
+                        + HEADER_PATTERN);
+        }
+    }
+
+    private List<Jar> getBundleClassPath() {
+        List<Jar> list = newList();
+        String bcp = getHeader(Analyzer.BUNDLE_CLASSPATH);
+        if (bcp == null) {
+            list.add(dot);
+        } else {
+            Map<String, Map<String, String>> entries = parseHeader(bcp);
+            for (Map.Entry<String, Map<String, String>> ex : entries.entrySet()) {
+                String jarOrDir = ex.getKey();
+                if (jarOrDir.equals(".")) {
+                    list.add(dot);
+                } else {
+                    if (jarOrDir.equals("/"))
+                        jarOrDir = "";
+                    if (jarOrDir.endsWith("/")) {
+                        error("Bundle-Classpath directory must not end with a slash: "
+                                + jarOrDir);
+                        jarOrDir = jarOrDir.substring(0, jarOrDir.length() - 1);
+                    }
+
+                    Resource resource = dot.getResource(jarOrDir);
+                    if (resource != null) {
+                        try {
+                            Jar sub = new Jar(jarOrDir);
+                            addClose(sub);
+                            EmbeddedResource.build(sub, resource);
+                            if (!jarOrDir.endsWith(".jar"))
+                                warning("Valid JAR file on Bundle-Classpath does not have .jar extension: "
+                                        + jarOrDir);
+                            list.add(sub);
+                        } catch (Exception e) {
+                            error("Invalid embedded JAR file on Bundle-Classpath: "
+                                    + jarOrDir + ", " + e);
+                        }
+                    } else if (dot.getDirectories().containsKey(jarOrDir)) {
+                        if (r3)
+                            error("R3 bundles do not support directories on the Bundle-ClassPath: "
+                                    + jarOrDir);
+
+                        try {
+                            Jar sub = new Jar(jarOrDir);
+                            addClose(sub);
+                            for (Map.Entry<String, Resource> entry : dot
+                                    .getResources().entrySet()) {
+                                if (entry.getKey().startsWith(jarOrDir))
+                                    sub.putResource(entry.getKey().substring(
+                                            jarOrDir.length() + 1), entry
+                                            .getValue());
+                            }
+                            list.add(sub);
+                        } catch (Exception e) {
+                            error("Invalid embedded directory file on Bundle-Classpath: "
+                                    + jarOrDir + ", " + e);
+                        }
+                    } else {
+                        // Map<String, String> info = ex.getValue();
+                        // if (! "optional".equals(
+                        // info.get(RESOLUTION_DIRECTIVE)))
+                        // warning("Cannot find a file or directory for
+                        // Bundle-Classpath entry: %s",
+                        // jarOrDir);
+                    }
+                }
+            }
+        }
+        return list;
+    }
+
+    /*
+     * Bundle-NativeCode ::= nativecode ( ',' nativecode )* ( ’,’ optional) ?
+     * nativecode ::= path ( ';' path )* // See 1.4.2 ( ';' parameter )+
+     * optional ::= ’*’
+     */
+    public void verifyNative() {
+        String nc = getHeader("Bundle-NativeCode");
+        doNative(nc);
+    }
+
+    public void doNative(String nc) {
+        if (nc != null) {
+            QuotedTokenizer qt = new QuotedTokenizer(nc, ",;=", false);
+            char del;
+            do {
+                do {
+                    String name = qt.nextToken();
+                    if (name == null) {
+                        error("Can not parse name from bundle native code header: "
+                                + nc);
+                        return;
+                    }
+                    del = qt.getSeparator();
+                    if (del == ';') {
+                        if (dot != null && !dot.exists(name)) {
+                            error("Native library not found in JAR: " + name);
+                        }
+                    } else {
+                        String value = null;
+                        if (del == '=')
+                            value = qt.nextToken();
+
+                        String key = name.toLowerCase();
+                        if (key.equals("osname")) {
+                            // ...
+                        } else if (key.equals("osversion")) {
+                            // verify version range
+                            verify(value, VERSIONRANGE);
+                        } else if (key.equals("language")) {
+                            verify(value, ISO639);
+                        } else if (key.equals("processor")) {
+                            // verify(value, PROCESSORS);
+                        } else if (key.equals("selection-filter")) {
+                            // verify syntax filter
+                            verifyFilter(value);
+                        } else if (name.equals("*") && value == null) {
+                            // Wildcard must be at end.
+                            if (qt.nextToken() != null)
+                                error("Bundle-Native code header may only END in wildcard: nc");
+                        } else {
+                            warning("Unknown attribute in native code: " + name
+                                    + "=" + value);
+                        }
+                        del = qt.getSeparator();
+                    }
+                } while (del == ';');
+            } while (del == ',');
+        }
+    }
+
+    public boolean verifyFilter(String value) {
+        try {
+            verifyFilter(value, 0);
+            return true;
+        } catch (Exception e) {
+            error("Not a valid filter: " + value + e.getMessage());
+            return false;
+        }
+    }
+
+    private void verifyActivator() {
+        String bactivator = getHeader("Bundle-Activator");
+        if (bactivator != null) {
+            Clazz cl = loadClass(bactivator);
+            if (cl == null) {
+                int n = bactivator.lastIndexOf('.');
+                if (n > 0) {
+                    String pack = bactivator.substring(0, n);
+                    if (mimports.containsKey(pack))
+                        return;
+                    error("Bundle-Activator not found on the bundle class path nor in imports: "
+                            + bactivator);
+                } else
+                    error("Activator uses default package and is not local (default package can not be imported): "
+                            + bactivator);
+            }
+        }
+    }
+
+    private Clazz loadClass(String className) {
+        String path = className.replace('.', '/') + ".class";
+        return (Clazz) classSpace.get(path);
+    }
+
+    private void verifyComponent() {
+        String serviceComponent = getHeader("Service-Component");
+        if (serviceComponent != null) {
+            Map<String, Map<String, String>> map = parseHeader(serviceComponent);
+            for (String component : map.keySet()) {
+                if (component.indexOf("*") < 0 && !dot.exists(component)) {
+                    error("Service-Component entry can not be located in JAR: "
+                            + component);
+                } else {
+                    // validate component ...
+                }
+            }
+        }
+    }
+
+    public void info() {
+        System.out.println("Refers                           : " + referred);
+        System.out.println("Contains                         : " + contained);
+        System.out.println("Manifest Imports                 : " + mimports);
+        System.out.println("Manifest Exports                 : " + mexports);
+    }
+
+    /**
+     * Invalid exports are exports mentioned in the manifest but not found on
+     * the classpath. This can be calculated with: exports - contains.
+     * 
+     * Unfortunately, we also must take duplicate names into account. These
+     * duplicates are of course no erroneous.
+     */
+    private void verifyInvalidExports() {
+        Set<String> invalidExport = newSet(mexports.keySet());
+        invalidExport.removeAll(contained.keySet());
+
+        // We might have duplicate names that are marked for it. These
+        // should not be counted. Should we test them against the contained
+        // set? Hmm. If someone wants to hang himself by using duplicates than
+        // I guess he can go ahead ... This is not a recommended practice
+        for (Iterator<String> i = invalidExport.iterator(); i.hasNext();) {
+            String pack = i.next();
+            if (isDuplicate(pack))
+                i.remove();
+        }
+
+        if (!invalidExport.isEmpty())
+            error("Exporting packages that are not on the Bundle-Classpath"
+                    + bundleClasspath + ": " + invalidExport);
+    }
+
+    /**
+     * Invalid imports are imports that we never refer to. They can be
+     * calculated by removing the refered packages from the imported packages.
+     * This leaves packages that the manifest imported but that we never use.
+     */
+    private void verifyInvalidImports() {
+        Set<String> invalidImport = newSet(mimports.keySet());
+        invalidImport.removeAll(referred.keySet());
+        // TODO Added this line but not sure why it worked before ...
+        invalidImport.removeAll(contained.keySet());
+        String bactivator = getHeader(Analyzer.BUNDLE_ACTIVATOR);
+        if (bactivator != null) {
+            int n = bactivator.lastIndexOf('.');
+            if (n > 0) {
+                invalidImport.remove(bactivator.substring(0, n));
+            }
+        }
+        if (isPedantic() && !invalidImport.isEmpty())
+            warning("Importing packages that are never refered to by any class on the Bundle-Classpath"
+                    + bundleClasspath + ": " + invalidImport);
+    }
+
+    /**
+     * Check for unresolved imports. These are referals that are not imported by
+     * the manifest and that are not part of our bundle classpath. The are
+     * calculated by removing all the imported packages and contained from the
+     * refered packages.
+     */
+    private void verifyUnresolvedReferences() {
+        Set<String> unresolvedReferences = new TreeSet<String>(referred
+                .keySet());
+        unresolvedReferences.removeAll(mimports.keySet());
+        unresolvedReferences.removeAll(contained.keySet());
+
+        // Remove any java.** packages.
+        for (Iterator<String> p = unresolvedReferences.iterator(); p.hasNext();) {
+            String pack = p.next();
+            if (pack.startsWith("java.") || ignore.containsKey(pack))
+                p.remove();
+            else {
+                // Remove any dynamic imports
+                if (isDynamicImport(pack))
+                    p.remove();
+            }
+        }
+
+        if (!unresolvedReferences.isEmpty()) {
+            // Now we want to know the
+            // classes that are the culprits
+            Set<String> culprits = new HashSet<String>();
+            for (Clazz clazz : classSpace.values()) {
+                if (hasOverlap(unresolvedReferences, clazz.getReferred()))
+                    culprits.add(clazz.getPath());
+            }
+
+            error("Unresolved references to " + unresolvedReferences
+                    + " by class(es) on the Bundle-Classpath" + bundleClasspath
+                    + ": " + culprits);
+        }
+    }
+
+    /**
+     * @param p
+     * @param pack
+     */
+    private boolean isDynamicImport(String pack) {
+        for (String pattern : mdynimports.keySet()) {
+            // Wildcard?
+            if (pattern.equals("*"))
+                return true; // All packages can be dynamically imported
+
+            if (pattern.endsWith(".*")) {
+                pattern = pattern.substring(0, pattern.length() - 2);
+                if (pack.startsWith(pattern)
+                        && (pack.length() == pattern.length() || pack
+                                .charAt(pattern.length()) == '.'))
+                    return true;
+            } else {
+                if (pack.equals(pattern))
+                    return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasOverlap(Set<?> a, Set<?> b) {
+        for (Iterator<?> i = a.iterator(); i.hasNext();) {
+            if (b.contains(i.next()))
+                return true;
+        }
+        return false;
+    }
+
+    public void verify() throws Exception {
+        if (classSpace == null)
+            classSpace = analyzeBundleClasspath(dot,
+                    parseHeader(getHeader(Analyzer.BUNDLE_CLASSPATH)),
+                    contained, referred, uses);
+        
+        
+        verifyDirectives("Export-Package", "uses:|mandatory:|include:|exclude:|" + IMPORT_DIRECTIVE);
+        verifyDirectives("Import-Package", "resolution:");
+        verifyDirectives("Require-Bundle", "visibility:|resolution:");
+        verifyDirectives("Fragment-Host", "resolution:");
+        verifyDirectives("Provide-Capability", "effective:|uses:");
+        verifyDirectives("Require-Capability", "effective:|resolve:|filter:");
+        verifyDirectives("Bundle-SymbolicName", "singleton:|fragment-attachment:|mandatory:");
+        
+        
+        verifyManifestFirst();
+        verifyActivator();
+        verifyActivationPolicy();
+        verifyComponent();
+        verifyNative();
+        verifyInvalidExports();
+        verifyInvalidImports();
+        verifyUnresolvedReferences();
+        verifySymbolicName();
+        verifyListHeader("Bundle-RequiredExecutionEnvironment", EENAME, false);
+        verifyHeader("Bundle-ManifestVersion", BUNDLEMANIFESTVERSION, false);
+        verifyHeader("Bundle-Version", VERSION, true);
+        verifyListHeader("Bundle-Classpath", FILE, false);
+        verifyDynamicImportPackage();
+        verifyBundleClasspath();
+        verifyUses();
+        if (usesRequire) {
+            if (!getErrors().isEmpty()) {
+                getWarnings()
+                        .add(
+                                0,
+                                "Bundle uses Require Bundle, this can generate false errors because then not enough information is available without the required bundles");
+            }
+        }
+    }
+
+    /**
+     * Verify if the header does not contain any other directives
+     * 
+     * @param header
+     * @param directives
+     */
+    private void verifyDirectives(String header, String directives) {
+    	Pattern pattern = Pattern.compile(directives);
+    	Map<String,Map<String,String>> map = parseHeader(manifest.getMainAttributes().getValue(header));
+    	for ( Map.Entry<String, Map<String,String>> entry : map.entrySet()) {
+    		for ( String key : entry.getValue().keySet()) {
+    			if ( key.endsWith(":")) {
+    				if ( ! key.startsWith("x-")) {
+    					Matcher m = pattern.matcher(key);
+    					if ( m.matches())
+    						continue;
+    					
+    					warning("Unknown directive %s in %s, allowed directives are %s, and 'x-*'.", key, header, directives.replace('|', ','));
+    				}
+    			}
+    		}
+    	}
+	}
+
+	/**
+     * Verify the use clauses
+     */
+    private void verifyUses() {
+    }
+
+    public boolean verifyActivationPolicy() {
+        String policy = getHeader(Constants.BUNDLE_ACTIVATIONPOLICY);
+        if (policy == null)
+            return true;
+
+        return verifyActivationPolicy(policy);
+    }
+
+    public boolean verifyActivationPolicy(String policy) {
+        Map<String, Map<String, String>> map = parseHeader(policy);
+        if (map.size() == 0)
+            warning("Bundle-ActivationPolicy is set but has no argument %s",
+                    policy);
+        else if (map.size() > 1)
+            warning("Bundle-ActivationPolicy has too many arguments %s", policy);
+        else {
+            Map<String, String> s = map.get("lazy");
+            if (s == null)
+                warning(
+                        "Bundle-ActivationPolicy set but is not set to lazy: %s",
+                        policy);
+            else
+                return true;
+        }
+
+        return false;
+    }
+
+    public void verifyBundleClasspath() {
+        Map<String, Map<String, String>> bcp = parseHeader(getHeader(Analyzer.BUNDLE_CLASSPATH));
+        if (bcp.isEmpty() || bcp.containsKey("."))
+            return;
+
+        for ( String path : bcp.keySet() ) {
+            if ( path.endsWith("/"))
+                error("A Bundle-ClassPath entry must not end with '/': %s", path);
+            
+            if ( dot.getDirectories().containsKey(path))
+                // We assume that any classes are in a directory
+                // and therefore do not care when the bundle is included
+                return;
+        }
+        
+        for (String path : dot.getResources().keySet()) {
+            if (path.endsWith(".class")) {
+                warning("The Bundle-Classpath does not contain the actual bundle JAR (as specified with '.' in the Bundle-Classpath) but the JAR does contain classes. Is this intentional?");
+                return;
+            }
+        }
+    }
+
+    /**
+     * <pre>
+     *          DynamicImport-Package ::= dynamic-description
+     *              ( ',' dynamic-description )*
+     *              
+     *          dynamic-description::= wildcard-names ( ';' parameter )*
+     *          wildcard-names ::= wildcard-name ( ';' wildcard-name )*
+     *          wildcard-name ::= package-name 
+     *                         | ( package-name '.*' ) // See 1.4.2
+     *                         | '*'
+     * </pre>
+     */
+    private void verifyDynamicImportPackage() {
+        verifyListHeader("DynamicImport-Package", WILDCARDPACKAGE, true);
+        String dynamicImportPackage = getHeader("DynamicImport-Package");
+        if (dynamicImportPackage == null)
+            return;
+
+        Map<String, Map<String, String>> map = parseHeader(dynamicImportPackage);
+        for (String name : map.keySet()) {
+            name = name.trim();
+            if (!verify(name, WILDCARDPACKAGE))
+                error("DynamicImport-Package header contains an invalid package name: "
+                        + name);
+
+            Map<String, String> sub = map.get(name);
+            if (r3 && sub.size() != 0) {
+                error("DynamicPackage-Import has attributes on import: "
+                        + name
+                        + ". This is however, an <=R3 bundle and attributes on this header were introduced in R4. ");
+            }
+        }
+    }
+
+    private void verifyManifestFirst() {
+        if (!dot.manifestFirst) {
+            error("Invalid JAR stream: Manifest should come first to be compatible with JarInputStream, it was not");
+        }
+    }
+
+    private void verifySymbolicName() {
+        Map<String, Map<String, String>> bsn = parseHeader(getHeader(Analyzer.BUNDLE_SYMBOLICNAME));
+        if (!bsn.isEmpty()) {
+            if (bsn.size() > 1)
+                error("More than one BSN specified " + bsn);
+
+            String name = (String) bsn.keySet().iterator().next();
+            if (!SYMBOLICNAME.matcher(name).matches()) {
+                error("Symbolic Name has invalid format: " + name);
+            }
+        }
+    }
+
+    /**
+     * <pre>
+     *         filter ::= ’(’ filter-comp ’)’
+     *         filter-comp ::= and | or | not | operation
+     *         and ::= ’&amp;’ filter-list
+     *         or ::= ’|’ filter-list
+     *         not ::= ’!’ filter
+     *         filter-list ::= filter | filter filter-list
+     *         operation ::= simple | present | substring
+     *         simple ::= attr filter-type value
+     *         filter-type ::= equal | approx | greater | less
+     *         equal ::= ’=’
+     *         approx ::= ’&tilde;=’
+     *         greater ::= ’&gt;=’
+     *         less ::= ’&lt;=’
+     *         present ::= attr ’=*’
+     *         substring ::= attr ’=’ initial any final
+     *         inital ::= () | value
+     *         any ::= ’*’ star-value
+     *         star-value ::= () | value ’*’ star-value
+     *         final ::= () | value
+     *         value ::= &lt;see text&gt;
+     * </pre>
+     * 
+     * @param expr
+     * @param index
+     * @return
+     */
+
+    public static int verifyFilter(String expr, int index) {
+        try {
+            while (Character.isWhitespace(expr.charAt(index)))
+                index++;
+
+            if (expr.charAt(index) != '(')
+                throw new IllegalArgumentException(
+                        "Filter mismatch: expected ( at position " + index
+                                + " : " + expr);
+
+            index++; // skip (
+
+            while (Character.isWhitespace(expr.charAt(index)))
+                index++;
+
+            switch (expr.charAt(index)) {
+            case '!':
+                index++; // skip !
+                while (Character.isWhitespace(expr.charAt(index)))
+                    index++;
+
+                if (expr.charAt(index) != '(')
+                    throw new IllegalArgumentException(
+                            "Filter mismatch: ! (not) must have one sub expression "
+                                    + index + " : " + expr);
+                while (Character.isWhitespace(expr.charAt(index)))
+                    index++;
+
+                index = verifyFilter(expr, index);
+                while (Character.isWhitespace(expr.charAt(index)))
+                    index++;
+                if (expr.charAt(index) != ')')
+                    throw new IllegalArgumentException(
+                            "Filter mismatch: expected ) at position " + index
+                                    + " : " + expr);
+                return index + 1;
+
+            case '&':
+            case '|':
+                index++; // skip operator
+                while (Character.isWhitespace(expr.charAt(index)))
+                    index++;
+                while (expr.charAt(index) == '(') {
+                    index = verifyFilter(expr, index);
+                    while (Character.isWhitespace(expr.charAt(index)))
+                        index++;
+                }
+
+                if (expr.charAt(index) != ')')
+                    throw new IllegalArgumentException(
+                            "Filter mismatch: expected ) at position " + index
+                                    + " : " + expr);
+                return index + 1; // skip )
+
+            default:
+                index = verifyFilterOperation(expr, index);
+                if (expr.charAt(index) != ')')
+                    throw new IllegalArgumentException(
+                            "Filter mismatch: expected ) at position " + index
+                                    + " : " + expr);
+                return index + 1;
+            }
+        } catch (IndexOutOfBoundsException e) {
+            throw new IllegalArgumentException(
+                    "Filter mismatch: early EOF from " + index);
+        }
+    }
+
+    static private int verifyFilterOperation(String expr, int index) {
+        StringBuffer sb = new StringBuffer();
+        while ("=><~()".indexOf(expr.charAt(index)) < 0) {
+            sb.append(expr.charAt(index++));
+        }
+        String attr = sb.toString().trim();
+        if (attr.length() == 0)
+            throw new IllegalArgumentException(
+                    "Filter mismatch: attr at index " + index + " is 0");
+        sb = new StringBuffer();
+        while ("=><~".indexOf(expr.charAt(index)) >= 0) {
+            sb.append(expr.charAt(index++));
+        }
+        String operator = sb.toString();
+        if (!verify(operator, FILTEROP))
+            throw new IllegalArgumentException(
+                    "Filter error, illegal operator " + operator + " at index "
+                            + index);
+
+        sb = new StringBuffer();
+        while (")".indexOf(expr.charAt(index)) < 0) {
+            switch (expr.charAt(index)) {
+            case '\\':
+                if ("\\)(*".indexOf(expr.charAt(index + 1)) >= 0 )
+                    index++;
+                else
+                    throw new IllegalArgumentException(
+                            "Filter error, illegal use of backslash at index "
+                                    + index
+                                    + ". Backslash may only be used before * or () or \\");
+            }
+            sb.append(expr.charAt(index++));
+        }
+        return index;
+    }
+
+    private String getHeader(String string) {
+        return main.getValue(string);
+    }
+
+    private boolean verifyHeader(String name, Pattern regex, boolean error) {
+        String value = manifest.getMainAttributes().getValue(name);
+        if (value == null)
+            return false;
+
+        QuotedTokenizer st = new QuotedTokenizer(value.trim(), ",");
+        for (Iterator<String> i = st.getTokenSet().iterator(); i.hasNext();) {
+            if (!verify((String) i.next(), regex)) {
+                String msg = "Invalid value for " + name + ", " + value
+                        + " does not match " + regex.pattern();
+                if (error)
+                    error(msg);
+                else
+                    warning(msg);
+            }
+        }
+        return true;
+    }
+
+    static private boolean verify(String value, Pattern regex) {
+        return regex.matcher(value).matches();
+    }
+
+    private boolean verifyListHeader(String name, Pattern regex, boolean error) {
+        String value = manifest.getMainAttributes().getValue(name);
+        if (value == null)
+            return false;
+
+        Map<String, Map<String, String>> map = parseHeader(value);
+        for (String header : map.keySet()) {
+            if (!regex.matcher(header).matches()) {
+                String msg = "Invalid value for " + name + ", " + value
+                        + " does not match " + regex.pattern();
+                if (error)
+                    error(msg);
+                else
+                    warning(msg);
+            }
+        }
+        return true;
+    }
+
+    public String getProperty(String key, String deflt) {
+        if (properties == null)
+            return deflt;
+        return properties.getProperty(key, deflt);
+    }
+
+    public void setClassSpace(Map<String, Clazz> classspace,
+            Map<String, Map<String, String>> contained,
+            Map<String, Map<String, String>> referred,
+            Map<String, Set<String>> uses) {
+        this.classSpace = classspace;
+        this.contained = contained;
+        this.referred = referred;
+        this.uses = uses;
+    }
+
+    public static boolean isVersion(String version) {
+        return VERSION.matcher(version).matches();
+    }
+
+    public static boolean isIdentifier(String value) {
+        if (value.length() < 1)
+            return false;
+
+        if (!Character.isJavaIdentifierStart(value.charAt(0)))
+            return false;
+
+        for (int i = 1; i < value.length(); i++) {
+            if (!Character.isJavaIdentifierPart(value.charAt(i)))
+                return false;
+        }
+        return true;
+    }
+
+    public static boolean isMember(String value, String[] matches) {
+        for (String match : matches) {
+            if (match.equals(value))
+                return true;
+        }
+        return false;
+    }
+
+	public static boolean isFQN(String name) {
+		if ( name.length() == 0)
+			return false;
+		if ( !Character.isJavaIdentifierStart(name.charAt(0)))
+			return false;
+		
+		for ( int i=1; i<name.length(); i++) {
+			char c = name.charAt(i);
+			if (Character.isJavaIdentifierPart(c) || c == '$' || c == '.')
+				continue;
+			
+			return false;
+		}
+		
+		return true;
+	}
+
+    /*
+     * public int verifyFilter(StringBuffer sb, String s, int rover) { rover =
+     * skip(s, rover); char c = s.charAt(rover); if (c == '(') { sb.append('(');
+     * char type; rover = skip(s, ++rover); c = s.charAt(rover); switch (c) {
+     * case '!': // not case '&': // and case '|': // or sb.append(c); type = c;
+     * while(true) { rover = skip(++rover); c = s.charAt(rover); if ( c != '(')
+     * break; rover = verifyFilter(s, rover); } break;
+     * 
+     * case ')': return rover + 1;
+     * 
+     * default: rover = skip(s,rover); c = s.charAt(rover); while (
+     * Character.isLetterOrDigit(c) || ) } } }
+     */
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/WriteResource.java b/bundleplugin/src/main/java/aQute/lib/osgi/WriteResource.java
new file mode 100644
index 0000000..99f7dae
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/WriteResource.java
@@ -0,0 +1,42 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+
+public abstract class WriteResource implements Resource {
+	long 	lastModified;
+	String	extra;
+
+	public InputStream openInputStream() throws Exception {
+	    PipedInputStream pin = new PipedInputStream();
+	    final PipedOutputStream pout = new PipedOutputStream(pin);
+	    Thread t = new Thread() {
+	        public void run() {
+	            try {
+                    write(pout);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                } finally {
+                    try {
+                        pout.close();
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+	        }
+	    };
+	    t.start();
+	    return pin;
+	}
+
+	public abstract void write(OutputStream out) throws IOException, Exception;
+	
+	public abstract long lastModified();
+
+	public String getExtra() {
+		return extra;
+	}
+
+	public void setExtra(String extra) {
+		this.extra = extra;
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/ZipResource.java b/bundleplugin/src/main/java/aQute/lib/osgi/ZipResource.java
new file mode 100644
index 0000000..83dcce3
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/ZipResource.java
@@ -0,0 +1,84 @@
+package aQute.lib.osgi;
+
+import java.io.*;
+import java.util.*;
+import java.util.regex.*;
+import java.util.zip.*;
+
+public class ZipResource implements Resource {
+    ZipFile  zip;
+    ZipEntry entry;
+    long     lastModified;
+    String   extra;
+
+    ZipResource(ZipFile zip, ZipEntry entry, long lastModified) {
+        this.zip = zip;
+        this.entry = entry;
+        this.lastModified = lastModified;
+        byte[] data = entry.getExtra();
+        if (data != null)
+            this.extra = new String(data);
+    }
+
+    public InputStream openInputStream() throws IOException {
+        return zip.getInputStream(entry);
+    }
+
+    public String toString() {
+        return ":" + zip.getName() + "(" + entry.getName() + "):";
+    }
+
+    public static ZipFile build(Jar jar, File file) throws ZipException,
+            IOException {
+        return build(jar, file, null);
+    }
+
+    public static ZipFile build(Jar jar, File file, Pattern pattern)
+            throws ZipException, IOException {
+
+        try {
+            ZipFile zip = new ZipFile(file);
+            nextEntry: for (Enumeration<? extends ZipEntry> e = zip.entries(); e
+                    .hasMoreElements();) {
+                ZipEntry entry = e.nextElement();
+                if (pattern != null) {
+                    Matcher m = pattern.matcher(entry.getName());
+                    if (!m.matches())
+                        continue nextEntry;
+                }
+                if (!entry.isDirectory()) {
+                    long time = entry.getTime();
+                    if (time <= 0)
+                        time = file.lastModified();
+                    jar.putResource(entry.getName(), new ZipResource(zip,
+                            entry, time), true);
+                }
+            }
+            return zip;
+        } catch (ZipException ze) {
+            throw new ZipException("The JAR/ZIP file ("
+                    + file.getAbsolutePath() + ") seems corrupted, error: "
+                    + ze.getMessage());
+        } catch (FileNotFoundException e) {
+            throw new IllegalArgumentException("Problem opening JAR: "
+                    + file.getAbsolutePath());
+        }
+    }
+
+    public void write(OutputStream out) throws Exception {
+        FileResource.copy(this, out);
+    }
+
+    public long lastModified() {
+        return lastModified;
+    }
+
+    public String getExtra() {
+        return extra;
+    }
+
+    public void setExtra(String extra) {
+        this.extra = extra;
+    }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/eclipse/EclipseClasspath.java b/bundleplugin/src/main/java/aQute/lib/osgi/eclipse/EclipseClasspath.java
new file mode 100644
index 0000000..47f441f
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/eclipse/EclipseClasspath.java
@@ -0,0 +1,248 @@
+package aQute.lib.osgi.eclipse;
+
+import java.io.*;
+import java.util.*;
+import java.util.regex.*;
+
+import javax.xml.parsers.*;
+
+import org.w3c.dom.*;
+import org.xml.sax.*;
+
+import aQute.libg.reporter.*;
+
+/**
+ * Parse the Eclipse project information for the classpath. Unfortunately, it is
+ * impossible to read the variables. They are ignored but that can cause
+ * problems.
+ * 
+ * @version $Revision$
+ */
+public class EclipseClasspath {
+    static DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory
+                                                                 .newInstance();
+    DocumentBuilder               db;
+    File                          project;
+    File                          workspace;
+    Set<File>                     sources                = new LinkedHashSet<File>();
+    Set<File>                     allSources                = new LinkedHashSet<File>();
+    
+    Set<File>                     classpath              = new LinkedHashSet<File>();
+    List<File>                    dependents             = new ArrayList<File>();
+    File                          output;
+    boolean                       recurse                = true;
+    Set<File>                     exports                = new LinkedHashSet<File>();
+    Map<String, String>           properties             = new HashMap<String, String>();
+    Reporter                      reporter;
+    int                           options;
+    Set<File>                     bootclasspath          = new LinkedHashSet<File>();
+
+    public final static int       DO_VARIABLES           = 1;
+
+    /**
+     * Parse an Eclipse project structure to discover the classpath.
+     * 
+     * @param workspace
+     *            Points to workspace
+     * @param project
+     *            Points to project
+     * @throws ParserConfigurationException
+     * @throws SAXException
+     * @throws IOException
+     */
+
+    public EclipseClasspath(Reporter reporter, File workspace, File project,
+            int options) throws Exception {
+        this.project = project.getCanonicalFile();
+        this.workspace = workspace.getCanonicalFile();
+        this.reporter = reporter;
+        db = documentBuilderFactory.newDocumentBuilder();
+        parse(this.project, true);
+        db = null;
+    }
+
+    public EclipseClasspath(Reporter reporter, File workspace, File project)
+            throws Exception {
+        this(reporter, workspace, project, 0);
+    }
+
+    /**
+     * Recursive routine to parse the files. If a sub project is detected, it is
+     * parsed before the parsing continues. This should give the right order.
+     * 
+     * @param project
+     *            Project directory
+     * @param top
+     *            If this is the top project
+     * @throws ParserConfigurationException
+     * @throws SAXException
+     * @throws IOException
+     */
+    void parse(File project, boolean top) throws ParserConfigurationException,
+            SAXException, IOException {
+        File file = new File(project, ".classpath");
+        if (!file.exists())
+            throw new FileNotFoundException(".classpath file not found: "
+                    + file.getAbsolutePath());
+
+        Document doc = db.parse(file);
+        NodeList nodelist = doc.getDocumentElement().getElementsByTagName(
+                "classpathentry");
+
+        if (nodelist == null)
+            throw new IllegalArgumentException(
+                    "Can not find classpathentry in classpath file");
+
+        for (int i = 0; i < nodelist.getLength(); i++) {
+            Node node = nodelist.item(i);
+            NamedNodeMap attrs = node.getAttributes();
+            String kind = get(attrs, "kind");
+            if ("src".equals(kind)) {
+                String path = get(attrs, "path");
+                // TODO boolean exported = "true".equalsIgnoreCase(get(attrs,
+                // "exported"));
+                if (path.startsWith("/")) {
+                    // We have another project
+                    File subProject = getFile(workspace, project, path);
+                    if (recurse)
+                        parse(subProject, false);
+                    dependents.add(subProject.getCanonicalFile());
+                } else {
+                    File src = getFile(workspace, project, path);
+                    allSources.add(src);
+                    if (top) {
+                        // We only want the sources for our own project
+                        // or we'll compile all at once. Not a good idea
+                        // because project settings can differ.
+                        sources.add(src);
+                    }
+                }
+            } else if ("lib".equals(kind)) {
+                String path = get(attrs, "path");
+                boolean exported = "true".equalsIgnoreCase(get(attrs,
+                        "exported"));
+                if (top || exported) {
+                    File jar = getFile(workspace, project, path);
+                    if (jar.getName().startsWith("ee."))
+                        bootclasspath.add(jar);
+                    else
+                        classpath.add(jar);
+                    if (exported)
+                        exports.add(jar);
+                }
+            } else if ("output".equals(kind)) {
+                String path = get(attrs, "path");
+                path = path.replace('/', File.separatorChar);
+                output = getFile(workspace, project, path);
+                classpath.add(output);
+                exports.add(output);
+            } else if ("var".equals(kind)) {
+                boolean exported = "true".equalsIgnoreCase(get(attrs,
+                        "exported"));
+                File lib = replaceVar(get(attrs, "path"));
+                File slib = replaceVar(get(attrs, "sourcepath"));
+                if (lib != null) {
+                    classpath.add(lib);
+                    if (exported)
+                        exports.add(lib);
+                }
+                if (slib != null)
+                    sources.add(slib);
+            } else if ("con".equals(kind)) {
+                // Should do something useful ...
+            }
+        }
+    }
+
+    private File getFile(File abs, File relative, String opath) {
+        String path = opath.replace('/', File.separatorChar);
+        File result = new File(path);
+        if (result.isAbsolute() && result.isFile()) {
+            return result;
+        }
+        if (path.startsWith(File.separator)) {
+            result = abs;
+            path = path.substring(1);
+        } else
+            result = relative;
+
+        StringTokenizer st = new StringTokenizer(path, File.separator);
+        while (st.hasMoreTokens()) {
+            String token = st.nextToken();
+            result = new File(result, token);
+        }
+
+        if (!result.exists())
+            System.err.println("File not found: project=" + project
+                    + " workspace=" + workspace + " path=" + opath + " file="
+                    + result);
+        return result;
+    }
+
+    static Pattern PATH = Pattern.compile("([A-Z_]+)/(.*)");
+
+    private File replaceVar(String path) {
+        if ((options & DO_VARIABLES) == 0)
+            return null;
+
+        Matcher m = PATH.matcher(path);
+        if (m.matches()) {
+            String var = m.group(1);
+            String remainder = m.group(2);
+            String base = (String) properties.get(var);
+            if (base != null) {
+                File b = new File(base);
+                File f = new File(b, remainder.replace('/', File.separatorChar));
+                return f;
+            } else
+                reporter.error("Can't find replacement variable for: " + path);
+        } else
+            reporter.error("Cant split variable path: " + path);
+        return null;
+    }
+
+    private String get(NamedNodeMap map, String name) {
+        Node node = map.getNamedItem(name);
+        if (node == null)
+            return null;
+
+        return node.getNodeValue();
+    }
+
+    public Set<File> getClasspath() {
+        return classpath;
+    }
+
+    public Set<File> getSourcepath() {
+        return sources;
+    }
+
+    public File getOutput() {
+        return output;
+    }
+
+    public List<File> getDependents() {
+        return dependents;
+    }
+
+    public void setRecurse(boolean recurse) {
+        this.recurse = recurse;
+    }
+
+    public Set<File> getExports() {
+        return exports;
+    }
+
+    public void setProperties(Map<String, String> map) {
+        this.properties = map;
+    }
+
+    public Set<File> getBootclasspath() {
+        return bootclasspath;
+    }
+
+    public Set<File> getAllSources() {
+        return allSources;
+    }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/osgi/packageinfo b/bundleplugin/src/main/java/aQute/lib/osgi/packageinfo
new file mode 100644
index 0000000..ec0efd4
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/packageinfo
@@ -0,0 +1 @@
+version 1.43.1
diff --git a/bundleplugin/src/main/java/aQute/lib/putjar/DirectoryInputStream.java b/bundleplugin/src/main/java/aQute/lib/putjar/DirectoryInputStream.java
new file mode 100644
index 0000000..5bd8178
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/putjar/DirectoryInputStream.java
@@ -0,0 +1,281 @@
+package aQute.lib.putjar;
+
+import java.io.*;
+import java.util.zip.*;
+
+import aQute.libg.fileiterator.*;
+
+public class DirectoryInputStream extends InputStream {
+    final File               root;
+    final FileIterator       fi;
+    File                     element;
+    int                      entries   = 0;
+    int                      state     = START;
+    long                     where     = 0;
+
+    final static int         START     = 0;
+    final static int         HEADER    = 1;
+    final static int         DATA      = 2;
+    final static int         DIRECTORY = 4;
+    final static int         EOF       = 5;
+
+    final static InputStream eof       = new ByteArrayInputStream(new byte[0]);
+    ByteArrayOutputStream    directory = new ByteArrayOutputStream();
+    InputStream              current   = eof;
+
+    public DirectoryInputStream(File dir) {
+        root = dir;
+        fi = new FileIterator(dir);
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (fi == null)
+            return -1;
+
+        int c = current.read();
+        if (c < 0) {
+            next();
+            c = current.read();
+        }
+        if (c >= 0)
+            where++;
+
+        return c;
+    }
+
+    void next() throws IOException {
+        switch (state) {
+        case START:
+        case DATA:
+            nextHeader();
+            break;
+
+        case HEADER:
+            if (element.isFile() && element.length() > 0) {
+                current = new FileInputStream(element);
+                state = DATA;
+            } else
+                nextHeader();
+            break;
+
+        case DIRECTORY:
+            state = EOF;
+            current = eof;
+            break;
+
+        case EOF:
+            break;
+        }
+    }
+
+    private void nextHeader() throws IOException {
+        if (fi.hasNext()) {
+            element = fi.next();
+            state = HEADER;
+            current = getHeader(root, element);
+            entries++;
+        } else {
+            current = getDirectory();
+            state = DIRECTORY;
+        }
+    }
+
+    /**
+     * <pre>
+     *     end of central dir signature    4 bytes  (0x06054b50)
+     *         number of this disk             2 bytes
+     *         number of the disk with the
+     *         start of the central directory  2 bytes
+     *         total number of entries in the
+     *         central directory on this disk  2 bytes
+     *         total number of entries in
+     *         the central directory           2 bytes
+     *         size of the central directory   4 bytes
+     *         offset of start of central
+     *         directory with respect to
+     *         the starting disk number        4 bytes
+     *         .ZIP file comment length        2 bytes
+     *         .ZIP file comment       (variable size)
+     * </pre>
+     * 
+     * @return
+     */
+    InputStream getDirectory() throws IOException {
+        long where = this.where;
+        int sizeDirectory = directory.size();
+
+        writeInt(directory, 0x504b0506); // Signature
+        writeShort(directory, 0); // # of disk
+        writeShort(directory, 0); // # of the disk with start of the central
+        // dir
+        writeShort(directory, entries); // # of entries
+        writeInt(directory, sizeDirectory); // Size of central dir
+        writeInt(directory, (int) where);
+        writeShort(directory, 0);
+
+        directory.close();
+
+        byte[] data = directory.toByteArray();
+        return new ByteArrayInputStream(data);
+    }
+
+    private void writeShort(OutputStream out, int v) throws IOException {
+        for (int i = 0; i < 2; i++) {
+            out.write((byte) (v & 0xFF));
+            v = v >> 8;
+        }
+    }
+
+    private void writeInt(OutputStream out, int v) throws IOException {
+        for (int i = 0; i < 4; i++) {
+            out.write((byte) (v & 0xFF));
+            v = v >> 8;
+        }
+    }
+
+    /**
+     * Local file header:
+     * 
+     * <pre>
+     * 
+     *         local file header signature     4 bytes  (0x04034b50)
+     *         version needed to extract       2 bytes
+     *         general purpose bit flag        2 bytes
+     *         compression method              2 bytes
+     *         last mod file time              2 bytes
+     *         last mod file date              2 bytes
+     *         crc-32                          4 bytes
+     *         compressed size                 4 bytes
+     *         uncompressed size               4 bytes
+     *         file name length                2 bytes
+     *         extra field length              2 bytes
+     * 
+     *         file name (variable size)
+     *         extra field (variable size)
+     * 
+     *     central file header signature   4 bytes  (0x02014b50)
+     *         version made by                 2 bytes
+     *         version needed to extract       2 bytes
+     *         general purpose bit flag        2 bytes
+     *         compression method              2 bytes
+     *         last mod file time              2 bytes
+     *         last mod file date              2 bytes
+     *         crc-32                          4 bytes
+     *         compressed size                 4 bytes
+     *         uncompressed size               4 bytes
+     *         file name length                2 bytes
+     *         extra field length              2 bytes
+     *         file comment length             2 bytes
+     *         disk number start               2 bytes
+     *         internal file attributes        2 bytes
+     *         external file attributes        4 bytes
+     *         relative offset of local header 4 bytes
+     * 
+     *         file name (variable size)
+     *         extra field (variable size)
+     *         file comment (variable size)
+     * </pre>
+     * </pre>
+     * 
+     * @param file
+     * @return
+     */
+    private InputStream getHeader(File root, File file) throws IOException {
+        long where = this.where;
+        ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        // Signature
+        writeInt(bout, 0x04034b50);
+        writeInt(directory, 0x504b0102);
+
+        // Version needed to extract
+        writeShort(directory, 0);
+
+        // Version needed to extract
+        writeShort(bout, 10);
+        writeShort(directory, 10);
+
+        // General purpose bit flag (use descriptor)
+        writeShort(bout, 0); // descriptor follows data
+        writeShort(directory, 0); // descriptor follows data
+
+        // Compresson method (stored)
+        writeShort(bout, 0);
+        writeShort(directory, 0);
+
+        // Mod time
+        writeInt(bout, 0);
+        writeInt(directory, 0);
+
+        if (file.isDirectory()) {
+            writeInt(bout, 0); // CRC
+            writeInt(bout, 0); // Compressed size
+            writeInt(bout, 0); // Uncompressed Size
+            writeInt(directory, 0);
+            writeInt(directory, 0);
+            writeInt(directory, 0);
+        } else {
+            CRC32 crc = getCRC(file);
+            writeInt(bout, (int) crc.getValue());
+            writeInt(bout, (int) file.length());
+            writeInt(bout, (int) file.length());
+            writeInt(directory, (int) crc.getValue());
+            writeInt(directory, (int) file.length());
+            writeInt(directory, (int) file.length());
+        }
+
+        String p = getPath(root, file);
+        if (file.isDirectory())
+            p = p + "/";
+        byte[] path = p.getBytes("UTF-8");
+        writeShort(bout, path.length);
+        writeShort(directory, path.length);
+
+        writeShort(bout, 0); // extra length
+        writeShort(directory, 0);
+
+        bout.write(path);
+
+        writeShort(directory, 0); // File comment length
+        writeShort(directory, 0); // disk number start 2 bytes
+        writeShort(directory, 0); // internal file attributes 2 bytes
+        writeInt(directory, 0); // external file attributes 4 bytes
+        writeInt(directory, (int) where); // relative offset of local header 4
+        // bytes
+
+        directory.write(path);
+
+        byte[] bytes = bout.toByteArray();
+        return new ByteArrayInputStream(bytes);
+    }
+
+    private String getPath(File root, File file) {
+        if (file.equals(root))
+            return "";
+
+        String p = getPath(root, file.getParentFile());
+        if (p.length() == 0)
+            p = file.getName();
+        else {
+            p = p + "/" + file.getName();
+        }
+        return p;
+    }
+
+    private CRC32 getCRC(File file) throws IOException {
+        CRC32 crc = new CRC32();
+        FileInputStream in = new FileInputStream(file);
+        try {
+            byte data[] = new byte[10000];
+            int size = in.read(data);
+            while (size > 0) {
+                crc.update(data, 0, size);
+                size = in.read(data);
+            }
+        } finally {
+            in.close();
+        }
+        return crc;
+    }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/putjar/packageinfo b/bundleplugin/src/main/java/aQute/lib/putjar/packageinfo
new file mode 100644
index 0000000..7c8de03
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/putjar/packageinfo
@@ -0,0 +1 @@
+version 1.0
diff --git a/bundleplugin/src/main/java/aQute/lib/spring/JPAComponent.java b/bundleplugin/src/main/java/aQute/lib/spring/JPAComponent.java
new file mode 100644
index 0000000..131709c
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/spring/JPAComponent.java
@@ -0,0 +1,25 @@
+package aQute.lib.spring;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import aQute.lib.osgi.Analyzer;
+
+/**
+ * This component is called when we find a resource in the META-INF/*.xml
+ * pattern. We parse the resource and and the imports to the builder.
+ * 
+ * Parsing is done with XSLT (first time I see the use of having XML for the
+ * Spring configuration files!).
+ * 
+ * @author aqute
+ * 
+ */
+public class JPAComponent extends XMLTypeProcessor {
+    
+    protected List<XMLType> getTypes(Analyzer analyzer) throws Exception {
+        List<XMLType> types = new ArrayList<XMLType>();        
+        process(types,"jpa.xsl", "META-INF", "persistence.xml");
+        return types;
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/spring/SpringComponent.java b/bundleplugin/src/main/java/aQute/lib/spring/SpringComponent.java
new file mode 100644
index 0000000..3318a0e
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/spring/SpringComponent.java
@@ -0,0 +1,97 @@
+package aQute.lib.spring;
+
+import java.io.*;
+import java.util.*;
+import java.util.regex.*;
+
+import javax.xml.transform.*;
+import javax.xml.transform.stream.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+
+/**
+ * This component is called when we find a resource in the META-INF/*.xml
+ * pattern. We parse the resource and and the imports to the builder.
+ * 
+ * Parsing is done with XSLT (first time I see the use of having XML for the
+ * Spring configuration files!).
+ * 
+ * @author aqute
+ * 
+ */
+public class SpringComponent implements AnalyzerPlugin {
+	static Transformer transformer;
+	static Pattern SPRING_SOURCE = Pattern.compile("META-INF/spring/.*\\.xml");
+	static Pattern QN = Pattern.compile("[_A-Za-z$][_A-Za-z0-9$]*(\\.[_A-Za-z$][_A-Za-z0-9$]*)*");
+
+	public static Set<CharSequence> analyze(InputStream in) throws Exception {
+		if (transformer == null) {
+			TransformerFactory tf = TransformerFactory.newInstance();
+			Source source = new StreamSource(SpringComponent.class
+					.getResourceAsStream("extract.xsl"));
+			transformer = tf.newTransformer(source);
+		}
+
+		Set<CharSequence> refers = new HashSet<CharSequence>();
+
+		ByteArrayOutputStream bout = new ByteArrayOutputStream();
+		Result r = new StreamResult(bout);
+		Source s = new StreamSource(in);
+		transformer.transform(s, r);
+
+		ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
+		bout.close();
+
+		BufferedReader br = new BufferedReader(new InputStreamReader(bin, "UTF8"));
+
+		String line = br.readLine();
+		while (line != null) {
+			line = line.trim();
+			if (line.length() > 0) {
+				String parts[] = line.split("\\s*,\\s*");
+				for (int i = 0; i < parts.length; i++) {
+					int n = parts[i].lastIndexOf('.');
+					if (n > 0) {
+						refers.add(parts[i].subSequence(0, n));
+					}
+				}
+			}
+			line = br.readLine();
+		}
+		br.close();
+		return refers;
+	}
+
+	@SuppressWarnings("unchecked")
+    public boolean analyzeJar(Analyzer analyzer) throws Exception {
+	    Jar jar = analyzer.getJar();
+		Map dir = (Map) jar.getDirectories().get("META-INF/spring");
+		if ( dir == null || dir.isEmpty())
+			return false;
+		
+		for (Iterator i = dir.entrySet().iterator(); i.hasNext();) {
+			Map.Entry entry = (Map.Entry) i.next();
+			String path = (String) entry.getKey();
+			Resource resource = (Resource) entry.getValue();
+			if (SPRING_SOURCE.matcher(path).matches()) {
+				try {
+				InputStream in = resource.openInputStream();
+				Set set = analyze(in);
+				in.close();
+				for (Iterator r = set.iterator(); r.hasNext();) {
+					String pack = (String) r.next();
+					if ( !QN.matcher(pack).matches())
+					    analyzer.warning("Package does not seem a package in spring resource ("+path+"): " + pack );
+					if (!analyzer.getReferred().containsKey(pack))
+						analyzer.getReferred().put(pack, new LinkedHashMap());
+				}
+				} catch( Exception e ) {
+					analyzer.error("Unexpected exception in processing spring resources("+path+"): " + e );
+				}
+			}
+		}
+		return false;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/spring/SpringXMLType.java b/bundleplugin/src/main/java/aQute/lib/spring/SpringXMLType.java
new file mode 100644
index 0000000..35b59a9
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/spring/SpringXMLType.java
@@ -0,0 +1,33 @@
+package aQute.lib.spring;
+
+import java.util.*;
+
+import aQute.lib.osgi.*;
+
+/**
+ * This component is called when we find a resource in the META-INF/*.xml
+ * pattern. We parse the resource and and the imports to the builder.
+ * 
+ * Parsing is done with XSLT (first time I see the use of having XML for the
+ * Spring configuration files!).
+ * 
+ * @author aqute
+ * 
+ */
+public class SpringXMLType extends XMLTypeProcessor {
+
+    protected List<XMLType> getTypes(Analyzer analyzer) throws Exception {
+        List<XMLType> types = new ArrayList<XMLType>();
+        
+        String header = analyzer.getProperty("Bundle-Blueprint", "META-INF/blueprint");
+        process(types,"extract.xsl", header, ".*\\.xml");
+        header = analyzer.getProperty("Spring-Context", "META-INF/spring");
+        process(types,"extract.xsl", header, ".*\\.xml"); 
+        
+        return types;
+    }
+
+ 
+
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/spring/XMLType.java b/bundleplugin/src/main/java/aQute/lib/spring/XMLType.java
new file mode 100644
index 0000000..9fadb35
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/spring/XMLType.java
@@ -0,0 +1,108 @@
+package aQute.lib.spring;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.regex.*;
+
+import javax.xml.transform.*;
+import javax.xml.transform.stream.*;
+
+import aQute.lib.osgi.*;
+
+public class XMLType {
+    
+    Transformer    transformer;
+    Pattern        paths;
+    String          root;
+    
+    
+    static Pattern QN = Pattern
+                              .compile("[_A-Za-z$][_A-Za-z0-9$]*(\\.[_A-Za-z$][_A-Za-z0-9$]*)*");
+
+    public XMLType(URL source, String root, String paths ) throws Exception {
+        transformer = getTransformer(source);
+        this.paths = Pattern.compile(paths);
+        this.root = root;
+    }
+    
+    public Set<String> analyze(InputStream in) throws Exception {
+        Set<String> refers = new HashSet<String>();
+
+        ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        Result r = new StreamResult(bout);
+        Source s = new StreamSource(in);
+        transformer.transform(s, r);
+
+        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
+        bout.close();
+
+        BufferedReader br = new BufferedReader(new InputStreamReader(bin, "UTF8"));
+
+        String line = br.readLine();
+        while (line != null) {
+            line = line.trim();
+            if (line.length() > 0) {
+                String parts[] = line.split("\\s*,\\s*");
+                for (int i = 0; i < parts.length; i++) {
+                    int n = parts[i].lastIndexOf('.');
+                    if (n > 0) {
+                        refers.add(parts[i].subSequence(0, n).toString());
+                    }
+                }
+            }
+            line = br.readLine();
+        }
+        br.close();
+        return refers;
+    }
+
+    public boolean analyzeJar(Analyzer analyzer) throws Exception {
+        Jar jar = analyzer.getJar();
+        Map<String,Resource> dir = jar.getDirectories().get(root);
+        if (dir == null || dir.isEmpty()) {
+            Resource resource  = jar.getResource(root);
+            if ( resource != null )
+                process(analyzer, root, resource);
+            return false;
+        }
+
+        for (Iterator<Map.Entry<String,Resource>> i = dir.entrySet().iterator(); i.hasNext();) {
+            Map.Entry<String,Resource> entry = i.next();
+            String path = entry.getKey();
+            Resource resource = entry.getValue();
+            if (paths.matcher(path).matches()) {
+                process(analyzer, path, resource);
+            }
+        }
+        return false;
+    }
+
+    private void process(Analyzer analyzer, String path, Resource resource) {
+        try {
+            InputStream in = resource.openInputStream();
+            Set<String> set = analyze(in);
+            in.close();
+            for (Iterator<String> r = set.iterator(); r.hasNext();) {
+                String pack = r.next();
+                if (!QN.matcher(pack).matches())
+                    analyzer
+                            .warning("Package does not seem a package in spring resource ("
+                                    + path + "): " + pack);
+                if (!analyzer.getReferred().containsKey(pack))
+                    analyzer.getReferred().put(pack,
+                            new LinkedHashMap<String,String>());
+            }
+        } catch (Exception e) {
+            analyzer
+                    .error("Unexpected exception in processing spring resources("
+                            + path + "): " + e);
+        }
+    }
+
+    protected Transformer getTransformer(java.net.URL url) throws Exception {
+        TransformerFactory tf = TransformerFactory.newInstance();
+        Source source = new StreamSource(url.openStream());
+        return tf.newTransformer(source);
+    }
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/spring/XMLTypeProcessor.java b/bundleplugin/src/main/java/aQute/lib/spring/XMLTypeProcessor.java
new file mode 100644
index 0000000..dde8b7e
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/spring/XMLTypeProcessor.java
@@ -0,0 +1,33 @@
+package aQute.lib.spring;
+
+import java.util.*;
+
+import aQute.bnd.service.*;
+import aQute.lib.osgi.*;
+
+public class XMLTypeProcessor implements AnalyzerPlugin {
+    
+    public boolean analyzeJar(Analyzer analyzer) throws Exception {
+        List<XMLType> types = getTypes(analyzer);
+        for ( XMLType type : types ) {
+            type.analyzeJar(analyzer);
+        }
+        return false;
+    }
+    
+    protected List<XMLType> getTypes(Analyzer analyzer) throws Exception {
+        return new ArrayList<XMLType>();
+    }
+
+
+    protected void process(List<XMLType> types, String resource, String paths,
+            String pattern) throws Exception {
+        
+        Map<String,Map<String,String>> map = Processor.parseHeader(paths,null);
+        for ( String path : map.keySet() ) {
+            types.add( new XMLType( getClass().getResource(resource), path, pattern ));
+        }
+    }
+
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/tag/Tag.java b/bundleplugin/src/main/java/aQute/lib/tag/Tag.java
new file mode 100644
index 0000000..8762cce
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/tag/Tag.java
@@ -0,0 +1,465 @@
+package aQute.lib.tag;
+
+import java.io.*;
+import java.text.*;
+import java.util.*;
+
+/**
+ * The Tag class represents a minimal XML tree. It consist of a named element
+ * with a hashtable of named attributes. Methods are provided to walk the tree
+ * and get its constituents. The content of a Tag is a list that contains String
+ * objects or other Tag objects.
+ */
+public class Tag {
+	Tag							parent;													// Parent
+	String						name;														// Name
+	final Map<String, String>	attributes	= new LinkedHashMap<String, String>();
+	final List<Object>			content		= new ArrayList<Object>();						// Content
+	static SimpleDateFormat		format		= new SimpleDateFormat("yyyyMMddHHmmss.SSS");
+	boolean						cdata;
+
+	/**
+	 * Construct a new Tag with a name.
+	 */
+	public Tag(String name, Object... contents) {
+		this.name = name;
+		for (Object c : contents)
+			content.add(c);
+	}
+
+	public Tag(Tag parent, String name, Object... contents) {
+		this(name,contents);
+		parent.addContent(this);
+	}
+
+	/**
+	 * Construct a new Tag with a name.
+	 */
+	public Tag(String name, Map<String, String> attributes, Object... contents) {
+		this(name,contents);
+		this.attributes.putAll(attributes);
+
+	}
+	public Tag(String name, Map<String, String> attributes) {
+		this(name, attributes, new Object[0]);
+	}
+
+	/**
+	 * Construct a new Tag with a name and a set of attributes. The attributes
+	 * are given as ( name, value ) ...
+	 */
+	public Tag(String name, String[] attributes, Object... contents) {
+		this(name,contents);
+		for (int i = 0; i < attributes.length; i += 2)
+			addAttribute(attributes[i], attributes[i + 1]);
+	}
+	
+	public Tag(String name, String[] attributes) {
+		this(name, attributes, new Object[0]);
+	}
+
+	/**
+	 * Add a new attribute.
+	 */
+	public Tag addAttribute(String key, String value) {
+		if (value != null)
+			attributes.put(key, value);
+		return this;
+	}
+
+	/**
+	 * Add a new attribute.
+	 */
+	public Tag addAttribute(String key, Object value) {
+		if (value == null)
+			return this;
+		attributes.put(key, value.toString());
+		return this;
+	}
+
+	/**
+	 * Add a new attribute.
+	 */
+	public Tag addAttribute(String key, int value) {
+		attributes.put(key, Integer.toString(value));
+		return this;
+	}
+
+	/**
+	 * Add a new date attribute. The date is formatted as the SimpleDateFormat
+	 * describes at the top of this class.
+	 */
+	public Tag addAttribute(String key, Date value) {
+		if (value != null)
+			attributes.put(key, format.format(value));
+		return this;
+	}
+
+	/**
+	 * Add a new content string.
+	 */
+	public Tag addContent(String string) {
+		if (string != null)
+			content.add(string);
+		return this;
+	}
+
+	/**
+	 * Add a new content tag.
+	 */
+	public Tag addContent(Tag tag) {
+		content.add(tag);
+		tag.parent = this;
+		return this;
+	}
+
+	/**
+	 * Return the name of the tag.
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Return the attribute value.
+	 */
+	public String getAttribute(String key) {
+		return (String) attributes.get(key);
+	}
+
+	/**
+	 * Return the attribute value or a default if not defined.
+	 */
+	public String getAttribute(String key, String deflt) {
+		String answer = getAttribute(key);
+		return answer == null ? deflt : answer;
+	}
+
+	/**
+	 * Answer the attributes as a Dictionary object.
+	 */
+	public Map<String, String> getAttributes() {
+		return attributes;
+	}
+
+	/**
+	 * Return the contents.
+	 */
+	public List<Object> getContents() {
+		return content;
+	}
+
+	/**
+	 * Return a string representation of this Tag and all its children
+	 * recursively.
+	 */
+	public String toString() {
+		StringWriter sw = new StringWriter();
+		print(0, new PrintWriter(sw));
+		return sw.toString();
+	}
+
+	/**
+	 * Return only the tags of the first level of descendants that match the
+	 * name.
+	 */
+	public List<Object> getContents(String tag) {
+		List<Object> out = new ArrayList<Object>();
+		for (Object o : out) {
+			if (o instanceof Tag && ((Tag) o).getName().equals(tag))
+				out.add(o);
+		}
+		return out;
+	}
+
+	/**
+	 * Return the whole contents as a String (no tag info and attributes).
+	 */
+	public String getContentsAsString() {
+		StringBuffer sb = new StringBuffer();
+		getContentsAsString(sb);
+		return sb.toString();
+	}
+
+	/**
+	 * convenient method to get the contents in a StringBuffer.
+	 */
+	public void getContentsAsString(StringBuffer sb) {
+		for (Object o : content) {
+			if (o instanceof Tag)
+				((Tag) o).getContentsAsString(sb);
+			else
+				sb.append(o.toString());
+		}
+	}
+
+	/**
+	 * Print the tag formatted to a PrintWriter.
+	 */
+	public Tag print(int indent, PrintWriter pw) {
+		pw.print("\n");
+		spaces(pw, indent);
+		pw.print('<');
+		pw.print(name);
+
+		for (String key : attributes.keySet()) {
+			String value = escape(attributes.get(key));
+			pw.print(' ');
+			pw.print(key);
+			pw.print("=");
+			String quote = "'";
+			if (value.indexOf(quote) >= 0)
+				quote = "\"";
+			pw.print(quote);
+			pw.print(value);
+			pw.print(quote);
+		}
+
+		if (content.size() == 0)
+			pw.print('/');
+		else {
+			pw.print('>');
+			for (Object c : content) {
+				if (c instanceof String) {
+					formatted(pw, indent + 2, 60, escape((String) c));
+				} else if (c instanceof Tag) {
+					Tag tag = (Tag) c;
+					tag.print(indent + 2, pw);
+				}
+			}
+			pw.print("\n");
+			spaces(pw, indent);
+			pw.print("</");
+			pw.print(name);
+		}
+		pw.print('>');
+		return this;
+	}
+
+	/**
+	 * Convenience method to print a string nicely and does character conversion
+	 * to entities.
+	 */
+	void formatted(PrintWriter pw, int left, int width, String s) {
+		int pos = width + 1;
+		s = s.trim();
+
+		for (int i = 0; i < s.length(); i++) {
+			char c = s.charAt(i);
+			if (i == 0 || (Character.isWhitespace(c) && pos > width - 3)) {
+				pw.print("\n");
+				spaces(pw, left);
+				pos = 0;
+			}
+			switch (c) {
+			case '<':
+				pw.print("&lt;");
+				pos += 4;
+				break;
+			case '>':
+				pw.print("&gt;");
+				pos += 4;
+				break;
+			case '&':
+				pw.print("&amp;");
+				pos += 5;
+				break;
+			default:
+				pw.print(c);
+				pos++;
+				break;
+			}
+
+		}
+	}
+
+	/**
+	 * Escape a string, do entity conversion.
+	 */
+	String escape(String s) {
+		StringBuffer sb = new StringBuffer();
+		for (int i = 0; i < s.length(); i++) {
+			char c = s.charAt(i);
+			switch (c) {
+			case '<':
+				sb.append("&lt;");
+				break;
+			case '>':
+				sb.append("&gt;");
+				break;
+			case '&':
+				sb.append("&amp;");
+				break;
+			default:
+				sb.append(c);
+				break;
+			}
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * Make spaces.
+	 */
+	void spaces(PrintWriter pw, int n) {
+		while (n-- > 0)
+			pw.print(' ');
+	}
+
+	/**
+	 * root/preferences/native/os
+	 */
+	public Collection<Tag> select(String path) {
+		return select(path, (Tag) null);
+	}
+
+	public Collection<Tag> select(String path, Tag mapping) {
+		List<Tag> v = new ArrayList<Tag>();
+		select(path, v, mapping);
+		return v;
+	}
+
+	void select(String path, List<Tag> results, Tag mapping) {
+		if (path.startsWith("//")) {
+			int i = path.indexOf('/', 2);
+			String name = path.substring(2, i < 0 ? path.length() : i);
+
+			for (Object o : content) {
+				if (o instanceof Tag) {
+					Tag child = (Tag) o;
+					if (match(name, child, mapping))
+						results.add(child);
+					child.select(path, results, mapping);
+				}
+
+			}
+			return;
+		}
+
+		if (path.length() == 0) {
+			results.add(this);
+			return;
+		}
+
+		int i = path.indexOf("/");
+		String elementName = path;
+		String remainder = "";
+		if (i > 0) {
+			elementName = path.substring(0, i);
+			remainder = path.substring(i + 1);
+		}
+
+		for (Object o : content) {
+			if (o instanceof Tag) {
+				Tag child = (Tag) o;
+				if (child.getName().equals(elementName) || elementName.equals("*"))
+					child.select(remainder, results, mapping);
+			}
+		}
+	}
+
+	public boolean match(String search, Tag child, Tag mapping) {
+		String target = child.getName();
+		String sn = null;
+		String tn = null;
+
+		if (search.equals("*"))
+			return true;
+
+		int s = search.indexOf(':');
+		if (s > 0) {
+			sn = search.substring(0, s);
+			search = search.substring(s + 1);
+		}
+		int t = target.indexOf(':');
+		if (t > 0) {
+			tn = target.substring(0, t);
+			target = target.substring(t + 1);
+		}
+
+		if (!search.equals(target)) // different tag names
+			return false;
+
+		if (mapping == null) {
+			return tn == sn || (sn != null && sn.equals(tn));
+		} else {
+			String suri = sn == null ? mapping.getAttribute("xmlns") : mapping
+					.getAttribute("xmlns:" + sn);
+			String turi = tn == null ? child.findRecursiveAttribute("xmlns") : child
+					.findRecursiveAttribute("xmlns:" + tn);
+			return turi == suri || (turi != null && suri != null && turi.equals(suri));
+		}
+	}
+
+	public String getString(String path) {
+		String attribute = null;
+		int index = path.indexOf("@");
+		if (index >= 0) {
+			// attribute
+			attribute = path.substring(index + 1);
+
+			if (index > 0) {
+				// prefix path
+				path = path.substring(index - 1); // skip -1
+			} else
+				path = "";
+		}
+		Collection<Tag> tags = select(path);
+		StringBuffer sb = new StringBuffer();
+		for (Tag tag : tags) {
+			if (attribute == null)
+				tag.getContentsAsString(sb);
+			else
+				sb.append(tag.getAttribute(attribute));
+		}
+		return sb.toString();
+	}
+
+	public String getStringContent() {
+		StringBuffer sb = new StringBuffer();
+		for (Object c : content) {
+			if (!(c instanceof Tag))
+				sb.append(c);
+		}
+		return sb.toString();
+	}
+
+	public String getNameSpace() {
+		return getNameSpace(name);
+	}
+
+	public String getNameSpace(String name) {
+		int index = name.indexOf(':');
+		if (index > 0) {
+			String ns = name.substring(0, index);
+			return findRecursiveAttribute("xmlns:" + ns);
+		} else
+			return findRecursiveAttribute("xmlns");
+	}
+
+	public String findRecursiveAttribute(String name) {
+		String value = getAttribute(name);
+		if (value != null)
+			return value;
+		if (parent != null)
+			return parent.findRecursiveAttribute(name);
+		return null;
+	}
+
+	public String getLocalName() {
+		int index = name.indexOf(':');
+		if (index <= 0)
+			return name;
+
+		return name.substring(index + 1);
+	}
+
+	public void rename(String string) {
+		name = string;
+	}
+
+	public void setCDATA() {
+		cdata = true;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/lib/tag/packageinfo b/bundleplugin/src/main/java/aQute/lib/tag/packageinfo
new file mode 100644
index 0000000..7c8de03
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/tag/packageinfo
@@ -0,0 +1 @@
+version 1.0