Sync with latest bnd code for testing purposes

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1354104 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/Container.java b/bundleplugin/src/main/java/aQute/bnd/build/Container.java
index f726527..394a14a 100644
--- a/bundleplugin/src/main/java/aQute/bnd/build/Container.java
+++ b/bundleplugin/src/main/java/aQute/bnd/build/Container.java
@@ -106,8 +106,7 @@
 	public boolean equals(Object other) {
 		if (other instanceof Container)
 			return file.equals(((Container) other).file);
-		else
-			return false;
+		return false;
 	}
 
 	public int hashCode() {
@@ -126,8 +125,7 @@
 	public String toString() {
 		if (getError() != null)
 			return "/error/" + getError();
-		else
-			return getFile().getAbsolutePath();
+		return getFile().getAbsolutePath();
 	}
 
 	public Map<String,String> getAttributes() {
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/Project.java b/bundleplugin/src/main/java/aQute/bnd/build/Project.java
index b55727a..54c2436 100644
--- a/bundleplugin/src/main/java/aQute/bnd/build/Project.java
+++ b/bundleplugin/src/main/java/aQute/bnd/build/Project.java
@@ -55,7 +55,7 @@
 	boolean						delayRunDependencies	= false;
 	final ProjectMessages		msgs					= ReporterMessages.base(this, ProjectMessages.class);
 
-	public Project(Workspace workspace, File projectDir, File buildFile) throws Exception {
+	public Project(Workspace workspace, @SuppressWarnings("unused") File projectDir, File buildFile) throws Exception {
 		super(workspace);
 		this.workspace = workspace;
 		setFileMustExist(false);
@@ -121,7 +121,8 @@
 			builder = new ProjectBuilder(parent);
 
 		builder.setBase(getBase());
-
+		builder.setPedantic(isPedantic());
+		builder.setTrace(isTrace());
 		return builder;
 	}
 
@@ -1057,8 +1058,7 @@
 		}
 		if (f.getName().endsWith("lib"))
 			return new Container(this, bsn, range, Container.TYPE.LIBRARY, f, null, attrs);
-		else
-			return new Container(this, bsn, range, Container.TYPE.REPO, f, null, attrs);
+		return new Container(this, bsn, range, Container.TYPE.REPO, f, null, attrs);
 	}
 
 	/**
@@ -1258,7 +1258,7 @@
 		}
 
 		if (isStale()) {
-			trace("Building " + this);
+			trace("building " + this);
 			files = buildLocal(underTest);
 		}
 
@@ -1366,8 +1366,8 @@
 						rdr.close();
 						f.delete();
 						break;
-					} else
-						files.add(ff);
+					}
+					files.add(ff);
 				}
 				return this.files = files.toArray(new File[files.size()]);
 			}
@@ -1377,8 +1377,7 @@
 		}
 		if (buildIfAbsent)
 			return files = buildLocal(false);
-		else
-			return files = null;
+		return files = null;
 	}
 
 	/**
@@ -1428,8 +1427,8 @@
 				}
 				getWorkspace().changedFile(bfs);
 				return files;
-			} else
-				return null;
+			}
+			return null;
 		}
 		finally {
 			builder.close();
@@ -1664,7 +1663,7 @@
 		return jar;
 	}
 
-	public String _project(String args[]) {
+	public String _project(@SuppressWarnings("unused") String args[]) {
 		return getBase().getAbsolutePath();
 	}
 
@@ -1753,7 +1752,7 @@
 	/**
 	 * Run all before command plugins
 	 */
-	void before(Project p, String a) {
+	void before(@SuppressWarnings("unused") Project p, String a) {
 		List<CommandPlugin> testPlugins = getPlugins(CommandPlugin.class);
 		for (CommandPlugin testPlugin : testPlugins) {
 			testPlugin.before(this, a);
@@ -1763,7 +1762,7 @@
 	/**
 	 * Run all after command plugins
 	 */
-	void after(Project p, String a, Throwable t) {
+	void after(@SuppressWarnings("unused") Project p, String a, Throwable t) {
 		List<CommandPlugin> testPlugins = getPlugins(CommandPlugin.class);
 		for (int i = testPlugins.size() - 1; i >= 0; i--) {
 			testPlugins.get(i).after(this, a, t);
@@ -1800,7 +1799,7 @@
 	}
 
 	@SuppressWarnings("unchecked")
-	public void script(String type, String script) throws Exception {
+	public void script(@SuppressWarnings("unused") String type, String script) throws Exception {
 		// TODO check tyiping
 		List<Scripter> scripters = getPlugins(Scripter.class);
 		if (scripters.isEmpty()) {
@@ -1812,7 +1811,7 @@
 		scripters.get(0).eval(x, new StringReader(script));
 	}
 
-	public String _repos(String args[]) throws Exception {
+	public String _repos(@SuppressWarnings("unused") String args[]) throws Exception {
 		List<RepositoryPlugin> repos = getPlugins(RepositoryPlugin.class);
 		List<String> names = new ArrayList<String>();
 		for (RepositoryPlugin rp : repos)
@@ -1906,7 +1905,7 @@
 	 * @return null or the builder for a sub file.
 	 * @throws Exception
 	 */
-	public Container getDeliverable(String bsn, Map<String,String> attrs) throws Exception {
+	public Container getDeliverable(String bsn, @SuppressWarnings("unused") Map<String,String> attrs) throws Exception {
 		Collection< ? extends Builder> builders = getSubBuilders();
 		for (Builder sub : builders) {
 			if (sub.getBsn().equals(bsn))
@@ -2040,19 +2039,18 @@
 
 		if (newVersion.compareTo(oldVersion) == 0) {
 			return;
-		} else {
-			PrintWriter pw = IO.writer(file);
-			pw.println("version " + newVersion);
-			pw.flush();
-			pw.close();
-
-			String path = packageName.replace('.', '/') + "/packageinfo";
-			File binary = IO.getFile(getOutput(), path);
-			binary.getParentFile().mkdirs();
-			IO.copy(file, binary);
-
-			refresh();
 		}
+		PrintWriter pw = IO.writer(file);
+		pw.println("version " + newVersion);
+		pw.flush();
+		pw.close();
+
+		String path = packageName.replace('.', '/') + "/packageinfo";
+		File binary = IO.getFile(getOutput(), path);
+		binary.getParentFile().mkdirs();
+		IO.copy(file, binary);
+
+		refresh();
 	}
 
 	File getPackageInfoFile(String packageName) {
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/Workspace.java b/bundleplugin/src/main/java/aQute/bnd/build/Workspace.java
index c74687f..50619c8 100644
--- a/bundleplugin/src/main/java/aQute/bnd/build/Workspace.java
+++ b/bundleplugin/src/main/java/aQute/bnd/build/Workspace.java
@@ -154,7 +154,7 @@
 		}
 	}
 
-	public String _workspace(String args[]) {
+	public String _workspace(@SuppressWarnings("unused") String args[]) {
 		return getBase().getAbsolutePath();
 	}
 
@@ -279,7 +279,6 @@
 					if (in != null)
 						unzip(in, root);
 					else {
-						System.err.println("!!!! Couldn't find embedded-repo.jar in bundle ");
 						error("Couldn't find embedded-repo.jar in bundle ");
 					}
 				}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/WorkspaceRepository.java b/bundleplugin/src/main/java/aQute/bnd/build/WorkspaceRepository.java
index 9e71975..9a1753c 100644
--- a/bundleplugin/src/main/java/aQute/bnd/build/WorkspaceRepository.java
+++ b/bundleplugin/src/main/java/aQute/bnd/build/WorkspaceRepository.java
@@ -22,7 +22,7 @@
 		this.workspace = workspace;
 	}
 
-	public File[] get(String bsn, String range) throws Exception {
+	private File[] get(String bsn, String range) throws Exception {
 		Collection<Project> projects = workspace.getAllProjects();
 		SortedMap<Version,File> foundVersion = new TreeMap<Version,File>();
 		for (Project project : projects) {
@@ -45,15 +45,13 @@
 		result = foundVersion.values().toArray(result);
 		if (!"latest".equals(range)) {
 			return result;
-		} else {
-			if (result.length > 0) {
-				return new File[] {
-					result[0]
-				};
-			} else {
-				return new File[0];
-			}
 		}
+		if (result.length > 0) {
+			return new File[] {
+				result[0]
+			};
+		}
+		return new File[0];
 	}
 
 	public File get(String bsn, String range, Strategy strategy, Map<String,String> properties) throws Exception {
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/BndEditModel.java b/bundleplugin/src/main/java/aQute/bnd/build/model/BndEditModel.java
new file mode 100644
index 0000000..8dee7e2
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/BndEditModel.java
@@ -0,0 +1,719 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Neil Bartlett.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Neil Bartlett - initial API and implementation
+ *******************************************************************************/
+package aQute.bnd.build.model;
+
+import java.beans.*;
+import java.io.*;
+import java.util.*;
+import java.util.Map.Entry;
+
+import org.osgi.framework.*;
+
+import aQute.bnd.build.model.clauses.*;
+import aQute.bnd.build.model.conversions.*;
+import aQute.lib.properties.*;
+import aQute.libg.header.*;
+import aQute.libg.tuple.*;
+import aQute.libg.version.Version;
+
+/**
+ * A model for a Bnd file. In the first iteration, use a simple Properties
+ * object; this will need to be enhanced to additionally record formatting, e.g.
+ * line breaks and empty lines, and comments.
+ * 
+ * @author Neil Bartlett
+ */
+public class BndEditModel {
+
+	public static final String										LINE_SEPARATOR				= " \\\n\t";
+	public static final String										LIST_SEPARATOR				= ",\\\n\t";
+
+	protected static final String									ISO_8859_1					= "ISO-8859-1";												//$NON-NLS-1$
+
+	protected static String[]										KNOWN_PROPERTIES			= new String[] {
+			Constants.BUNDLE_SYMBOLICNAME, Constants.BUNDLE_VERSION, Constants.BUNDLE_ACTIVATOR,
+			Constants.EXPORT_PACKAGE, Constants.IMPORT_PACKAGE, aQute.lib.osgi.Constants.PRIVATE_PACKAGE,
+			aQute.lib.osgi.Constants.SOURCES,
+			aQute.lib.osgi.Constants.SERVICE_COMPONENT, aQute.lib.osgi.Constants.CLASSPATH,
+			aQute.lib.osgi.Constants.BUILDPATH, aQute.lib.osgi.Constants.BUILDPACKAGES,
+			aQute.lib.osgi.Constants.RUNBUNDLES, aQute.lib.osgi.Constants.RUNPROPERTIES, aQute.lib.osgi.Constants.SUB,
+			// BndConstants.RUNFRAMEWORK,
+			aQute.lib.osgi.Constants.RUNVM,
+			// BndConstants.RUNVMARGS,
+			// BndConstants.TESTSUITES,
+			aQute.lib.osgi.Constants.TESTCASES, aQute.lib.osgi.Constants.PLUGIN, aQute.lib.osgi.Constants.PLUGINPATH,
+			aQute.lib.osgi.Constants.RUNREPOS,
+																								// BndConstants.RUNREQUIRE,
+																								// BndConstants.RUNEE,
+																								// BndConstants.RESOLVE_MODE
+																								};
+
+	public static final String										BUNDLE_VERSION_MACRO		= "${"
+																										+ Constants.BUNDLE_VERSION
+																										+ "}";
+
+	protected final Map<String,Converter< ? extends Object,String>>	converters					= new HashMap<String,Converter< ? extends Object,String>>();
+	protected final Map<String,Converter<String, ? extends Object>>	formatters					= new HashMap<String,Converter<String, ? extends Object>>();
+	// private final DataModelHelper obrModelHelper = new DataModelHelperImpl();
+
+	private final PropertyChangeSupport								propChangeSupport			= new PropertyChangeSupport(
+																										this);
+	private final Properties										properties					= new Properties();
+
+	private File													bndResource;
+	private boolean													projectFile;
+	private final Map<String,Object>								objectProperties			= new HashMap<String,Object>();
+	private final Map<String,String>								changesToSave				= new HashMap<String,String>();
+
+	// CONVERTERS
+	protected Converter<List<VersionedClause>,String>				buildPathConverter			= new ClauseListConverter<VersionedClause>(
+																										new Converter<VersionedClause,Pair<String,Attrs>>() {
+																											public VersionedClause convert(
+																													Pair<String,Attrs> input)
+																													throws IllegalArgumentException {
+																												return new VersionedClause(
+																														input.getFirst(),
+																														input.getSecond());
+																											}
+																										});
+	protected Converter<List<VersionedClause>,String>				buildPackagesConverter		= new ClauseListConverter<VersionedClause>(
+																										new Converter<VersionedClause,Pair<String,Attrs>>() {
+																											public VersionedClause convert(
+																													Pair<String,Attrs> input)
+																													throws IllegalArgumentException {
+																												return new VersionedClause(
+																														input.getFirst(),
+																														input.getSecond());
+																											}
+																										});
+	protected Converter<List<VersionedClause>,String>				clauseListConverter			= new ClauseListConverter<VersionedClause>(
+																										new VersionedClauseConverter());
+	protected Converter<String,String>								stringConverter				= new NoopConverter<String>();
+	protected Converter<Boolean,String>								includedSourcesConverter	= new Converter<Boolean,String>() {
+																									public Boolean convert(
+																											String string)
+																											throws IllegalArgumentException {
+																										return Boolean
+																												.valueOf(string);
+																									}
+																								};
+	protected Converter<VersionPolicy,String>						versionPolicyConverter		= new Converter<VersionPolicy,String>() {
+																									public VersionPolicy convert(
+																											String string)
+																											throws IllegalArgumentException {
+																										return VersionPolicy
+																												.parse(string);
+																									}
+																								};
+	protected Converter<List<String>,String>						listConverter				= SimpleListConverter
+																										.create();
+	protected Converter<List<HeaderClause>,String>					headerClauseListConverter	= new HeaderClauseListConverter();
+	protected ClauseListConverter<ExportedPackage>					exportPackageConverter		= new ClauseListConverter<ExportedPackage>(
+																										new Converter<ExportedPackage,Pair<String,Attrs>>() {
+																											public ExportedPackage convert(
+																													Pair<String,Attrs> input) {
+																												return new ExportedPackage(
+																														input.getFirst(),
+																														input.getSecond());
+																											}
+																										});
+	protected Converter<List<ServiceComponent>,String>				serviceComponentConverter	= new ClauseListConverter<ServiceComponent>(
+																										new Converter<ServiceComponent,Pair<String,Attrs>>() {
+																											public ServiceComponent convert(
+																													Pair<String,Attrs> input)
+																													throws IllegalArgumentException {
+																												return new ServiceComponent(
+																														input.getFirst(),
+																														input.getSecond());
+																											}
+																										});
+	protected Converter<List<ImportPattern>,String>					importPatternConverter		= new ClauseListConverter<ImportPattern>(
+																										new Converter<ImportPattern,Pair<String,Attrs>>() {
+																											public ImportPattern convert(
+																													Pair<String,Attrs> input)
+																													throws IllegalArgumentException {
+																												return new ImportPattern(
+																														input.getFirst(),
+																														input.getSecond());
+																											}
+																										});
+
+	protected Converter<Map<String,String>,String>					propertiesConverter			= new PropertiesConverter();
+
+	// Converter<List<Requirement>, String> requirementListConverter =
+	// SimpleListConverter.create(new Converter<Requirement, String>() {
+	// public Requirement convert(String input) throws IllegalArgumentException
+	// {
+	// int index = input.indexOf(":");
+	// if (index < 0)
+	// throw new IllegalArgumentException("Invalid format for OBR requirement");
+	//
+	// String name = input.substring(0, index);
+	// String filter = input.substring(index + 1);
+	//
+	// return new Requirement(name, filter);
+	// }
+	// });
+	// Converter<EE, String> eeConverter = new Converter<EE, String>() {
+	// public EE convert(String input) throws IllegalArgumentException {
+	// return EE.parse(input);
+	// }
+	// };
+	//
+	// Converter<ResolveMode, String> resolveModeConverter =
+	// EnumConverter.create(ResolveMode.class, ResolveMode.manual);
+
+	// FORMATTERS
+	protected Converter<String,Object>								defaultFormatter			= new DefaultFormatter();
+	protected Converter<String,String>								newlineEscapeFormatter		= new NewlineEscapedStringFormatter();
+	protected Converter<String,Boolean>								defaultFalseBoolFormatter	= new DefaultBooleanFormatter(
+																										false);
+	protected Converter<String,Collection< ? >>						stringListFormatter			= new CollectionFormatter<Object>(
+																										LIST_SEPARATOR,
+																										(String) null);
+	protected Converter<String,Collection< ? extends HeaderClause>>	headerClauseListFormatter	= new CollectionFormatter<HeaderClause>(
+																										LIST_SEPARATOR,
+																										new HeaderClauseFormatter(),
+																										null);
+	protected Converter<String,Map<String,String>>					propertiesFormatter			= new MapFormatter(
+																										LIST_SEPARATOR,
+																										new PropertiesEntryFormatter(),
+																										null);
+	// Converter<String, Collection<? extends Requirement>>
+	// requirementListFormatter = new
+	// CollectionFormatter<Requirement>(LIST_SEPARATOR, new Converter<String,
+	// Requirement>() {
+	// public String convert(Requirement input) throws IllegalArgumentException
+	// {
+	// return new
+	// StringBuilder().append(input.getName()).append(':').append(input.getFilter()).toString();
+	// }
+	// }, null);
+	// Converter<String, EE> eeFormatter = new Converter<String, EE>() {
+	// public String convert(EE input) throws IllegalArgumentException {
+	// return input != null ? input.getEEName() : null;
+	// }
+	// };
+	Converter<String,Collection< ? extends String>>					runReposFormatter			= new CollectionFormatter<String>(
+																										LIST_SEPARATOR,
+																										aQute.lib.osgi.Constants.EMPTY_HEADER);
+
+	// Converter<String, ResolveMode> resolveModeFormatter =
+	// EnumFormatter.create(ResolveMode.class, ResolveMode.manual);
+
+	@SuppressWarnings("deprecation")
+	public BndEditModel() {
+		// register converters
+		converters.put(aQute.lib.osgi.Constants.BUILDPATH, buildPathConverter);
+		converters.put(aQute.lib.osgi.Constants.BUILDPACKAGES, buildPackagesConverter);
+		converters.put(aQute.lib.osgi.Constants.RUNBUNDLES, clauseListConverter);
+		converters.put(Constants.BUNDLE_SYMBOLICNAME, stringConverter);
+		converters.put(Constants.BUNDLE_VERSION, stringConverter);
+		converters.put(Constants.BUNDLE_ACTIVATOR, stringConverter);
+		converters.put(aQute.lib.osgi.Constants.OUTPUT, stringConverter);
+		converters.put(aQute.lib.osgi.Constants.SOURCES, includedSourcesConverter);
+		converters.put(aQute.lib.osgi.Constants.PRIVATE_PACKAGE, listConverter);
+		converters.put(aQute.lib.osgi.Constants.CLASSPATH, listConverter);
+		converters.put(Constants.EXPORT_PACKAGE, exportPackageConverter);
+		converters.put(aQute.lib.osgi.Constants.SERVICE_COMPONENT, serviceComponentConverter);
+		converters.put(Constants.IMPORT_PACKAGE, importPatternConverter);
+		// converters.put(BndConstants.RUNFRAMEWORK, stringConverter);
+		converters.put(aQute.lib.osgi.Constants.SUB, listConverter);
+		converters.put(aQute.lib.osgi.Constants.RUNPROPERTIES, propertiesConverter);
+		converters.put(aQute.lib.osgi.Constants.RUNVM, stringConverter);
+		// converters.put(BndConstants.RUNVMARGS, stringConverter);
+		converters.put(aQute.lib.osgi.Constants.TESTSUITES, listConverter);
+		converters.put(aQute.lib.osgi.Constants.TESTCASES, listConverter);
+		converters.put(aQute.lib.osgi.Constants.PLUGIN, headerClauseListConverter);
+		// converters.put(BndConstants.RUNREQUIRE, requirementListConverter);
+		// converters.put(BndConstants.RUNEE, new NoopConverter<String>());
+		// converters.put(BndConstants.RUNREPOS, listConverter);
+		// converters.put(BndConstants.RESOLVE_MODE, resolveModeConverter);
+
+		formatters.put(aQute.lib.osgi.Constants.BUILDPATH, headerClauseListFormatter);
+		formatters.put(aQute.lib.osgi.Constants.BUILDPACKAGES, headerClauseListFormatter);
+		formatters.put(aQute.lib.osgi.Constants.RUNBUNDLES, headerClauseListFormatter);
+		formatters.put(Constants.BUNDLE_SYMBOLICNAME, newlineEscapeFormatter);
+		formatters.put(Constants.BUNDLE_VERSION, newlineEscapeFormatter);
+		formatters.put(Constants.BUNDLE_ACTIVATOR, newlineEscapeFormatter);
+		formatters.put(aQute.lib.osgi.Constants.OUTPUT, newlineEscapeFormatter);
+		formatters.put(aQute.lib.osgi.Constants.SOURCES, defaultFalseBoolFormatter);
+		formatters.put(aQute.lib.osgi.Constants.PRIVATE_PACKAGE, stringListFormatter);
+		formatters.put(aQute.lib.osgi.Constants.CLASSPATH, stringListFormatter);
+		formatters.put(Constants.EXPORT_PACKAGE, headerClauseListFormatter);
+		formatters.put(aQute.lib.osgi.Constants.SERVICE_COMPONENT, headerClauseListFormatter);
+		formatters.put(Constants.IMPORT_PACKAGE, headerClauseListFormatter);
+		// formatters.put(BndConstants.RUNFRAMEWORK, newlineEscapeFormatter);
+		formatters.put(aQute.lib.osgi.Constants.SUB, stringListFormatter);
+		formatters.put(aQute.lib.osgi.Constants.RUNPROPERTIES, propertiesFormatter);
+		formatters.put(aQute.lib.osgi.Constants.RUNVM, newlineEscapeFormatter);
+		// formatters.put(BndConstants.RUNVMARGS, newlineEscapeFormatter);
+		// formatters.put(BndConstants.TESTSUITES, stringListFormatter);
+		formatters.put(aQute.lib.osgi.Constants.TESTCASES, stringListFormatter);
+		formatters.put(aQute.lib.osgi.Constants.PLUGIN, headerClauseListFormatter);
+		// formatters.put(BndConstants.RUNREQUIRE, requirementListFormatter);
+		// formatters.put(BndConstants.RUNEE, new NoopConverter<String>());
+		// formatters.put(BndConstants.RUNREPOS, runReposFormatter);
+		// formatters.put(BndConstants.RESOLVE_MODE, resolveModeFormatter);
+	}
+
+	public void loadFrom(IDocument document) throws IOException {
+		InputStream inputStream = new ByteArrayInputStream(document.get().getBytes(ISO_8859_1));
+		loadFrom(inputStream);
+	}
+
+	public void loadFrom(File file) throws IOException {
+		loadFrom(new BufferedInputStream(new FileInputStream(file)));
+	}
+
+	public void loadFrom(InputStream inputStream) throws IOException {
+		try {
+			// Clear and load
+			properties.clear();
+			properties.load(inputStream);
+			objectProperties.clear();
+			changesToSave.clear();
+
+			// Fire property changes on all known property names
+			for (String prop : KNOWN_PROPERTIES) {
+				// null values for old and new forced the change to be fired
+				propChangeSupport.firePropertyChange(prop, null, null);
+			}
+		}
+		finally {
+			inputStream.close();
+		}
+
+	}
+
+	public void saveChangesTo(IDocument document) {
+		for (Iterator<Entry<String,String>> iter = changesToSave.entrySet().iterator(); iter.hasNext();) {
+			Entry<String,String> entry = iter.next();
+			iter.remove();
+
+			String propertyName = entry.getKey();
+			String stringValue = entry.getValue();
+
+			updateDocument(document, propertyName, stringValue);
+		}
+	}
+
+	protected static IRegion findEntry(IDocument document, String name) throws Exception {
+		PropertiesLineReader reader = new PropertiesLineReader(document);
+		LineType type = reader.next();
+		while (type != LineType.eof) {
+			if (type == LineType.entry) {
+				String key = reader.key();
+				if (name.equals(key))
+					return reader.region();
+			}
+			type = reader.next();
+		}
+		return null;
+	}
+
+	protected static void updateDocument(IDocument document, String name, String value) {
+		String newEntry;
+		if (value != null) {
+			StringBuilder buffer = new StringBuilder();
+			buffer.append(name).append(": ").append(value);
+			newEntry = buffer.toString();
+		} else {
+			newEntry = "";
+		}
+
+		try {
+			IRegion region = findEntry(document, name);
+			if (region != null) {
+				// Replace an existing entry
+				int offset = region.getOffset();
+				int length = region.getLength();
+
+				// If the replacement is empty, remove one extra character to
+				// the right, i.e. the following newline,
+				// unless this would take us past the end of the document
+				if (newEntry.length() == 0 && offset + length + 1 < document.getLength()) {
+					length++;
+				}
+				document.replace(offset, length, newEntry);
+			} else if (newEntry.length() > 0) {
+				// This is a new entry, put it at the end of the file
+
+				// Does the last line of the document have a newline? If not,
+				// we need to add one.
+				if (document.getLength() > 0 && document.getChar(document.getLength() - 1) != '\n')
+					newEntry = "\n" + newEntry;
+				document.replace(document.getLength(), 0, newEntry);
+			}
+		}
+		catch (Exception e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+	}
+
+	public List<String> getAllPropertyNames() {
+		List<String> result = new ArrayList<String>(properties.size());
+
+		Enumeration<String> names = (Enumeration<String>) properties.propertyNames();
+
+		while (names.hasMoreElements()) {
+			result.add(names.nextElement());
+		}
+		return result;
+	}
+
+	public Object genericGet(String propertyName) {
+		Converter< ? extends Object,String> converter = converters.get(propertyName);
+		if (converter == null)
+			converter = new NoopConverter<String>();
+		return doGetObject(propertyName, converter);
+	}
+
+	public void genericSet(String propertyName, Object value) {
+		Object oldValue = genericGet(propertyName);
+		Converter<String,Object> formatter = (Converter<String,Object>) formatters.get(propertyName);
+		if (formatter == null)
+			formatter = new DefaultFormatter();
+		doSetObject(propertyName, oldValue, value, formatter);
+	}
+
+	public String getBundleSymbolicName() {
+		return doGetObject(Constants.BUNDLE_SYMBOLICNAME, stringConverter);
+	}
+
+	public void setBundleSymbolicName(String bundleSymbolicName) {
+		doSetObject(Constants.BUNDLE_SYMBOLICNAME, getBundleSymbolicName(), bundleSymbolicName, newlineEscapeFormatter);
+	}
+
+	public String getBundleVersionString() {
+		return doGetObject(Constants.BUNDLE_VERSION, stringConverter);
+	}
+
+	public void setBundleVersion(String bundleVersion) {
+		doSetObject(Constants.BUNDLE_VERSION, getBundleVersionString(), bundleVersion, newlineEscapeFormatter);
+	}
+
+	public String getBundleActivator() {
+		return doGetObject(Constants.BUNDLE_ACTIVATOR, stringConverter);
+	}
+
+	public void setBundleActivator(String bundleActivator) {
+		doSetObject(Constants.BUNDLE_ACTIVATOR, getBundleActivator(), bundleActivator, newlineEscapeFormatter);
+	}
+
+	public String getOutputFile() {
+		return doGetObject(aQute.lib.osgi.Constants.OUTPUT, stringConverter);
+	}
+
+	public void setOutputFile(String name) {
+		doSetObject(aQute.lib.osgi.Constants.OUTPUT, getOutputFile(), name, newlineEscapeFormatter);
+	}
+
+	public boolean isIncludeSources() {
+		return doGetObject(aQute.lib.osgi.Constants.SOURCES, includedSourcesConverter);
+	}
+
+	public void setIncludeSources(boolean includeSources) {
+		boolean oldValue = isIncludeSources();
+		doSetObject(aQute.lib.osgi.Constants.SOURCES, oldValue, includeSources, defaultFalseBoolFormatter);
+	}
+
+	public List<String> getPrivatePackages() {
+		return doGetObject(aQute.lib.osgi.Constants.PRIVATE_PACKAGE, listConverter);
+	}
+
+	public void setPrivatePackages(List< ? extends String> packages) {
+		List<String> oldPackages = getPrivatePackages();
+		doSetObject(aQute.lib.osgi.Constants.PRIVATE_PACKAGE, oldPackages, packages, stringListFormatter);
+	}
+
+	public List<ExportedPackage> getSystemPackages() {
+		return doGetObject(aQute.lib.osgi.Constants.RUNSYSTEMPACKAGES, exportPackageConverter);
+	}
+
+	public void setSystemPackages(List< ? extends ExportedPackage> packages) {
+		List<ExportedPackage> oldPackages = getSystemPackages();
+		doSetObject(aQute.lib.osgi.Constants.RUNSYSTEMPACKAGES, oldPackages, packages, headerClauseListFormatter);
+	}
+
+	public List<String> getClassPath() {
+		return doGetObject(aQute.lib.osgi.Constants.CLASSPATH, listConverter);
+	}
+
+	public void addPrivatePackage(String packageName) {
+		List<String> packages = getPrivatePackages();
+		if (packages == null)
+			packages = new ArrayList<String>();
+		else
+			packages = new ArrayList<String>(packages);
+		packages.add(packageName);
+		setPrivatePackages(packages);
+	}
+
+	public void setClassPath(List< ? extends String> classPath) {
+		List<String> oldClassPath = getClassPath();
+		doSetObject(aQute.lib.osgi.Constants.CLASSPATH, oldClassPath, classPath, stringListFormatter);
+	}
+
+	public List<ExportedPackage> getExportedPackages() {
+		return doGetObject(Constants.EXPORT_PACKAGE, exportPackageConverter);
+	}
+
+	public void setExportedPackages(List< ? extends ExportedPackage> exports) {
+		boolean referencesBundleVersion = false;
+
+		if (exports != null) {
+			for (ExportedPackage pkg : exports) {
+				String versionString = pkg.getVersionString();
+				if (versionString != null && versionString.indexOf(BUNDLE_VERSION_MACRO) > -1) {
+					referencesBundleVersion = true;
+				}
+			}
+		}
+		List<ExportedPackage> oldValue = getExportedPackages();
+		doSetObject(Constants.EXPORT_PACKAGE, oldValue, exports, headerClauseListFormatter);
+
+		if (referencesBundleVersion && getBundleVersionString() == null) {
+			setBundleVersion(new Version(0, 0, 0).toString());
+		}
+	}
+
+	public void addExportedPackage(ExportedPackage export) {
+		List<ExportedPackage> exports = getExportedPackages();
+		exports = (exports == null) ? new ArrayList<ExportedPackage>() : new ArrayList<ExportedPackage>(exports);
+		exports.add(export);
+		setExportedPackages(exports);
+	}
+
+	public List<String> getDSAnnotationPatterns() {
+		return doGetObject(aQute.lib.osgi.Constants.DSANNOTATIONS, listConverter);
+	}
+
+	public void setDSAnnotationPatterns(List< ? extends String> patterns) {
+		List<String> oldValue = getDSAnnotationPatterns();
+		doSetObject(aQute.lib.osgi.Constants.DSANNOTATIONS, oldValue, patterns, stringListFormatter);
+	}
+
+	public List<ServiceComponent> getServiceComponents() {
+		return doGetObject(aQute.lib.osgi.Constants.SERVICE_COMPONENT, serviceComponentConverter);
+	}
+
+	public void setServiceComponents(List< ? extends ServiceComponent> components) {
+		List<ServiceComponent> oldValue = getServiceComponents();
+		doSetObject(aQute.lib.osgi.Constants.SERVICE_COMPONENT, oldValue, components, headerClauseListFormatter);
+	}
+
+	public List<ImportPattern> getImportPatterns() {
+		return doGetObject(Constants.IMPORT_PACKAGE, importPatternConverter);
+	}
+
+	public void setImportPatterns(List< ? extends ImportPattern> patterns) {
+		List<ImportPattern> oldValue = getImportPatterns();
+		doSetObject(Constants.IMPORT_PACKAGE, oldValue, patterns, headerClauseListFormatter);
+	}
+
+	public List<VersionedClause> getBuildPath() {
+		return doGetObject(aQute.lib.osgi.Constants.BUILDPATH, buildPathConverter);
+	}
+
+	public void setBuildPath(List< ? extends VersionedClause> paths) {
+		List<VersionedClause> oldValue = getBuildPath();
+		doSetObject(aQute.lib.osgi.Constants.BUILDPATH, oldValue, paths, headerClauseListFormatter);
+	}
+
+	public List<VersionedClause> getBuildPackages() {
+		return doGetObject(aQute.lib.osgi.Constants.BUILDPACKAGES, buildPackagesConverter);
+	}
+
+	public void setBuildPackages(List< ? extends VersionedClause> paths) {
+		List<VersionedClause> oldValue = getBuildPackages();
+		doSetObject(aQute.lib.osgi.Constants.BUILDPACKAGES, oldValue, paths, headerClauseListFormatter);
+	}
+
+	public List<VersionedClause> getRunBundles() {
+		return doGetObject(aQute.lib.osgi.Constants.RUNBUNDLES, clauseListConverter);
+	}
+
+	public void setRunBundles(List< ? extends VersionedClause> paths) {
+		List<VersionedClause> oldValue = getBuildPath();
+		doSetObject(aQute.lib.osgi.Constants.RUNBUNDLES, oldValue, paths, headerClauseListFormatter);
+	}
+
+	public boolean isIncludedPackage(String packageName) {
+		final Collection<String> privatePackages = getPrivatePackages();
+		if (privatePackages != null) {
+			if (privatePackages.contains(packageName))
+				return true;
+		}
+		final Collection<ExportedPackage> exportedPackages = getExportedPackages();
+		if (exportedPackages != null) {
+			for (ExportedPackage pkg : exportedPackages) {
+				if (packageName.equals(pkg.getName())) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+
+	public List<String> getSubBndFiles() {
+		return doGetObject(aQute.lib.osgi.Constants.SUB, listConverter);
+	}
+
+	public void setSubBndFiles(List<String> subBndFiles) {
+		List<String> oldValue = getSubBndFiles();
+		doSetObject(aQute.lib.osgi.Constants.SUB, oldValue, subBndFiles, stringListFormatter);
+	}
+
+	public Map<String,String> getRunProperties() {
+		return doGetObject(aQute.lib.osgi.Constants.RUNPROPERTIES, propertiesConverter);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * @see bndtools.editor.model.IBndModel#setRunProperties(java.util.Map)
+	 */
+	public void setRunProperties(Map<String,String> props) {
+		Map<String,String> old = getRunProperties();
+		doSetObject(aQute.lib.osgi.Constants.RUNPROPERTIES, old, props, propertiesFormatter);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * @see bndtools.editor.model.IBndModel#getRunVMArgs()
+	 */
+	public String getRunVMArgs() {
+		return doGetObject(aQute.lib.osgi.Constants.RUNVM, stringConverter);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * @see bndtools.editor.model.IBndModel#setRunVMArgs(java.lang.String)
+	 */
+	public void setRunVMArgs(String args) {
+		String old = getRunVMArgs();
+		doSetObject(aQute.lib.osgi.Constants.RUNVM, old, args, newlineEscapeFormatter);
+	}
+
+	@SuppressWarnings("deprecation")
+	public List<String> getTestSuites() {
+		List<String> testCases = doGetObject(aQute.lib.osgi.Constants.TESTCASES, listConverter);
+		testCases = testCases != null ? testCases : Collections.<String> emptyList();
+
+		List<String> testSuites = doGetObject(aQute.lib.osgi.Constants.TESTSUITES, listConverter);
+		testSuites = testSuites != null ? testSuites : Collections.<String> emptyList();
+
+		List<String> result = new ArrayList<String>(testCases.size() + testSuites.size());
+		result.addAll(testCases);
+		result.addAll(testSuites);
+		return result;
+	}
+
+	@SuppressWarnings("deprecation")
+	public void setTestSuites(List<String> suites) {
+		List<String> old = getTestSuites();
+		doSetObject(aQute.lib.osgi.Constants.TESTCASES, old, suites, stringListFormatter);
+		doSetObject(aQute.lib.osgi.Constants.TESTSUITES, null, null, stringListFormatter);
+	}
+
+	public List<HeaderClause> getPlugins() {
+		return doGetObject(aQute.lib.osgi.Constants.PLUGIN, headerClauseListConverter);
+	}
+
+	public void setPlugins(List<HeaderClause> plugins) {
+		List<HeaderClause> old = getPlugins();
+		doSetObject(aQute.lib.osgi.Constants.PLUGIN, old, plugins, headerClauseListFormatter);
+	}
+
+	public List<String> getPluginPath() {
+		return doGetObject(aQute.lib.osgi.Constants.PLUGINPATH, listConverter);
+	}
+
+	public void setPluginPath(List<String> pluginPath) {
+		List<String> old = getPluginPath();
+		doSetObject(aQute.lib.osgi.Constants.PLUGINPATH, old, pluginPath, stringListFormatter);
+	}
+	
+    public List<String> getRunRepos() {
+        return doGetObject(aQute.lib.osgi.Constants.RUNREPOS, listConverter);
+    }
+
+    public void setRunRepos(List<String> repos) {
+        List<String> old = getRunRepos();
+        doSetObject(aQute.lib.osgi.Constants.RUNREPOS, old, repos, runReposFormatter);
+    }
+    
+    public String getRunFramework() {
+        return doGetObject(aQute.lib.osgi.Constants.RUNFRAMEWORK, stringConverter);
+    }
+
+    public void setRunFramework(String clause) {
+        String oldValue = getRunFramework();
+        doSetObject(aQute.lib.osgi.Constants.RUNFRAMEWORK, oldValue, clause, newlineEscapeFormatter);
+    }
+
+
+	protected <R> R doGetObject(String name, Converter< ? extends R, ? super String> converter) {
+		R result;
+		if (objectProperties.containsKey(name)) {
+			R temp = (R) objectProperties.get(name);
+			result = temp;
+		} else if (changesToSave.containsKey(name)) {
+			result = converter.convert(changesToSave.get(name));
+			objectProperties.put(name, result);
+		} else if (properties.containsKey(name)) {
+			result = converter.convert(properties.getProperty(name));
+			objectProperties.put(name, result);
+		} else {
+			result = null;
+		}
+		return result;
+	}
+
+	protected <T> void doSetObject(String name, T oldValue, T newValue, Converter<String, ? super T> formatter) {
+		objectProperties.put(name, newValue);
+		changesToSave.put(name, formatter.convert(newValue));
+		propChangeSupport.firePropertyChange(name, oldValue, newValue);
+	}
+
+	public void setProjectFile(boolean projectFile) {
+		this.projectFile = projectFile;
+	}
+
+	public boolean isProjectFile() {
+		return this.projectFile;
+	}
+
+	public void addPropertyChangeListener(PropertyChangeListener listener) {
+		propChangeSupport.addPropertyChangeListener(listener);
+	}
+
+	public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+		propChangeSupport.addPropertyChangeListener(propertyName, listener);
+	}
+
+	public void removePropertyChangeListener(PropertyChangeListener listener) {
+		propChangeSupport.removePropertyChangeListener(listener);
+	}
+
+	public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+		propChangeSupport.removePropertyChangeListener(propertyName, listener);
+	}
+
+	public void setBndResource(File bndResource) {
+		this.bndResource = bndResource;
+	}
+
+	public File getBndResource() {
+		return bndResource;
+	}
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/LowerVersionMatchType.java b/bundleplugin/src/main/java/aQute/bnd/build/model/LowerVersionMatchType.java
new file mode 100644
index 0000000..dcdf969
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/LowerVersionMatchType.java
@@ -0,0 +1,34 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Neil Bartlett.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Neil Bartlett - initial API and implementation
+ *******************************************************************************/
+package aQute.bnd.build.model;
+
+public enum LowerVersionMatchType {
+	Exact("${@}"), Micro("${version;===;${@}}"), Minor("${version;==;${@}}"), Major("${version;=;${@}}");
+
+	private final String	representation;
+
+	private LowerVersionMatchType(String representation) {
+		this.representation = representation;
+	}
+
+	public String getRepresentation() {
+		return representation;
+	}
+
+	public static LowerVersionMatchType parse(String string) throws IllegalArgumentException {
+		for (LowerVersionMatchType type : LowerVersionMatchType.class.getEnumConstants()) {
+			if (type.getRepresentation().equals(string)) {
+				return type;
+			}
+		}
+		throw new IllegalArgumentException("Failed to parse version match type.");
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/UpperVersionMatchType.java b/bundleplugin/src/main/java/aQute/bnd/build/model/UpperVersionMatchType.java
new file mode 100644
index 0000000..91d97ca
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/UpperVersionMatchType.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Neil Bartlett.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Neil Bartlett - initial API and implementation
+ *******************************************************************************/
+package aQute.bnd.build.model;
+
+public enum UpperVersionMatchType {
+	Exact("${@}"), NextMicro("${version;==+;${@}}"), NextMinor("${version;=+;${@}}"), NextMajor("${version;+;${@}}");
+
+	private final String	representation;
+
+	private UpperVersionMatchType(String representation) {
+		this.representation = representation;
+	}
+
+	public String getRepresentation() {
+		return representation;
+	}
+
+	public static UpperVersionMatchType parse(String string) throws IllegalArgumentException {
+		for (UpperVersionMatchType type : UpperVersionMatchType.class.getEnumConstants()) {
+			if (type.getRepresentation().equals(string)) {
+				return type;
+			}
+		}
+		throw new IllegalArgumentException("Failed to parse version match type.");
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/VersionPolicy.java b/bundleplugin/src/main/java/aQute/bnd/build/model/VersionPolicy.java
new file mode 100644
index 0000000..3367c9f
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/VersionPolicy.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Neil Bartlett.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Neil Bartlett - initial API and implementation
+ *******************************************************************************/
+package aQute.bnd.build.model;
+
+public class VersionPolicy {
+	private final LowerVersionMatchType	lowerMatch;
+	private final UpperVersionMatchType	upperMatch;
+	private final boolean				upperInclusive;
+
+	public VersionPolicy(LowerVersionMatchType lowerMatch, UpperVersionMatchType upperMatch, boolean upperInclusive) {
+		assert lowerMatch != null;
+		this.lowerMatch = lowerMatch;
+		this.upperMatch = upperMatch;
+		this.upperInclusive = upperInclusive;
+	}
+
+	static VersionPolicy parse(String string) throws IllegalArgumentException {
+		String lowerSegment;
+		String upperSegment;
+		boolean upperInclusive;
+
+		if (string.charAt(0) == '[') {
+			int commaIndex = string.indexOf(',');
+			if (commaIndex < 0)
+				throw new IllegalArgumentException("Failed to parse version policy.");
+			lowerSegment = string.substring(1, commaIndex);
+
+			char lastChar = string.charAt(string.length() - 1);
+			if (lastChar == ')')
+				upperInclusive = false;
+			else if (lastChar == ']')
+				upperInclusive = true;
+			else
+				throw new IllegalArgumentException("Failed to parse version policy.");
+
+			upperSegment = string.substring(commaIndex + 1, string.length() - 1);
+		} else {
+			lowerSegment = string;
+			upperSegment = null;
+			upperInclusive = true;
+		}
+
+		LowerVersionMatchType lower = LowerVersionMatchType.parse(lowerSegment);
+		UpperVersionMatchType upper = upperSegment != null ? UpperVersionMatchType.parse(upperSegment) : null;
+
+		return new VersionPolicy(lower, upper, upperInclusive);
+	}
+
+	public LowerVersionMatchType getLowerMatch() {
+		return lowerMatch;
+	}
+
+	public UpperVersionMatchType getUpperMatch() {
+		return upperMatch;
+	}
+
+	public boolean isUpperInclusive() {
+		return upperInclusive;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder buffer = new StringBuilder();
+
+		if (upperMatch != null) {
+			buffer.append('[');
+			buffer.append(lowerMatch.getRepresentation());
+			buffer.append(',');
+			buffer.append(upperMatch.getRepresentation());
+			buffer.append(upperInclusive ? ']' : ')');
+		} else {
+			buffer.append(lowerMatch.getRepresentation());
+		}
+
+		return buffer.toString();
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ComponentSvcReference.java b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ComponentSvcReference.java
new file mode 100644
index 0000000..7c6700e
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ComponentSvcReference.java
@@ -0,0 +1,112 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Neil Bartlett.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Neil Bartlett - initial API and implementation
+ *******************************************************************************/
+package aQute.bnd.build.model.clauses;
+
+public class ComponentSvcReference implements Cloneable {
+
+	private String	name;
+	private String	bind;
+	private String	unbind;
+	private String	serviceClass;
+	private boolean	optional;
+	private boolean	multiple;
+	private boolean	dynamic;
+	private String	targetFilter;
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getBind() {
+		return bind;
+	}
+
+	public void setBind(String bind) {
+		this.bind = bind;
+	}
+
+	public String getUnbind() {
+		return unbind;
+	}
+
+	public void setUnbind(String unbind) {
+		this.unbind = unbind;
+	}
+
+	public String getServiceClass() {
+		return serviceClass;
+	}
+
+	public void setServiceClass(String serviceClass) {
+		this.serviceClass = serviceClass;
+	}
+
+	public boolean isOptional() {
+		return optional;
+	}
+
+	public void setOptional(boolean optional) {
+		this.optional = optional;
+	}
+
+	public boolean isMultiple() {
+		return multiple;
+	}
+
+	public void setMultiple(boolean multiple) {
+		this.multiple = multiple;
+	}
+
+	public boolean isDynamic() {
+		return dynamic;
+	}
+
+	public void setDynamic(boolean dynamic) {
+		this.dynamic = dynamic;
+	}
+
+	public String getTargetFilter() {
+		return targetFilter;
+	}
+
+	public void setTargetFilter(String targetFilter) {
+		this.targetFilter = targetFilter;
+	}
+
+	@Override
+	public ComponentSvcReference clone() {
+		ComponentSvcReference copy = new ComponentSvcReference();
+		copy.name = this.name;
+		copy.serviceClass = this.serviceClass;
+		copy.bind = this.bind;
+		copy.unbind = this.unbind;
+		copy.optional = this.optional;
+		copy.multiple = this.multiple;
+		copy.dynamic = this.dynamic;
+		copy.targetFilter = this.targetFilter;
+		return copy;
+	}
+
+	public void copyFrom(ComponentSvcReference other) {
+		this.name = other.name;
+		this.serviceClass = other.serviceClass;
+		this.bind = other.bind;
+		this.unbind = other.unbind;
+		this.optional = other.optional;
+		this.multiple = other.multiple;
+		this.dynamic = other.dynamic;
+		this.targetFilter = other.targetFilter;
+	}
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ExportedPackage.java b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ExportedPackage.java
new file mode 100644
index 0000000..0e9be04
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ExportedPackage.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Neil Bartlett.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Neil Bartlett - initial API and implementation
+ *******************************************************************************/
+package aQute.bnd.build.model.clauses;
+
+import org.osgi.framework.Constants;
+
+import aQute.libg.header.Attrs;
+
+public class ExportedPackage extends HeaderClause {
+
+	public ExportedPackage(String packageName, Attrs attribs) {
+		super(packageName, attribs);
+	}
+
+	@Override
+	protected boolean newlinesBetweenAttributes() {
+		return false;
+	}
+
+	public void setVersionString(String version) {
+		attribs.put(Constants.VERSION_ATTRIBUTE, version);
+	}
+
+	public String getVersionString() {
+		return attribs.get(Constants.VERSION_ATTRIBUTE);
+	}
+
+	public boolean isProvided() {
+		return Boolean.valueOf(attribs.get(aQute.lib.osgi.Constants.PROVIDE_DIRECTIVE));
+	}
+
+	public void setProvided(boolean provided) {
+		if (provided)
+			attribs.put(aQute.lib.osgi.Constants.PROVIDE_DIRECTIVE, Boolean.toString(true));
+		else
+			attribs.remove(aQute.lib.osgi.Constants.PROVIDE_DIRECTIVE);
+	}
+
+	@Override
+	public ExportedPackage clone() {
+		return new ExportedPackage(this.name, new Attrs(this.attribs));
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/HeaderClause.java b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/HeaderClause.java
new file mode 100644
index 0000000..b0391a6
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/HeaderClause.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Neil Bartlett.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Neil Bartlett - initial API and implementation
+ *******************************************************************************/
+package aQute.bnd.build.model.clauses;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+
+import aQute.libg.header.Attrs;
+
+public class HeaderClause implements Cloneable, Comparable<HeaderClause> {
+
+	private static final String	INTERNAL_LIST_SEPARATOR				= ";";
+	private static final String	INTERNAL_LIST_SEPARATOR_NEWLINES	= INTERNAL_LIST_SEPARATOR + "\\\n\t\t";
+
+	protected String			name;
+	protected Attrs				attribs;
+
+	public HeaderClause(String name, Attrs attribs) {
+		assert name != null;
+		assert attribs != null;
+
+		this.name = name;
+		this.attribs = attribs;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public Attrs getAttribs() {
+		return attribs;
+	}
+
+	public List<String> getListAttrib(String attrib) {
+		String string = attribs.get(attrib);
+		if (string == null)
+			return null;
+
+		List<String> result = new ArrayList<String>();
+		StringTokenizer tokenizer = new StringTokenizer(string, ",");
+		while (tokenizer.hasMoreTokens()) {
+			result.add(tokenizer.nextToken().trim());
+		}
+
+		return result;
+	}
+
+	public void setListAttrib(String attrib, Collection< ? extends String> value) {
+		if (value == null || value.isEmpty())
+			attribs.remove(attrib);
+		else {
+			StringBuilder buffer = new StringBuilder();
+			boolean first = true;
+			for (String string : value) {
+				if (!first)
+					buffer.append(',');
+				buffer.append(string);
+				first = false;
+			}
+			attribs.put(attrib, buffer.toString());
+		}
+	}
+
+	public void formatTo(StringBuilder buffer) {
+		formatTo(buffer, null);
+	}
+
+	public void formatTo(StringBuilder buffer, Comparator<Entry<String,String>> sorter) {
+		String separator = newlinesBetweenAttributes() ? INTERNAL_LIST_SEPARATOR_NEWLINES : INTERNAL_LIST_SEPARATOR;
+		buffer.append(name);
+		if (attribs != null) {
+			Set<Entry<String,String>> set;
+			if (sorter != null) {
+				set = new TreeSet<Map.Entry<String,String>>(sorter);
+				set.addAll(attribs.entrySet());
+			} else {
+				set = attribs.entrySet();
+			}
+
+			for (Iterator<Entry<String,String>> iter = set.iterator(); iter.hasNext();) {
+				Entry<String,String> entry = iter.next();
+				String name = entry.getKey();
+				String value = entry.getValue();
+
+				if (value != null && value.length() > 0) {
+					buffer.append(separator);
+
+					// If the value contains any comma or equals, then quote the
+					// whole thing
+					if (value.indexOf(',') > -1 || value.indexOf('=') > -1)
+						value = "'" + value + "'";
+
+					buffer.append(name).append('=').append(value);
+				}
+			}
+		}
+	}
+
+	protected boolean newlinesBetweenAttributes() {
+		return false;
+	}
+
+	@Override
+	public HeaderClause clone() {
+		try {
+			HeaderClause clone = (HeaderClause) super.clone();
+			clone.name = this.name;
+			clone.attribs = new Attrs(this.attribs);
+			return clone;
+		}
+		catch (CloneNotSupportedException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public int compareTo(HeaderClause other) {
+		return this.name.compareTo(other.name);
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((attribs == null) ? 0 : attribs.hashCode());
+		result = prime * result + ((name == null) ? 0 : name.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;
+		HeaderClause other = (HeaderClause) obj;
+		if (attribs == null) {
+			if (other.attribs != null)
+				return false;
+		} else if (!attribs.isEqual(other.attribs))
+			return false;
+		if (name == null) {
+			if (other.name != null)
+				return false;
+		} else if (!name.equals(other.name))
+			return false;
+		return true;
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ImportPattern.java b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ImportPattern.java
new file mode 100644
index 0000000..5e0854c
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ImportPattern.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Neil Bartlett.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Neil Bartlett - initial API and implementation
+ *******************************************************************************/
+package aQute.bnd.build.model.clauses;
+
+import org.osgi.framework.Constants;
+
+import aQute.libg.header.Attrs;
+
+public class ImportPattern extends VersionedClause implements Cloneable {
+
+	public ImportPattern(String pattern, Attrs attributes) {
+		super(pattern, attributes);
+	}
+
+	public boolean isOptional() {
+		String resolution = attribs.get(aQute.lib.osgi.Constants.RESOLUTION_DIRECTIVE);
+		return Constants.RESOLUTION_OPTIONAL.equals(resolution);
+	}
+
+	public void setOptional(boolean optional) {
+		if (optional)
+			attribs.put(aQute.lib.osgi.Constants.RESOLUTION_DIRECTIVE, Constants.RESOLUTION_OPTIONAL);
+		else
+			attribs.remove(aQute.lib.osgi.Constants.RESOLUTION_DIRECTIVE);
+	}
+
+	@Override
+	public ImportPattern clone() {
+		return new ImportPattern(this.name, new Attrs(this.attribs));
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ServiceComponent.java b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ServiceComponent.java
new file mode 100644
index 0000000..69f8610
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/ServiceComponent.java
@@ -0,0 +1,266 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Neil Bartlett.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Neil Bartlett - initial API and implementation
+ *******************************************************************************/
+package aQute.bnd.build.model.clauses;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import aQute.libg.header.Attrs;
+
+public class ServiceComponent extends HeaderClause implements Cloneable {
+
+	// v1.0.0 attributes
+	// public final static String COMPONENT_NAME = "name:";
+	public final static String		COMPONENT_FACTORY				= "factory:";
+	public final static String		COMPONENT_SERVICEFACTORY		= "servicefactory:";
+	public final static String		COMPONENT_IMMEDIATE				= "immediate:";
+	public final static String		COMPONENT_ENABLED				= "enabled:";
+
+	public final static String		COMPONENT_DYNAMIC				= "dynamic:";
+	public final static String		COMPONENT_MULTIPLE				= "multiple:";
+	public final static String		COMPONENT_PROVIDE				= "provide:";
+	public final static String		COMPONENT_OPTIONAL				= "optional:";
+	public final static String		COMPONENT_PROPERTIES			= "properties:";
+	// public final static String COMPONENT_IMPLEMENTATION = "implementation:";
+
+	// v1.1.0 attributes
+	public final static String		COMPONENT_VERSION				= "version:";
+	public final static String		COMPONENT_CONFIGURATION_POLICY	= "configuration-policy:";
+	public final static String		COMPONENT_MODIFIED				= "modified:";
+	public final static String		COMPONENT_ACTIVATE				= "activate:";
+	public final static String		COMPONENT_DEACTIVATE			= "deactivate:";
+
+	private final static Pattern	REFERENCE_PATTERN				= Pattern.compile("([^(]+)(\\(.+\\))?");
+
+	public ServiceComponent(String name, Attrs attribs) {
+		super(name, attribs);
+	}
+
+	public boolean isPath() {
+		return name.indexOf('/') >= 0 || name.endsWith(".xml");
+	}
+
+	private Set<String> getStringSet(String attrib) {
+		List<String> list = getListAttrib(attrib);
+		return list != null ? new HashSet<String>(list) : new HashSet<String>();
+	}
+
+	public void setPropertiesMap(Map<String,String> properties) {
+		List<String> strings = new ArrayList<String>(properties.size());
+		for (Entry<String,String> entry : properties.entrySet()) {
+			String line = new StringBuilder().append(entry.getKey()).append("=").append(entry.getValue()).toString();
+			strings.add(line);
+		}
+		setListAttrib(COMPONENT_PROPERTIES, strings);
+	}
+
+	public Map<String,String> getPropertiesMap() {
+		Map<String,String> result = new LinkedHashMap<String,String>();
+
+		List<String> list = getListAttrib(COMPONENT_PROPERTIES);
+		if (list != null) {
+			for (String entryStr : list) {
+				String name;
+				String value;
+
+				int index = entryStr.lastIndexOf('=');
+				if (index == -1) {
+					name = entryStr;
+					value = null;
+				} else {
+					name = entryStr.substring(0, index);
+					value = entryStr.substring(index + 1);
+				}
+
+				result.put(name, value);
+			}
+		}
+
+		return result;
+	}
+
+	public void setSvcRefs(List< ? extends ComponentSvcReference> refs) {
+		// First remove all existing references, i.e. non-directives
+		for (Iterator<String> iter = attribs.keySet().iterator(); iter.hasNext();) {
+			String name = iter.next();
+			if (!name.endsWith(":")) {
+				iter.remove();
+			}
+		}
+
+		// Add in the references
+		Set<String> dynamic = new HashSet<String>();
+		Set<String> optional = new HashSet<String>();
+		Set<String> multiple = new HashSet<String>();
+		for (ComponentSvcReference ref : refs) {
+			// Build the reference name with bind and unbind
+			String expandedRefName = ref.getName();
+			if (ref.getBind() != null) {
+				expandedRefName += "/" + ref.getBind();
+				if (ref.getUnbind() != null) {
+					expandedRefName += "/" + ref.getUnbind();
+				}
+			}
+
+			// Start building the map value
+			StringBuilder buffer = new StringBuilder();
+			buffer.append(ref.getServiceClass());
+
+			// Add the target filter
+			if (ref.getTargetFilter() != null) {
+				buffer.append('(').append(ref.getTargetFilter()).append(')');
+			}
+
+			// Work out the cardinality suffix (i.e. *, +, ? org ~).
+			// Adding to the dynamic/multiple/optional lists for non-standard
+			// cases
+			String cardinalitySuffix;
+			if (ref.isDynamic()) {
+				if (ref.isOptional()) {
+					if (ref.isMultiple()) // 0..n dynamic
+						cardinalitySuffix = "*";
+					else
+						// 0..1 dynamic
+						cardinalitySuffix = "?";
+				} else {
+					if (ref.isMultiple()) // 1..n dynamic
+						cardinalitySuffix = "+";
+					else { // 1..1 dynamic, not a normal combination
+						cardinalitySuffix = null;
+						dynamic.add(ref.getName());
+					}
+				}
+			} else {
+				if (ref.isOptional()) {
+					if (ref.isMultiple()) { // 0..n static, not a normal
+											// combination
+						cardinalitySuffix = null;
+						optional.add(ref.getName());
+						multiple.add(ref.getName());
+					} else { // 0..1 static
+						cardinalitySuffix = "~";
+					}
+				} else {
+					if (ref.isMultiple()) { // 1..n static, not a normal
+											// combination
+						multiple.add(ref.getName());
+						cardinalitySuffix = null;
+					} else { // 1..1 static
+						cardinalitySuffix = null;
+					}
+				}
+			}
+
+			if (cardinalitySuffix != null)
+				buffer.append(cardinalitySuffix);
+
+			// Write to the map
+			attribs.put(expandedRefName, buffer.toString());
+		}
+		setListAttrib(COMPONENT_OPTIONAL, optional);
+		setListAttrib(COMPONENT_MULTIPLE, multiple);
+		setListAttrib(COMPONENT_DYNAMIC, dynamic);
+	}
+
+	public List<ComponentSvcReference> getSvcRefs() {
+		List<ComponentSvcReference> result = new ArrayList<ComponentSvcReference>();
+
+		Set<String> dynamicSet = getStringSet(COMPONENT_DYNAMIC);
+		Set<String> optionalSet = getStringSet(COMPONENT_OPTIONAL);
+		Set<String> multipleSet = getStringSet(COMPONENT_MULTIPLE);
+
+		for (Entry<String,String> entry : attribs.entrySet()) {
+			String referenceName = entry.getKey();
+
+			// Skip directives
+			if (referenceName.endsWith(":"))//$NON-NLS-1$
+				continue;
+
+			ComponentSvcReference svcRef = new ComponentSvcReference();
+
+			String bind = null;
+			String unbind = null;
+
+			if (referenceName.indexOf('/') >= 0) {
+				String parts[] = referenceName.split("/");
+				referenceName = parts[0];
+				bind = parts[1];
+				if (parts.length > 2)
+					unbind = parts[2];
+				/*
+				 * else if (bind.startsWith("add")) unbind =
+				 * bind.replaceAll("add(.+)", "remove$1"); else unbind = "un" +
+				 * bind; } else if
+				 * (Character.isLowerCase(referenceName.charAt(0))) { bind =
+				 * "set" + Character.toUpperCase(referenceName.charAt(0)) +
+				 * referenceName.substring(1); unbind = "un" + bind;
+				 */
+			}
+			svcRef.setName(referenceName);
+			svcRef.setBind(bind);
+			svcRef.setUnbind(unbind);
+
+			String interfaceName = entry.getValue();
+			if (interfaceName == null || interfaceName.length() == 0) {
+				continue;
+			}
+			svcRef.setServiceClass(interfaceName);
+
+			// Check the cardinality by looking at the last
+			// character of the value
+			char c = interfaceName.charAt(interfaceName.length() - 1);
+			if ("?+*~".indexOf(c) >= 0) {
+				if (c == '?' || c == '*' || c == '~')
+					optionalSet.add(referenceName);
+				if (c == '+' || c == '*')
+					multipleSet.add(referenceName);
+				if (c == '+' || c == '*' || c == '?')
+					dynamicSet.add(referenceName);
+				interfaceName = interfaceName.substring(0, interfaceName.length() - 1);
+			}
+			svcRef.setOptional(optionalSet.contains(referenceName));
+			svcRef.setMultiple(multipleSet.contains(referenceName));
+			svcRef.setDynamic(dynamicSet.contains(referenceName));
+
+			// Parse the target from the interface name
+			// The target is a filter.
+			String target = null;
+			Matcher m = REFERENCE_PATTERN.matcher(interfaceName);
+			if (m.matches()) {
+				interfaceName = m.group(1);
+				target = m.group(2);
+			}
+			svcRef.setTargetFilter(target);
+
+			result.add(svcRef);
+		}
+
+		return result;
+	}
+
+	@Override
+	public ServiceComponent clone() {
+		return new ServiceComponent(this.name, new Attrs(this.attribs));
+	}
+
+	@Override
+	protected boolean newlinesBetweenAttributes() {
+		return true;
+	}
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/VersionedClause.java b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/VersionedClause.java
new file mode 100644
index 0000000..a6e79a9
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/VersionedClause.java
@@ -0,0 +1,27 @@
+package aQute.bnd.build.model.clauses;
+
+import org.osgi.framework.Constants;
+
+import aQute.libg.header.Attrs;
+
+public class VersionedClause extends HeaderClause implements Cloneable {
+	public VersionedClause(String name, Attrs attribs) {
+		super(name, attribs);
+	}
+
+	public String getVersionRange() {
+		return attribs.get(Constants.VERSION_ATTRIBUTE);
+	}
+
+	public void setVersionRange(String versionRangeString) {
+		attribs.put(Constants.VERSION_ATTRIBUTE, versionRangeString);
+	}
+
+	@Override
+	public VersionedClause clone() {
+		VersionedClause clone = (VersionedClause) super.clone();
+		clone.name = this.name;
+		clone.attribs = new Attrs(this.attribs);
+		return clone;
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/packageinfo b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/packageinfo
new file mode 100644
index 0000000..55af8e5
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/clauses/packageinfo
@@ -0,0 +1 @@
+version 2
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/ClauseListConverter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/ClauseListConverter.java
new file mode 100644
index 0000000..130b60b
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/ClauseListConverter.java
@@ -0,0 +1,31 @@
+package aQute.bnd.build.model.conversions;
+
+import java.util.*;
+import java.util.Map.Entry;
+
+import aQute.lib.osgi.*;
+import aQute.libg.header.*;
+import aQute.libg.tuple.*;
+
+public class ClauseListConverter<R> implements Converter<List<R>,String> {
+
+	private final Converter< ? extends R, ? super Pair<String,Attrs>>	itemConverter;
+
+	public ClauseListConverter(Converter< ? extends R, ? super Pair<String,Attrs>> itemConverter) {
+		this.itemConverter = itemConverter;
+	}
+
+	public List<R> convert(String input) throws IllegalArgumentException {
+		List<R> result = new ArrayList<R>();
+
+		Parameters header = new Parameters(input);
+		for (Entry<String,Attrs> entry : header.entrySet()) {
+			String key = Processor.removeDuplicateMarker(entry.getKey());
+			Pair<String,Attrs> pair = Pair.newInstance(key, entry.getValue());
+			result.add(itemConverter.convert(pair));
+		}
+
+		return result;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/CollectionFormatter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/CollectionFormatter.java
new file mode 100644
index 0000000..d9f345a
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/CollectionFormatter.java
@@ -0,0 +1,47 @@
+package aQute.bnd.build.model.conversions;
+
+import java.util.*;
+
+public class CollectionFormatter<T> implements Converter<String,Collection< ? extends T>> {
+
+	private final String						separator;
+	private final Converter<String, ? super T>	itemFormatter;
+	private final String						emptyOutput;
+
+	public CollectionFormatter(String separator) {
+		this(separator, (String) null);
+	}
+
+	public CollectionFormatter(String separator, String emptyOutput) {
+		this(separator, new DefaultFormatter(), emptyOutput);
+	}
+
+	public CollectionFormatter(String separator, Converter<String, ? super T> itemFormatter) {
+		this(separator, itemFormatter, null);
+	}
+
+	public CollectionFormatter(String separator, Converter<String, ? super T> itemFormatter, String emptyOutput) {
+		this.separator = separator;
+		this.itemFormatter = itemFormatter;
+		this.emptyOutput = emptyOutput;
+	}
+
+	public String convert(Collection< ? extends T> input) throws IllegalArgumentException {
+		String result = null;
+		if (input != null) {
+			if (input.isEmpty()) {
+				result = emptyOutput;
+			} else {
+				StringBuilder buffer = new StringBuilder();
+				for (Iterator< ? extends T> iter = input.iterator(); iter.hasNext();) {
+					T item = iter.next();
+					buffer.append(itemFormatter.convert(item));
+					if (iter.hasNext())
+						buffer.append(separator);
+				}
+				result = buffer.toString();
+			}
+		}
+		return result;
+	}
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/Converter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/Converter.java
new file mode 100644
index 0000000..8513f52
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/Converter.java
@@ -0,0 +1,5 @@
+package aQute.bnd.build.model.conversions;
+
+public interface Converter<R, T> {
+	R convert(T input) throws IllegalArgumentException;
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/DefaultBooleanFormatter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/DefaultBooleanFormatter.java
new file mode 100644
index 0000000..f188569
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/DefaultBooleanFormatter.java
@@ -0,0 +1,26 @@
+package aQute.bnd.build.model.conversions;
+
+/**
+ * Formatter for booleans with a default value; if the input value matches the
+ * default then it is formatted to <code>null</code>.
+ * 
+ * @author Neil Bartlett
+ */
+public class DefaultBooleanFormatter implements Converter<String,Boolean> {
+
+	private final boolean	defaultValue;
+
+	public DefaultBooleanFormatter(boolean defaultValue) {
+		this.defaultValue = defaultValue;
+	}
+
+	public String convert(Boolean input) throws IllegalArgumentException {
+		String result = null;
+
+		if (input != null && input.booleanValue() != defaultValue)
+			result = input.toString();
+
+		return result;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/DefaultFormatter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/DefaultFormatter.java
new file mode 100644
index 0000000..a552c0d
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/DefaultFormatter.java
@@ -0,0 +1,9 @@
+package aQute.bnd.build.model.conversions;
+
+public class DefaultFormatter implements Converter<String,Object> {
+
+	public String convert(Object input) throws IllegalArgumentException {
+		return input == null ? null : input.toString();
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/EnumConverter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/EnumConverter.java
new file mode 100644
index 0000000..4c94d3b
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/EnumConverter.java
@@ -0,0 +1,27 @@
+package aQute.bnd.build.model.conversions;
+
+public class EnumConverter<E extends Enum<E>> implements Converter<E,String> {
+
+	private final Class<E>	enumType;
+	private final E			defaultValue;
+
+	public static <E extends Enum<E>> EnumConverter<E> create(Class<E> enumType) {
+		return new EnumConverter<E>(enumType, null);
+	}
+
+	public static <E extends Enum<E>> EnumConverter<E> create(Class<E> enumType, E defaultValue) {
+		return new EnumConverter<E>(enumType, defaultValue);
+	}
+
+	private EnumConverter(Class<E> enumType, E defaultValue) {
+		this.enumType = enumType;
+		this.defaultValue = defaultValue;
+	}
+
+	public E convert(String input) throws IllegalArgumentException {
+		if (input == null)
+			return defaultValue;
+		return Enum.valueOf(enumType, input);
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/EnumFormatter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/EnumFormatter.java
new file mode 100644
index 0000000..8500a25
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/EnumFormatter.java
@@ -0,0 +1,53 @@
+package aQute.bnd.build.model.conversions;
+
+/**
+ * Formats an enum type. Outputs {@code null} when the value of the enum is
+ * equal to a default value.
+ * 
+ * @param <E>
+ * @author Neil Bartlett
+ */
+public class EnumFormatter<E extends Enum<E>> implements Converter<String,E> {
+
+	private final E			defaultValue;
+
+	/**
+	 * Construct a new formatter with no default value, i.e. any non-null value
+	 * of the enum will print that value.
+	 * 
+	 * @param enumType
+	 *            The enum type.
+	 * @return
+	 */
+	public static <E extends Enum<E>> EnumFormatter<E> create(Class<E> enumType) {
+		return new EnumFormatter<E>(null);
+	}
+
+	/**
+	 * Construct a new formatter with the specified default value.
+	 * 
+	 * @param enumType
+	 *            The enum type.
+	 * @param defaultValue
+	 *            The default value, which will never be output.
+	 * @return
+	 */
+	public static <E extends Enum<E>> EnumFormatter<E> create(Class<E> enumType, E defaultValue) {
+		return new EnumFormatter<E>(defaultValue);
+	}
+
+	private EnumFormatter(E defaultValue) {
+		this.defaultValue = defaultValue;
+	}
+
+	public String convert(E input) throws IllegalArgumentException {
+		String result;
+		if (input == defaultValue || input == null)
+			result = null;
+		else {
+			result = input.toString();
+		}
+		return result;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/HeaderClauseConverter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/HeaderClauseConverter.java
new file mode 100644
index 0000000..dfb3a5d
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/HeaderClauseConverter.java
@@ -0,0 +1,13 @@
+package aQute.bnd.build.model.conversions;
+
+import aQute.bnd.build.model.clauses.*;
+import aQute.libg.header.*;
+import aQute.libg.tuple.*;
+
+public class HeaderClauseConverter implements Converter<HeaderClause,Pair<String,Attrs>> {
+
+	public HeaderClause convert(Pair<String,Attrs> input) throws IllegalArgumentException {
+		return new HeaderClause(input.getFirst(), input.getSecond());
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/HeaderClauseFormatter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/HeaderClauseFormatter.java
new file mode 100644
index 0000000..f4161f5
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/HeaderClauseFormatter.java
@@ -0,0 +1,11 @@
+package aQute.bnd.build.model.conversions;
+
+import aQute.bnd.build.model.clauses.*;
+
+public class HeaderClauseFormatter implements Converter<String,HeaderClause> {
+	public String convert(HeaderClause input) throws IllegalArgumentException {
+		StringBuilder buffer = new StringBuilder();
+		input.formatTo(buffer);
+		return buffer.toString();
+	}
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/HeaderClauseListConverter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/HeaderClauseListConverter.java
new file mode 100644
index 0000000..65d8471
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/HeaderClauseListConverter.java
@@ -0,0 +1,11 @@
+package aQute.bnd.build.model.conversions;
+
+import aQute.bnd.build.model.clauses.*;
+
+public class HeaderClauseListConverter extends ClauseListConverter<HeaderClause> {
+
+	public HeaderClauseListConverter() {
+		super(new HeaderClauseConverter());
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/MapFormatter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/MapFormatter.java
new file mode 100644
index 0000000..03d6f71
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/MapFormatter.java
@@ -0,0 +1,18 @@
+package aQute.bnd.build.model.conversions;
+
+import java.util.*;
+import java.util.Map.Entry;
+
+public class MapFormatter implements Converter<String,Map<String,String>> {
+
+	private CollectionFormatter<Entry<String,String>>	entrySetFormatter;
+
+	public MapFormatter(String listSeparator, Converter<String, ? super Entry<String,String>> entryFormatter,
+			String emptyOutput) {
+		entrySetFormatter = new CollectionFormatter<Entry<String,String>>(listSeparator, entryFormatter, emptyOutput);
+	}
+
+	public String convert(Map<String,String> input) throws IllegalArgumentException {
+		return entrySetFormatter.convert(input.entrySet());
+	}
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/NewlineEscapedStringFormatter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/NewlineEscapedStringFormatter.java
new file mode 100644
index 0000000..790cda0
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/NewlineEscapedStringFormatter.java
@@ -0,0 +1,33 @@
+package aQute.bnd.build.model.conversions;
+
+import aQute.bnd.build.model.*;
+
+public class NewlineEscapedStringFormatter implements Converter<String,String> {
+
+	public String convert(String input) throws IllegalArgumentException {
+		if (input == null)
+			return null;
+
+		// Shortcut the result for the majority of cases where there is no
+		// newline
+		if (input.indexOf('\n') == -1)
+			return input;
+
+		// Build a new string with newlines escaped
+		StringBuilder result = new StringBuilder();
+		int position = 0;
+		while (position < input.length()) {
+			int newlineIndex = input.indexOf('\n', position);
+			if (newlineIndex == -1) {
+				result.append(input.substring(position));
+				break;
+			}
+			result.append(input.substring(position, newlineIndex));
+			result.append(BndEditModel.LINE_SEPARATOR);
+			position = newlineIndex + 1;
+		}
+
+		return result.toString();
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/NoopConverter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/NoopConverter.java
new file mode 100644
index 0000000..7a9f39d
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/NoopConverter.java
@@ -0,0 +1,7 @@
+package aQute.bnd.build.model.conversions;
+
+public class NoopConverter<T> implements Converter<T,T> {
+	public T convert(T input) throws IllegalArgumentException {
+		return input;
+	}
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/PropertiesConverter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/PropertiesConverter.java
new file mode 100644
index 0000000..a1ecf5f
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/PropertiesConverter.java
@@ -0,0 +1,13 @@
+package aQute.bnd.build.model.conversions;
+
+import java.util.*;
+
+import aQute.libg.header.*;
+
+public class PropertiesConverter implements Converter<Map<String,String>,String> {
+
+	public Map<String,String> convert(String input) throws IllegalArgumentException {
+		return OSGiHeader.parseProperties(input);
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/PropertiesEntryFormatter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/PropertiesEntryFormatter.java
new file mode 100644
index 0000000..b487190
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/PropertiesEntryFormatter.java
@@ -0,0 +1,26 @@
+package aQute.bnd.build.model.conversions;
+
+import java.util.Map.Entry;
+
+public class PropertiesEntryFormatter implements Converter<String,Entry<String,String>> {
+	public String convert(Entry<String,String> entry) {
+		StringBuilder buffer = new StringBuilder();
+
+		String name = entry.getKey();
+		buffer.append(name).append('=');
+
+		String value = entry.getValue();
+		if (value != null && value.length() > 0) {
+			int quotableIndex = value.indexOf(',');
+			if (quotableIndex == -1)
+				quotableIndex = value.indexOf('=');
+
+			if (quotableIndex >= 0) {
+				buffer.append('\'').append(value).append('\'');
+			} else {
+				buffer.append(value);
+			}
+		}
+		return buffer.toString();
+	}
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/SimpleListConverter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/SimpleListConverter.java
new file mode 100644
index 0000000..2291be3
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/SimpleListConverter.java
@@ -0,0 +1,41 @@
+package aQute.bnd.build.model.conversions;
+
+import java.util.*;
+
+import aQute.lib.osgi.*;
+import aQute.libg.qtokens.*;
+
+public class SimpleListConverter<R> implements Converter<List<R>,String> {
+
+	private Converter< ? extends R, ? super String>	itemConverter;
+
+	public static <R> Converter<List<R>,String> create(Converter<R, ? super String> itemConverter) {
+		return new SimpleListConverter<R>(itemConverter);
+	}
+
+	public static Converter<List<String>,String> create() {
+		return new SimpleListConverter<String>(new NoopConverter<String>());
+	}
+
+	private SimpleListConverter(Converter< ? extends R, ? super String> itemConverter) {
+		this.itemConverter = itemConverter;
+	}
+
+	public List<R> convert(String input) throws IllegalArgumentException {
+		List<R> result = new ArrayList<R>();
+
+		if (Constants.EMPTY_HEADER.equalsIgnoreCase(input.trim()))
+			return result;
+
+		QuotedTokenizer qt = new QuotedTokenizer(input, ",");
+		String token = qt.nextToken();
+
+		while (token != null) {
+			result.add(itemConverter.convert(token.trim()));
+			token = qt.nextToken();
+		}
+
+		return result;
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/StringEntryConverter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/StringEntryConverter.java
new file mode 100644
index 0000000..c857ddc
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/StringEntryConverter.java
@@ -0,0 +1,11 @@
+package aQute.bnd.build.model.conversions;
+
+import java.util.Map.Entry;
+
+public class StringEntryConverter implements Converter<String,Entry<String, ? >> {
+
+	public String convert(Entry<String, ? > input) throws IllegalArgumentException {
+		return input.getKey();
+	}
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/VersionedClauseConverter.java b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/VersionedClauseConverter.java
new file mode 100644
index 0000000..5aead1c
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/VersionedClauseConverter.java
@@ -0,0 +1,11 @@
+package aQute.bnd.build.model.conversions;
+
+import aQute.bnd.build.model.clauses.*;
+import aQute.libg.header.*;
+import aQute.libg.tuple.*;
+
+public class VersionedClauseConverter implements Converter<VersionedClause,Pair<String,Attrs>> {
+	public VersionedClause convert(Pair<String,Attrs> input) throws IllegalArgumentException {
+		return new VersionedClause(input.getFirst(), input.getSecond());
+	}
+}
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/packageinfo b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/packageinfo
new file mode 100644
index 0000000..55af8e5
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/conversions/packageinfo
@@ -0,0 +1 @@
+version 2
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/model/packageinfo b/bundleplugin/src/main/java/aQute/bnd/build/model/packageinfo
new file mode 100644
index 0000000..55af8e5
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/build/model/packageinfo
@@ -0,0 +1 @@
+version 2
\ No newline at end of file
diff --git a/bundleplugin/src/main/java/aQute/bnd/build/packageinfo b/bundleplugin/src/main/java/aQute/bnd/build/packageinfo
index 5035fd2..0ff7674 100644
--- a/bundleplugin/src/main/java/aQute/bnd/build/packageinfo
+++ b/bundleplugin/src/main/java/aQute/bnd/build/packageinfo
@@ -1 +1 @@
-version 1.44.0
+version 1.45.0