Use local copy of latest bndlib code for pre-release testing purposes

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1347815 13f79535-47bb-0310-9956-ffa450edef68
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 100755
index 0000000..8be2edb
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/lib/osgi/Analyzer.java
@@ -0,0 +1,2486 @@
+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 static aQute.libg.generics.Create.*;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import java.util.Map.Entry;
+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.base64.*;
+import aQute.lib.collections.*;
+import aQute.lib.filter.*;
+import aQute.lib.hex.*;
+import aQute.lib.io.*;
+import aQute.lib.osgi.Descriptors.Descriptor;
+import aQute.lib.osgi.Descriptors.PackageRef;
+import aQute.lib.osgi.Descriptors.TypeRef;
+import aQute.libg.cryptography.*;
+import aQute.libg.generics.*;
+import aQute.libg.header.*;
+import aQute.libg.version.Version;
+
+public class Analyzer extends Processor {
+	private final SortedSet<Clazz.JAVA>				ees						= new TreeSet<Clazz.JAVA>();
+	static Properties								bndInfo;
+
+	// Bundle parameters
+	private Jar										dot;
+	private final Packages							contained				= new Packages();
+	private final Packages							referred				= new Packages();
+	private Packages								exports;
+	private Packages								imports;
+	private TypeRef									activator;
+
+	// Global parameters
+	private final MultiMap<PackageRef, PackageRef>	uses					= new MultiMap<PackageRef, PackageRef>(
+																					PackageRef.class,
+																					PackageRef.class,
+																					true);
+	private final Packages							classpathExports		= new Packages();
+	private final Descriptors						descriptors				= new Descriptors();
+	private final List<Jar>							classpath				= list();
+	private final Map<TypeRef, Clazz>				classspace				= map();
+	private final Map<TypeRef, Clazz>				importedClassesCache	= map();
+	private boolean									analyzed				= false;
+	private boolean									diagnostics				= false;
+	private boolean									inited					= false;
+
+	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;
+			uses.clear();
+			classspace.clear();
+			classpathExports.clear();
+
+			// Parse all the class in the
+			// the jar according to the OSGi bcp
+			analyzeBundleClasspath();
+
+			//
+			// calculate class versions in use
+			//
+			for (Clazz c : classspace.values()) {
+				ees.add(c.getFormat());
+			}
+
+			//
+			// Get exported packages from the
+			// entries on the classpath
+			//
+
+			for (Jar current : getClasspath()) {
+				getExternalExports(current, classpathExports);
+				for (String dir : current.getDirectories().keySet()) {
+					PackageRef packageRef = getPackageRef(dir);
+					Resource resource = current.getResource(dir + "/packageinfo");
+					setPackageInfo(packageRef, resource, classpathExports);
+				}
+			}
+
+			// Handle the bundle activator
+
+			String s = getProperty(BUNDLE_ACTIVATOR);
+			if (s != null) {
+				activator = getTypeRefFromFQN(s);
+				referTo(activator);
+			}
+
+			// Execute any plugins
+			// TODO handle better reanalyze
+			doPlugins();
+
+			Jar extra = getExtra();
+			while (extra != null) {
+				dot.addAll(extra);
+				analyzeJar(extra, "", true);
+				extra = getExtra();
+			}
+
+			referred.keySet().removeAll(contained.keySet());
+
+			//
+			// EXPORTS
+			//
+			{
+				Set<Instruction> unused = Create.set();
+
+				Instructions filter = new Instructions(getExportPackage());
+				filter.append(getExportContents());
+
+				exports = filter(filter, contained, unused);
+
+				if (!unused.isEmpty()) {
+					warning("Unused Export-Package instructions: %s ", unused);
+				}
+
+				// See what information we can find to augment the
+				// exports. I.e. look on the classpath
+				augmentExports(exports);
+			}
+
+			//
+			// IMPORTS
+			// Imports MUST come after exports because we use information from
+			// the exports
+			//
+			{
+				// Add all exports that do not have an -noimport: directive
+				// to the imports.
+				Packages referredAndExported = new Packages(referred);
+				referredAndExported.putAll(doExportsToImports(exports));
+
+				removeDynamicImports(referredAndExported);
+
+				// Remove any Java references ... where are the closures???
+				for (Iterator<PackageRef> i = referredAndExported.keySet().iterator(); i.hasNext();) {
+					if (i.next().isJava())
+						i.remove();
+				}
+
+				Set<Instruction> unused = Create.set();
+				String h = getProperty(IMPORT_PACKAGE);
+				if (h == null) // If not set use a default
+					h = "*";
+
+				if (isPedantic() && h.trim().length() == 0)
+					warning("Empty Import-Package header");
+
+				Instructions filter = new Instructions(h);
+				imports = filter(filter, referredAndExported, unused);
+				if (!unused.isEmpty()) {
+					// We ignore the end wildcard catch
+					if (!(unused.size() == 1 && unused.iterator().next().toString().equals("*")))
+						warning("Unused Import-Package instructions: %s ", unused);
+				}
+
+				// See what information we can find to augment the
+				// imports. I.e. look in the exports
+				augmentImports(imports, exports);
+			}
+
+			//
+			// USES
+			//
+			// Add the uses clause to the exports
+			doUses(exports, uses, imports);
+
+			//
+			// Checks
+			//
+			if (referred.containsKey(Descriptors.DEFAULT_PACKAGE)) {
+				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 "
+						+ uses.transpose().get(Descriptors.DEFAULT_PACKAGE));
+			}
+
+		}
+	}
+
+	/**
+	 * Discussed with BJ and decided to kill the .
+	 * 
+	 * @param referredAndExported
+	 */
+	void removeDynamicImports(Packages referredAndExported) {
+
+		// // Remove any matching a dynamic import package instruction
+		// Instructions dynamicImports = new
+		// Instructions(getDynamicImportPackage());
+		// Collection<PackageRef> dynamic = dynamicImports.select(
+		// referredAndExported.keySet(), false);
+		// referredAndExported.keySet().removeAll(dynamic);
+	}
+
+	protected Jar getExtra() throws Exception {
+		return null;
+	}
+
+	/**
+	 * 
+	 */
+	void doPlugins() {
+		for (AnalyzerPlugin plugin : getPlugins(AnalyzerPlugin.class)) {
+			try {
+				boolean reanalyze = plugin.analyzeJar(this);
+				if (reanalyze) {
+					classspace.clear();
+					analyzeBundleClasspath();
+				}
+			}
+			catch (Exception e) {
+				error("Analyzer Plugin %s failed %s", plugin, e);
+			}
+		}
+	}
+
+	/**
+	 * 
+	 * @return
+	 */
+	boolean isResourceOnly() {
+		return isTrue(getProperty(RESOURCEONLY));
+	}
+
+	/**
+	 * 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);
+
+		// Remove all the Java packages from the imports
+		if (!imports.isEmpty()) {
+			main.putValue(IMPORT_PACKAGE, printClauses(imports));
+		}
+		else {
+			main.remove(IMPORT_PACKAGE);
+		}
+
+		Packages temp = new Packages(contained);
+		temp.keySet().removeAll(exports.keySet());
+
+		if (!temp.isEmpty())
+			main.putValue(PRIVATE_PACKAGE, printClauses(temp));
+		else
+			main.remove(PRIVATE_PACKAGE);
+
+		Parameters bcp = getBundleClasspath();
+		if (bcp.isEmpty() || (bcp.containsKey(".") && bcp.size() == 1))
+			main.remove(BUNDLE_CLASSPATH);
+		else
+			main.putValue(BUNDLE_CLASSPATH, printClauses(bcp));
+
+		doNamesection(dot, manifest);
+
+		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
+		Instructions instructions = new Instructions(getProperty(REMOVEHEADERS));
+		Collection<Object> result = instructions.select(main.keySet(), false);
+		main.keySet().removeAll(result);
+
+		dot.setManifest(manifest);
+		return manifest;
+	}
+
+	/**
+	 * Parse the namesection as instructions and then match them against the
+	 * current set of resources
+	 * 
+	 * For example:
+	 * 
+	 * <pre>
+	 * 	-namesection: *;baz=true, abc/def/bar/X.class=3
+	 * </pre>
+	 * 
+	 * The raw value of {@link Constants#NAMESECTION} is used but the values of
+	 * the attributes are replaced where @ is set to the resource name. This
+	 * allows macro to operate on the resource
+	 * 
+	 */
+
+	private void doNamesection(Jar dot, Manifest manifest) {
+
+		Parameters namesection = parseHeader(getProperties().getProperty(NAMESECTION));
+		Instructions instructions = new Instructions(namesection);
+		Set<String> resources = new HashSet<String>(dot.getResources().keySet());
+
+		//
+		// For each instruction, iterator over the resources and filter
+		// them. If a resource matches, it must be removed even if the
+		// instruction is negative. If positive, add a name section
+		// to the manifest for the given resource name. Then add all
+		// attributes from the instruction to that name section.
+		//
+		for (Map.Entry<Instruction, Attrs> instr : instructions.entrySet()) {
+			boolean matched = false;
+
+			// For each instruction
+
+			for (Iterator<String> i = resources.iterator(); i.hasNext();) {
+				String path = i.next();
+				// For each resource
+
+				if (instr.getKey().matches(path)) {
+
+					// Instruction matches the resource
+
+					matched = true;
+					if (!instr.getKey().isNegated()) {
+
+						// Positive match, add the attributes
+
+						Attributes attrs = manifest.getAttributes(path);
+						if (attrs == null) {
+							attrs = new Attributes();
+							manifest.getEntries().put(path, attrs);
+						}
+
+						//
+						// Add all the properties from the instruction to the
+						// name section
+						//
+
+						for (Map.Entry<String, String> property : instr.getValue().entrySet()) {
+							setProperty("@", path);
+							try {
+								String processed = getReplacer().process(property.getValue());
+								attrs.putValue(property.getKey(), processed);
+							}
+							finally {
+								unsetProperty("@");
+							}
+						}
+					}
+					i.remove();
+				}
+			}
+
+			if (!matched && resources.size() > 0)
+				warning("The instruction %s in %s did not match any resources", instr.getKey(),
+						NAMESECTION);
+		}
+
+	}
+
+	/**
+	 * 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 = "";
+		StringBuilder sb = new StringBuilder();
+		Map<String, Map<String, Resource>> map = bundle.getDirectories();
+		for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) {
+			String directory = 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 Packages getContained() {
+		return contained;
+	}
+
+	public Packages getExports() {
+		return exports;
+	}
+
+	public Packages getImports() {
+		return imports;
+	}
+
+	public Jar getJar() {
+		return dot;
+	}
+
+	public Packages getReferred() {
+		return referred;
+	}
+
+	/**
+	 * Return the set of unreachable code depending on exports and the bundle
+	 * activator.
+	 * 
+	 * @return
+	 */
+	public Set<PackageRef> getUnreachable() {
+		Set<PackageRef> unreachable = new HashSet<PackageRef>(uses.keySet()); // all
+		for (Iterator<PackageRef> r = exports.keySet().iterator(); r.hasNext();) {
+			PackageRef packageRef = r.next();
+			removeTransitive(packageRef, unreachable);
+		}
+		if (activator != null) {
+			removeTransitive(activator.getPackageRef(), unreachable);
+		}
+		return unreachable;
+	}
+
+	public MultiMap<PackageRef, PackageRef> 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) {
+		synchronized (Analyzer.class) {
+			if (bndInfo == null) {
+				bndInfo = new Properties();
+				InputStream in = Analyzer.class.getResourceAsStream("bnd.info");
+				try {
+					if (in != null) {
+						bndInfo.load(in);
+						in.close();
+					}
+				}
+				catch (IOException ioe) {
+					warning("Could not read bnd.info in " + Analyzer.class.getPackage() + ioe);
+				}
+				finally {
+					IO.close(in);
+				}
+			}
+		}
+		return bndInfo.getProperty(key, defaultValue);
+	}
+
+	/**
+	 * Merge the existing manifest with the instructions but do not override
+	 * existing properties.
+	 * 
+	 * @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, attributes.getValue(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) {
+		if (dot != null)
+			removeClose(dot);
+
+		this.dot = jar;
+		if (dot != null)
+			addClose(dot);
+
+		return jar;
+	}
+
+	protected void begin() {
+		if (inited == false) {
+			inited = true;
+			super.begin();
+
+			updateModified(getBndLastModified(), "bnd last modified");
+			verifyManifestHeadersCase(getProperties());
+
+		}
+	}
+
+	/**
+	 * 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.getSource() != null && entry.getSource().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
+	 */
+	private 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());
+				}
+			}
+		}
+	}
+
+	/**
+	 * 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 ...
+	 * 
+	 */
+	Packages doExportsToImports(Packages exports) {
+
+		// private packages = contained - exported.
+		Set<PackageRef> privatePackages = new HashSet<PackageRef>(contained.keySet());
+		privatePackages.removeAll(exports.keySet());
+
+		// private references = ∀ p : private packages | uses(p)
+		Set<PackageRef> privateReferences = newSet();
+		for (PackageRef p : privatePackages) {
+			Collection<PackageRef> uses = this.uses.get(p);
+			if (uses != null)
+				privateReferences.addAll(uses);
+		}
+
+		// Assume we are going to export all exported packages
+		Set<PackageRef> toBeImported = new HashSet<PackageRef>(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.
+		// TODO 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<PackageRef> i = toBeImported.iterator(); i.hasNext();) {
+			PackageRef next = i.next();
+			Collection<PackageRef> usedByExportedPackage = this.uses.get(next);
+
+			for (PackageRef privatePackage : privatePackages) {
+				if (usedByExportedPackage.contains(privatePackage)) {
+					i.remove();
+					break;
+				}
+			}
+		}
+
+		// Clean up attributes and generate result map
+		Packages result = new Packages();
+		for (Iterator<PackageRef> i = toBeImported.iterator(); i.hasNext();) {
+			PackageRef ep = i.next();
+			Attrs 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 = new Attrs();
+			parameters.remove(VERSION_ATTRIBUTE);
+			result.put(ep, parameters);
+		}
+		return result;
+	}
+
+	public boolean referred(PackageRef packageName) {
+		// return true;
+		for (Map.Entry<PackageRef, List<PackageRef>> contained : uses.entrySet()) {
+			if (!contained.getKey().equals(packageName)) {
+				if (contained.getValue().contains(packageName))
+					return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * 
+	 * @param jar
+	 */
+	private void getExternalExports(Jar jar, Packages classpathExports) {
+		try {
+			Manifest m = jar.getManifest();
+			if (m != null) {
+				Domain domain = Domain.domain(m);
+				Parameters exported = domain.getExportPackage();
+				for (Entry<String, Attrs> e : exported.entrySet()) {
+					PackageRef ref = getPackageRef(e.getKey());
+					if (!classpathExports.containsKey(ref)) {
+						// TODO e.getValue().put(SOURCE_DIRECTIVE,
+						// jar.getBsn()+"-"+jar.getVersion());
+
+						classpathExports.put(ref, e.getValue());
+					}
+				}
+			}
+		}
+		catch (Exception e) {
+			warning("Erroneous Manifest for " + jar + " " + e);
+		}
+	}
+
+	/**
+	 * Find some more information about imports in manifest and other places. It
+	 * is assumed that the augmentsExports has already copied external attrs
+	 * from the classpathExports.
+	 * 
+	 * @throws Exception
+	 */
+	void augmentImports(Packages imports, Packages exports) throws Exception {
+		List<PackageRef> noimports = Create.list();
+		Set<PackageRef> provided = findProvidedPackages();
+
+		for (PackageRef packageRef : imports.keySet()) {
+			String packageName = packageRef.getFQN();
+
+			setProperty(CURRENT_PACKAGE, packageName);
+			try {
+				Attrs importAttributes = imports.get(packageRef);
+				Attrs exportAttributes = exports.get(packageRef,
+						classpathExports.get(packageRef, new Attrs()));
+
+				String exportVersion = exportAttributes.getVersion();
+				String importRange = importAttributes.getVersion();
+
+				if (exportVersion == null) {
+					// TODO Should check if the source is from a bundle.
+
+				}
+				else {
+
+					//
+					// Version Policy - Import version substitution. We
+					// calculate the export version and then allow the
+					// import version attribute to use it in a substitution
+					// by using a ${@} macro. The export version can
+					// be defined externally or locally
+					//
+
+					boolean provider = isTrue(importAttributes.get(PROVIDE_DIRECTIVE))
+							|| isTrue(exportAttributes.get(PROVIDE_DIRECTIVE))
+							|| provided.contains(packageRef);
+
+					exportVersion = cleanupVersion(exportVersion);
+
+					try {
+						setProperty("@", exportVersion);
+
+						if (importRange != null) {
+							importRange = cleanupVersion(importRange);
+							importRange = getReplacer().process(importRange);
+						}
+						else
+							importRange = getVersionPolicy(provider);
+
+					}
+					finally {
+						unsetProperty("@");
+					}
+					importAttributes.put(VERSION_ATTRIBUTE, importRange);
+				}
+
+				//
+				// Check if exporter has mandatory attributes
+				//
+				String mandatory = exportAttributes.get(MANDATORY_DIRECTIVE);
+				if (mandatory != null) {
+					String[] attrs = mandatory.split("\\s*,\\s*");
+					for (int i = 0; i < attrs.length; i++) {
+						if (!importAttributes.containsKey(attrs[i]))
+							importAttributes.put(attrs[i], exportAttributes.get(attrs[i]));
+					}
+				}
+
+				if (exportAttributes.containsKey(IMPORT_DIRECTIVE))
+					importAttributes.put(IMPORT_DIRECTIVE, exportAttributes.get(IMPORT_DIRECTIVE));
+
+				fixupAttributes(importAttributes);
+				removeAttributes(importAttributes);
+
+				String result = importAttributes.get(Constants.VERSION_ATTRIBUTE);
+				if (result == null)
+					noimports.add(packageRef);
+			}
+			finally {
+				unsetProperty(CURRENT_PACKAGE);
+			}
+		}
+
+		if (isPedantic() && noimports.size() != 0) {
+			warning("Imports that lack version ranges: %s", noimports);
+		}
+	}
+
+	/**
+	 * Find the packages we depend on, where we implement an interface that is a
+	 * Provider Type. These packages, when we import them, must use the provider
+	 * policy.
+	 * 
+	 * @throws Exception
+	 */
+	Set<PackageRef> findProvidedPackages() throws Exception {
+		Set<PackageRef> providers = Create.set();
+		Set<TypeRef> cached = Create.set();
+
+		for (Clazz c : classspace.values()) {
+			TypeRef[] interfaces = c.getInterfaces();
+			if (interfaces != null)
+				for (TypeRef t : interfaces)
+					if (cached.contains(t) || isProvider(t)) {
+						cached.add(t);
+						providers.add(t.getPackageRef());
+					}
+		}
+		return providers;
+	}
+
+	private boolean isProvider(TypeRef t) throws Exception {
+		Clazz c = findClass(t);
+		if (c == null)
+			return false;
+
+		if (c.annotations == null)
+			return false;
+
+		TypeRef pt = getTypeRefFromFQN(ProviderType.class.getName());
+		boolean result = c.annotations.contains(pt);
+		return result;
+	}
+
+	/**
+	 * Provide any macro substitutions and versions for exported packages.
+	 */
+
+	void augmentExports(Packages exports) {
+		for (PackageRef packageRef : exports.keySet()) {
+			String packageName = packageRef.getFQN();
+			setProperty(CURRENT_PACKAGE, packageName);
+			try {
+				Attrs attributes = exports.get(packageRef);
+				Attrs exporterAttributes = classpathExports.get(packageRef);
+				if (exporterAttributes == null)
+					continue;
+
+				for (Map.Entry<String, String> entry : exporterAttributes.entrySet()) {
+					String key = entry.getKey();
+					if (key.equalsIgnoreCase(SPECIFICATION_VERSION))
+						key = VERSION_ATTRIBUTE;
+
+					// dont overwrite and no directives
+					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(Attrs 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. 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.
+	 */
+
+	void removeAttributes(Attrs attributes) {
+		String remove = attributes.remove(REMOVE_ATTRIBUTE_DIRECTIVE);
+
+		if (remove != null) {
+			Instructions removeInstr = new Instructions(remove);
+			attributes.keySet().removeAll(removeInstr.select(attributes.keySet(), false));
+		}
+
+		// Remove any ! valued attributes
+		for (Iterator<Entry<String, String>> i = attributes.entrySet().iterator(); i.hasNext();) {
+			String v = i.next().getValue();
+			if (v.equals("!"))
+				i.remove();
+		}
+	}
+
+	/**
+	 * 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(Packages exports, MultiMap<PackageRef, PackageRef> uses, Packages imports) {
+		if ("true".equalsIgnoreCase(getProperty(NOUSES)))
+			return;
+
+		for (Iterator<PackageRef> i = exports.keySet().iterator(); i.hasNext();) {
+			PackageRef packageRef = i.next();
+			String packageName = packageRef.getFQN();
+			setProperty(CURRENT_PACKAGE, packageName);
+			try {
+				doUses(packageRef, exports, uses, imports);
+			}
+			finally {
+				unsetProperty(CURRENT_PACKAGE);
+			}
+
+		}
+	}
+
+	/**
+	 * @param packageName
+	 * @param exports
+	 * @param uses
+	 * @param imports
+	 */
+	protected void doUses(PackageRef packageRef, Packages exports,
+			MultiMap<PackageRef, PackageRef> uses, Packages imports) {
+		Attrs clause = exports.get(packageRef);
+
+		// Check if someone already set the uses: directive
+		String override = clause.get(USES_DIRECTIVE);
+		if (override == null)
+			override = USES_USES;
+
+		// Get the used packages
+		Collection<PackageRef> usedPackages = uses.get(packageRef);
+
+		if (usedPackages != null) {
+
+			// Only do a uses on exported or imported packages
+			// and uses should also not contain our own package
+			// name
+			Set<PackageRef> sharedPackages = new HashSet<PackageRef>();
+			sharedPackages.addAll(imports.keySet());
+			sharedPackages.addAll(exports.keySet());
+			sharedPackages.retainAll(usedPackages);
+			sharedPackages.remove(packageRef);
+
+			StringBuilder sb = new StringBuilder();
+			String del = "";
+			for (Iterator<PackageRef> u = sharedPackages.iterator(); u.hasNext();) {
+				PackageRef usedPackage = u.next();
+				if (!usedPackage.isJava()) {
+					sb.append(del);
+					sb.append(usedPackage.getFQN());
+					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, Matcher.quoteReplacement(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(PackageRef name, Set<PackageRef> unreachable) {
+		if (!unreachable.contains(name))
+			return;
+
+		unreachable.remove(name);
+
+		List<PackageRef> ref = uses.get(name);
+		if (ref != null) {
+			for (Iterator<PackageRef> r = ref.iterator(); r.hasNext();) {
+				PackageRef element = r.next();
+				removeTransitive(element, unreachable);
+			}
+		}
+	}
+
+	/**
+	 * Helper method to set the package info resource
+	 * 
+	 * @param dir
+	 * @param key
+	 * @param value
+	 * @throws Exception
+	 */
+	void setPackageInfo(PackageRef packageRef, Resource r, Packages classpathExports)
+			throws Exception {
+		if (r == null)
+			return;
+
+		Properties p = new Properties();
+		InputStream in = r.openInputStream();
+		try {
+			p.load(in);
+		}
+		finally {
+			in.close();
+		}
+		Attrs map = classpathExports.get(packageRef);
+		if (map == null) {
+			classpathExports.put(packageRef, map = new Attrs());
+		}
+		for (@SuppressWarnings("unchecked")
+		Enumeration<String> t = (Enumeration<String>) p.propertyNames(); t.hasMoreElements();) {
+			String key = t.nextElement();
+			String value = map.get(key);
+			if (value == null) {
+				value = p.getProperty(key);
+
+				// Messy, to allow directives we need to
+				// allow the value to start with a ':' since we cannot
+				// encode this in a property name
+
+				if (value.startsWith(":")) {
+					key = key + ":";
+					value = value.substring(1);
+				}
+				map.put(key, value);
+			}
+		}
+	}
+
+	public void close() {
+		if (diagnostics) {
+			PrintStream out = System.err;
+			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];
+				//$FALL-THROUGH$
+			case 2 :
+				regexp = args[1];
+		}
+		StringBuilder sb = new StringBuilder();
+		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(entry.getKey(), 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(Collection< ? > jars) throws IOException {
+		for (Object jar : jars) {
+			if (jar instanceof Jar)
+				addClasspath((Jar) jar);
+			else
+				if (jar instanceof File)
+					addClasspath((File) jar);
+				else
+					if (jar instanceof String)
+						addClasspath(getFile((String) jar));
+					else
+						error("Cannot convert to JAR to add to classpath %s. Not a File, Jar, or String",
+								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;
+	}
+
+	private void analyzeBundleClasspath() throws Exception {
+		Parameters bcp = getBundleClasspath();
+
+		if (bcp.isEmpty()) {
+			analyzeJar(dot, "", true);
+		}
+		else {
+			boolean okToIncludeDirs = true;
+
+			for (String path : bcp.keySet()) {
+				if (dot.getDirectories().containsKey(path)) {
+					okToIncludeDirs = false;
+					break;
+				}
+			}
+
+			for (String path : bcp.keySet()) {
+				Attrs info = bcp.get(path);
+
+				if (path.equals(".")) {
+					analyzeJar(dot, "", okToIncludeDirs);
+					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, "", 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 (bcp.containsKey("."))
+							warning("Bundle-ClassPath uses a directory '%s' as well as '.'. This means bnd does not know if a directory is a package.",
+									path, path);
+						analyzeJar(dot, Processor.appendPath(path) + "/", true);
+					}
+					else {
+						if (!"optional".equals(info.get(RESOLUTION_DIRECTIVE)))
+							warning("No sub JAR or directory " + path);
+					}
+				}
+			}
+
+		}
+	}
+
+	/**
+	 * 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 boolean analyzeJar(Jar jar, String prefix, boolean okToIncludeDirs) throws Exception {
+		Map<String, Clazz> mismatched = new HashMap<String, Clazz>();
+
+		next: for (String path : jar.getResources().keySet()) {
+			if (path.startsWith(prefix)) {
+
+				String relativePath = path.substring(prefix.length());
+
+				if (okToIncludeDirs) {
+					int n = relativePath.lastIndexOf('/');
+					if (n < 0)
+						n = relativePath.length();
+					String relativeDir = relativePath.substring(0, n);
+
+					PackageRef packageRef = getPackageRef(relativeDir);
+					if (!packageRef.isMetaData() && !contained.containsKey(packageRef)) {
+						contained.put(packageRef);
+
+						// For each package we encounter for the first
+						// time. Unfortunately we can only do this once
+						// we found a class since the bcp has a tendency
+						// to overlap
+						if (!packageRef.isMetaData()) {
+							Resource pinfo = jar.getResource(prefix + packageRef.getPath()
+									+ "/packageinfo");
+							setPackageInfo(packageRef, pinfo, classpathExports);
+						}
+					}
+				}
+
+				// Check class resources, we need to analyze them
+				if (path.endsWith(".class")) {
+					Resource resource = jar.getResource(path);
+					Clazz clazz;
+					Attrs info = null;
+
+					try {
+						InputStream in = resource.openInputStream();
+						clazz = new Clazz(this, path, resource);
+						try {
+							// Check if we have a package-info
+							if (relativePath.endsWith("/package-info.class")) {
+								// package-info can contain an Export annotation
+								info = new Attrs();
+								parsePackageInfoClass(clazz, info);
+							}
+							else {
+								// Otherwise we just parse it simply
+								clazz.parseClassFile();
+							}
+						}
+						finally {
+							in.close();
+						}
+					}
+					catch (Throwable e) {
+						error("Invalid class file %s (%s)", e, relativePath, e);
+						e.printStackTrace();
+						continue next;
+					}
+
+					String calculatedPath = clazz.getClassName().getPath();
+					if (!calculatedPath.equals(relativePath)) {
+						// If there is a mismatch we
+						// warning
+						if (okToIncludeDirs) // assume already reported
+							mismatched.put(clazz.getAbsolutePath(), clazz);
+					}
+					else {
+						classspace.put(clazz.getClassName(), clazz);
+						PackageRef packageRef = clazz.getClassName().getPackageRef();
+
+						if (!contained.containsKey(packageRef)) {
+							contained.put(packageRef);
+							if (!packageRef.isMetaData()) {
+								Resource pinfo = jar.getResource(prefix + packageRef.getPath()
+										+ "/packageinfo");
+								setPackageInfo(packageRef, pinfo, classpathExports);
+							}
+						}
+						if (info != null)
+							contained.merge(packageRef, false, info);
+
+						Set<PackageRef> set = Create.set();
+
+						// Look at the referred packages
+						// and copy them to our baseline
+						for (PackageRef p : clazz.getReferred()) {
+							referred.put(p);
+							set.add(p);
+						}
+						set.remove(packageRef);
+						uses.addAll(packageRef, set);
+					}
+				}
+			}
+		}
+
+		if (mismatched.size() > 0) {
+			error("Classes found in the wrong directory: %s", mismatched);
+			return false;
+		}
+		return true;
+	}
+
+	static Pattern	OBJECT_REFERENCE	= Pattern.compile("L([^/]+/)*([^;]+);");
+
+	private void parsePackageInfoClass(final Clazz clazz, final Attrs info) throws Exception {
+		clazz.parseClassFileWithCollector(new ClassDataCollector() {
+			@Override
+			public void annotation(Annotation a) {
+				String name = a.name.getFQN();
+				if (aQute.bnd.annotation.Version.class.getName().equals(name)) {
+
+					// 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 annotation 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.getClassName().getFQN());
+							}
+						}
+						catch (Exception e) {
+							// Ignore
+						}
+					}
+				}
+				else
+					if (name.equals(Export.class.getName())) {
+
+						// Check mandatory attributes
+						Attrs 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()) {
+				StringBuilder result = new StringBuilder();
+				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(StringBuilder 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);
+		}
+	}
+
+	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)) {
+				String s = args[++i];
+				instr = new Instruction(s);
+			}
+			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<TypeRef, 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(TypeRef typeRef) throws Exception {
+		Clazz c = classspace.get(typeRef);
+		if (c != null)
+			return c;
+
+		c = importedClassesCache.get(typeRef);
+		if (c != null)
+			return c;
+
+		Resource r = findResource(typeRef.getPath());
+		if (r == null) {
+			getClass().getClassLoader();
+			URL url = ClassLoader.getSystemResource(typeRef.getPath());
+			if (url != null)
+				r = new URLResource(url);
+		}
+		if (r != null) {
+			c = new Clazz(this, typeRef.getPath(), r);
+			c.parseClassFile();
+			importedClassesCache.put(typeRef, 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(TypeRef ref) {
+		PackageRef pack = ref.getPackageRef();
+		if (!referred.containsKey(pack))
+			referred.put(pack, new Attrs());
+	}
+
+	public void referToByBinaryName(String binaryClassName) {
+		TypeRef ref = descriptors.getTypeRef(binaryClassName);
+		referTo(ref);
+	}
+
+	/**
+	 * Ensure that we are running on the correct bnd.
+	 */
+	void doRequireBnd() {
+		Attrs 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);
+			}
+		}
+	}
+
+	/**
+	 * md5 macro
+	 */
+
+	static String	_md5Help	= "${md5;path}";
+
+	public String _md5(String args[]) throws Exception {
+		Macro.verifyCommand(args, _md5Help,
+				new Pattern[] {null, null, Pattern.compile("base64|hex")}, 2, 3);
+
+		Digester<MD5> digester = MD5.getDigester();
+		Resource r = dot.getResource(args[1]);
+		if (r == null)
+			throw new FileNotFoundException("From " + digester + ", not found " + args[1]);
+
+		IO.copy(r.openInputStream(), digester);
+		boolean hex = args.length > 2 && args[2].equals("hex");
+		if (hex)
+			return Hex.toHexString(digester.digest().digest());
+		else
+			return Base64.encodeBase64(digester.digest().digest());
+	}
+
+	/**
+	 * SHA1 macro
+	 */
+
+	static String	_sha1Help	= "${sha1;path}";
+
+	public String _sha1(String args[]) throws Exception {
+		Macro.verifyCommand(args, _sha1Help,
+				new Pattern[] {null, null, Pattern.compile("base64|hex")}, 2, 3);
+		Digester<SHA1> digester = SHA1.getDigester();
+		Resource r = dot.getResource(args[1]);
+		if (r == null)
+			throw new FileNotFoundException("From sha1, not found " + args[1]);
+
+		IO.copy(r.openInputStream(), digester);
+		return Base64.encodeBase64(digester.digest().digest());
+	}
+
+	public Descriptor getDescriptor(String descriptor) {
+		return descriptors.getDescriptor(descriptor);
+	}
+
+	public TypeRef getTypeRef(String binaryClassName) {
+		return descriptors.getTypeRef(binaryClassName);
+	}
+
+	public PackageRef getPackageRef(String binaryName) {
+		return descriptors.getPackageRef(binaryName);
+	}
+
+	public TypeRef getTypeRefFromFQN(String fqn) {
+		return descriptors.getTypeRefFromFQN(fqn);
+	}
+
+	public TypeRef getTypeRefFromPath(String path) {
+		return descriptors.getTypeRefFromPath(path);
+	}
+
+	public boolean isImported(PackageRef packageRef) {
+		return imports.containsKey(packageRef);
+	}
+
+	/**
+	 * Merge the attributes of two maps, where the first map can contain
+	 * wildcarded names. The idea is that the first map contains instructions
+	 * (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.
+	 * @param source the actual found packages, contains no duplicates
+	 * 
+	 * @return Only the packages that were filtered by the given instructions
+	 */
+
+	Packages filter(Instructions instructions, Packages source, Set<Instruction> nomatch) {
+		Packages result = new Packages();
+		List<PackageRef> refs = new ArrayList<PackageRef>(source.keySet());
+		Collections.sort(refs);
+
+		List<Instruction> filters = new ArrayList<Instruction>(instructions.keySet());
+		if (nomatch == null)
+			nomatch = Create.set();
+
+		for (Instruction instruction : filters) {
+			boolean match = false;
+
+			for (Iterator<PackageRef> i = refs.iterator(); i.hasNext();) {
+				PackageRef packageRef = i.next();
+
+				if (packageRef.isMetaData()) {
+					i.remove(); // no use checking it again
+					continue;
+				}
+
+				String packageName = packageRef.getFQN();
+
+				if (instruction.matches(packageName)) {
+					match = true;
+					if (!instruction.isNegated()) {
+						result.merge(packageRef, instruction.isDuplicate(), source.get(packageRef),
+								instructions.get(instruction));
+					}
+					i.remove(); // Can never match again for another pattern
+				}
+			}
+			if (!match && !instruction.isAny())
+				nomatch.add(instruction);
+		}
+
+		/*
+		 * Tricky. If we have umatched instructions they might indicate that we
+		 * want to have multiple decorators for the same package. So we check
+		 * the unmatched against the result list. If then then match and have
+		 * actually interesting properties then we merge them
+		 */
+
+		for (Iterator<Instruction> i = nomatch.iterator(); i.hasNext();) {
+			Instruction instruction = i.next();
+
+			// We assume the user knows what he is
+			// doing and inserted a literal. So
+			// we ignore any not matched literals
+			if (instruction.isLiteral()) {
+				result.merge(getPackageRef(instruction.getLiteral()), true,
+						instructions.get(instruction));
+				i.remove();
+				continue;
+			}
+
+			// Not matching a negated instruction looks
+			// like an error ...
+			if (instruction.isNegated()) {
+				continue;
+			}
+
+			// An optional instruction should not generate
+			// an error
+			if (instruction.isOptional()) {
+				i.remove();
+				continue;
+			}
+
+			// boolean matched = false;
+			// Set<PackageRef> prefs = new HashSet<PackageRef>(result.keySet());
+			// for (PackageRef ref : prefs) {
+			// if (instruction.matches(ref.getFQN())) {
+			// result.merge(ref, true, source.get(ref),
+			// instructions.get(instruction));
+			// matched = true;
+			// }
+			// }
+			// if (matched)
+			// i.remove();
+		}
+		return result;
+	}
+
+	public void setDiagnostics(boolean b) {
+		diagnostics = b;
+	}
+
+	public Clazz.JAVA getLowestEE() {
+		if (ees.isEmpty())
+			return Clazz.JAVA.JDK1_4;
+
+		return ees.first();
+	}
+
+	public String _ee(String args[]) {
+		return getLowestEE().getEE();
+	}
+
+	/**
+	 * Calculate the output file for the given target. The strategy is:
+	 * 
+	 * <pre>
+	 * parameter given if not null and not directory
+	 * if directory, this will be the output directory
+	 * based on bsn-version.jar
+	 * name of the source file if exists
+	 * Untitled-[n]
+	 * </pre>
+	 * 
+	 * @param output may be null, otherwise a file path relative to base
+	 */
+	public File getOutputFile(String output) {
+
+		if (output == null)
+			output = get(Constants.OUTPUT);
+
+		File outputDir;
+
+		if (output != null) {
+			File outputFile = getFile(output);
+			if (outputFile.isDirectory())
+				outputDir = outputFile;
+			else
+				return outputFile;
+		}
+		else
+			outputDir = getBase();
+
+		if (getBundleSymbolicName() != null) {
+			String bsn = getBundleSymbolicName();
+			String version = getBundleVersion();
+			Version v = Version.parseVersion(version);
+			String outputName = bsn + "-" + v.getWithoutQualifier()
+					+ Constants.DEFAULT_JAR_EXTENSION;
+			return new File(outputDir, outputName);
+		}
+
+		File source = getJar().getSource();
+		if (source != null) {
+			String outputName = source.getName();
+			return new File(outputDir, outputName);
+		}
+
+		error("Cannot establish an output name from %s, nor bsn, nor source file name, using Untitled",
+				output);
+		int n = 0;
+		File f = getFile(outputDir, "Untitled");
+		while (f.isFile()) {
+			f = getFile(outputDir, "Untitled-" + n++);
+		}
+		return f;
+	}
+
+	/**
+	 * Utility function to carefully save the file. Will create a backup if the
+	 * source file has the same path as the output. It will also only save if
+	 * the file was modified or the force flag is true
+	 * 
+	 * @param output the output file, if null {@link #getOutputFile(String)} is
+	 *        used.
+	 * @param force if it needs to be overwritten
+	 * @throws Exception
+	 */
+
+	public boolean save(File output, boolean force) throws Exception {
+		if (output == null)
+			output = getOutputFile(null);
+
+		Jar jar = getJar();
+		File source = jar.getSource();
+
+		trace("check for modified build=%s file=%s, diff=%s", jar.lastModified(),
+				output.lastModified(), jar.lastModified() - output.lastModified());
+
+		if (!output.exists() || output.lastModified() <= jar.lastModified() || force) {
+			output.getParentFile().mkdirs();
+			if (source != null && output.getCanonicalPath().equals(source.getCanonicalPath())) {
+				File bak = new File(source.getParentFile(), source.getName() + ".bak");
+				if (!source.renameTo(bak)) {
+					error("Could not create backup file %s", bak);
+				}
+				else
+					source.delete();
+			}
+			try {
+				trace("Saving jar to %s", output);
+				getJar().write(output);
+			}
+			catch (Exception e) {
+				output.delete();
+				error("Cannot write JAR file to %s due to %s", e, output, e.getMessage());
+			}
+			return true;
+		}
+		else {
+			trace("Not modified %s", output);
+			return false;
+		}
+	}
+
+	/**
+	 * Set default import and export instructions if none are set
+	 */
+	public void setDefaults(String bsn, Version version) {
+		if (getExportPackage() == null)
+			setExportPackage("*");
+		if (getImportPackage() == null)
+			setExportPackage("*");
+		if (bsn != null && getBundleSymbolicName() == null)
+			setBundleSymbolicName(bsn);
+		if (version != null && getBundleVersion() == null)
+			setBundleVersion(version);
+	}
+}