package aQute.bnd.build.model;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;

import org.osgi.resource.Requirement;

import aQute.bnd.build.Project;
import aQute.bnd.build.Workspace;
import aQute.bnd.build.model.clauses.ExportedPackage;
import aQute.bnd.build.model.clauses.HeaderClause;
import aQute.bnd.build.model.clauses.ImportPattern;
import aQute.bnd.build.model.clauses.ServiceComponent;
import aQute.bnd.build.model.clauses.VersionedClause;
import aQute.bnd.build.model.conversions.CollectionFormatter;
import aQute.bnd.build.model.conversions.Converter;
import aQute.bnd.build.model.conversions.DefaultBooleanFormatter;
import aQute.bnd.build.model.conversions.DefaultFormatter;
import aQute.bnd.build.model.conversions.EEConverter;
import aQute.bnd.build.model.conversions.EEFormatter;
import aQute.bnd.build.model.conversions.HeaderClauseFormatter;
import aQute.bnd.build.model.conversions.HeaderClauseListConverter;
import aQute.bnd.build.model.conversions.MapFormatter;
import aQute.bnd.build.model.conversions.NewlineEscapedStringFormatter;
import aQute.bnd.build.model.conversions.NoopConverter;
import aQute.bnd.build.model.conversions.PropertiesConverter;
import aQute.bnd.build.model.conversions.PropertiesEntryFormatter;
import aQute.bnd.build.model.conversions.RequirementFormatter;
import aQute.bnd.build.model.conversions.RequirementListConverter;
import aQute.bnd.build.model.conversions.SimpleListConverter;
import aQute.bnd.build.model.conversions.VersionedClauseConverter;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Processor;
import aQute.bnd.properties.IDocument;
import aQute.bnd.properties.IRegion;
import aQute.bnd.properties.LineType;
import aQute.bnd.properties.PropertiesLineReader;
import aQute.bnd.version.Version;
import aQute.lib.io.IO;
import aQute.lib.utf8properties.UTF8Properties;

/**
 * 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										NEWLINE_LINE_SEPARATOR		= "\\n\\\n\t";
	public static final String										LIST_SEPARATOR				= ",\\\n\t";

	private static String[]											KNOWN_PROPERTIES			= new String[] {
																										Constants.BUNDLE_LICENSE,
																										Constants.BUNDLE_CATEGORY,
																										Constants.BUNDLE_NAME,
																										Constants.BUNDLE_DESCRIPTION,
																										Constants.BUNDLE_COPYRIGHT,
																										Constants.BUNDLE_UPDATELOCATION,
																										Constants.BUNDLE_VENDOR,
																										Constants.BUNDLE_CONTACTADDRESS,
																										Constants.BUNDLE_DOCURL,
																										Constants.BUNDLE_SYMBOLICNAME,
																										Constants.BUNDLE_VERSION,
																										Constants.BUNDLE_ACTIVATOR,
																										Constants.EXPORT_PACKAGE,
																										Constants.IMPORT_PACKAGE,
																										aQute.bnd.osgi.Constants.PRIVATE_PACKAGE,
																										aQute.bnd.osgi.Constants.SOURCES,
																										aQute.bnd.osgi.Constants.SERVICE_COMPONENT,
																										aQute.bnd.osgi.Constants.CLASSPATH,
																										aQute.bnd.osgi.Constants.BUILDPATH,
																										aQute.bnd.osgi.Constants.RUNBUNDLES,
																										aQute.bnd.osgi.Constants.RUNPROPERTIES,
																										aQute.bnd.osgi.Constants.SUB,
																										aQute.bnd.osgi.Constants.RUNFRAMEWORK,
																										aQute.bnd.osgi.Constants.RUNFW,
																										aQute.bnd.osgi.Constants.RUNVM,
																										aQute.bnd.osgi.Constants.RUNPROGRAMARGS,
																										aQute.bnd.osgi.Constants.DISTRO,
																										// BndConstants.RUNVMARGS,
																										// BndConstants.TESTSUITES,
																										aQute.bnd.osgi.Constants.TESTCASES,
																										aQute.bnd.osgi.Constants.PLUGIN,
																										aQute.bnd.osgi.Constants.PLUGINPATH,
																										aQute.bnd.osgi.Constants.RUNREPOS,
																										aQute.bnd.osgi.Constants.RUNREQUIRES,
																										aQute.bnd.osgi.Constants.RUNEE,
																										aQute.bnd.osgi.Constants.RUNBLACKLIST,
																										Constants.BUNDLE_BLUEPRINT,
																										Constants.INCLUDE_RESOURCE,
			Constants.STANDALONE
																									};

	public static final String										PROP_WORKSPACE				= "_workspace";

	public static final String										BUNDLE_VERSION_MACRO		= "${"
			+ Constants.BUNDLE_VERSION + "}";

	private final Map<String,Converter< ? extends Object,String>>	converters					= new HashMap<String,Converter< ? extends Object,String>>();
	private final Map<String,Converter<String, ? extends Object>>	formatters					= new HashMap<String,Converter<String, ? extends Object>>();
	// private final DataModelHelper obrModelHelper = new DataModelHelperImpl();

	private File													bndResource;
	private String													bndResourceName;

	private final PropertyChangeSupport								propChangeSupport			= new PropertyChangeSupport(
			this);
	private Properties												properties					= new UTF8Properties();
	private final Map<String,Object>								objectProperties			= new HashMap<String,Object>();
	private final Map<String,String>								changesToSave				= new TreeMap<String,String>();
	private Project													project;

	// CONVERTERS
	private Converter<List<VersionedClause>,String>					buildPathConverter			= new HeaderClauseListConverter<VersionedClause>(
			new Converter<VersionedClause,HeaderClause>() {
				public VersionedClause convert(HeaderClause input) throws IllegalArgumentException {
					if (input == null)
						return null;
					return new VersionedClause(input.getName(), input.getAttribs());
				}

				@Override
				public VersionedClause error(String msg) {
					return null;
				}
			});
	private Converter<List<VersionedClause>,String>					buildPackagesConverter		= new HeaderClauseListConverter<VersionedClause>(
			new Converter<VersionedClause,HeaderClause>() {
				public VersionedClause convert(HeaderClause input) throws IllegalArgumentException {
					if (input == null)
						return null;
					return new VersionedClause(input.getName(), input.getAttribs());
				}

				@Override
				public VersionedClause error(String msg) {
					return VersionedClause.error(msg);
				}
			});
	private Converter<List<VersionedClause>,String>					clauseListConverter			= new HeaderClauseListConverter<VersionedClause>(
			new VersionedClauseConverter());
	private Converter<String,String>								stringConverter				= new NoopConverter<String>();
	private Converter<Boolean,String>								includedSourcesConverter	= new Converter<Boolean,String>() {
																									public Boolean convert(
																											String string)
																													throws IllegalArgumentException {
																										return Boolean
																												.valueOf(
																														string);
																									}

																									@Override
																									public Boolean error(
																											String msg) {
																										return Boolean.FALSE;
																									}
																								};
	private Converter<List<String>,String>							listConverter				= SimpleListConverter
			.create();

	private Converter<List<HeaderClause>,String>					headerClauseListConverter	= new HeaderClauseListConverter<>(
			new NoopConverter<HeaderClause>());

	private Converter<List<ExportedPackage>,String>					exportPackageConverter		= new HeaderClauseListConverter<>(
			new Converter<ExportedPackage,HeaderClause>() {
				public ExportedPackage convert(HeaderClause input) {
					if (input == null)
						return null;
					return new ExportedPackage(input.getName(), input.getAttribs());
				}

				@Override
				public ExportedPackage error(String msg) {
					return ExportedPackage.error(msg);
				}
			});

	private Converter<List<ServiceComponent>,String>				serviceComponentConverter	= new HeaderClauseListConverter<ServiceComponent>(
			new Converter<ServiceComponent,HeaderClause>() {
				public ServiceComponent convert(HeaderClause input) throws IllegalArgumentException {
					if (input == null)
						return null;
					return new ServiceComponent(input.getName(), input.getAttribs());
				}

				@Override
				public ServiceComponent error(String msg) {
					return ServiceComponent.error(msg);
				}
			});
	private Converter<List<ImportPattern>,String>					importPatternConverter		= new HeaderClauseListConverter<ImportPattern>(
			new Converter<ImportPattern,HeaderClause>() {
				public ImportPattern convert(HeaderClause input) throws IllegalArgumentException {
					if (input == null)
						return null;
					return new ImportPattern(input.getName(), input.getAttribs());
				}

				@Override
				public ImportPattern error(String msg) {
					return ImportPattern.error(msg);
				}
			});

	private Converter<Map<String,String>,String>					propertiesConverter			= new PropertiesConverter();

	private Converter<List<Requirement>,String>						requirementListConverter	= new RequirementListConverter();
	private Converter<EE,String>									eeConverter					= new EEConverter();

	// Converter<ResolveMode, String> resolveModeConverter =
	// EnumConverter.create(ResolveMode.class, ResolveMode.manual);

	// FORMATTERS
	private Converter<String,String>								newlineEscapeFormatter		= new NewlineEscapedStringFormatter();
	private Converter<String,Boolean>								defaultFalseBoolFormatter	= new DefaultBooleanFormatter(
			false);
	private Converter<String,Collection< ? >>						stringListFormatter			= new CollectionFormatter<Object>(
			LIST_SEPARATOR, (String) null);
	private Converter<String,Collection< ? extends HeaderClause>>	headerClauseListFormatter	= new CollectionFormatter<HeaderClause>(
			LIST_SEPARATOR, new HeaderClauseFormatter(), null);
	private Converter<String,Map<String,String>>					propertiesFormatter			= new MapFormatter(
			LIST_SEPARATOR, new PropertiesEntryFormatter(), null);

	private Converter<String,Collection< ? extends Requirement>>	requirementListFormatter	= new CollectionFormatter<Requirement>(
			LIST_SEPARATOR, new RequirementFormatter(), null);

	private Converter<String,Collection< ? extends HeaderClause>>	standaloneLinkListFormatter	= new CollectionFormatter<>(
			LIST_SEPARATOR, new HeaderClauseFormatter(), "");

	private Converter<String,EE>									eeFormatter					= new EEFormatter();
	private Converter<String,Collection< ? extends String>>			runReposFormatter			= new CollectionFormatter<String>(
			LIST_SEPARATOR, aQute.bnd.osgi.Constants.EMPTY_HEADER);
	private Workspace												workspace;

	// Converter<String, ResolveMode> resolveModeFormatter =
	// EnumFormatter.create(ResolveMode.class, ResolveMode.manual);

	@SuppressWarnings("deprecation")
	public BndEditModel() {
		// register converters
		converters.put(aQute.bnd.osgi.Constants.BUNDLE_LICENSE, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.BUNDLE_CATEGORY, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.BUNDLE_NAME, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.BUNDLE_DESCRIPTION, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.BUNDLE_COPYRIGHT, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.BUNDLE_UPDATELOCATION, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.BUNDLE_VENDOR, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.BUNDLE_CONTACTADDRESS, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.BUNDLE_DOCURL, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.BUILDPATH, buildPathConverter);
		converters.put(aQute.bnd.osgi.Constants.BUILDPACKAGES, buildPackagesConverter);
		converters.put(aQute.bnd.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.bnd.osgi.Constants.OUTPUT, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.SOURCES, includedSourcesConverter);
		converters.put(aQute.bnd.osgi.Constants.PRIVATE_PACKAGE, listConverter);
		converters.put(aQute.bnd.osgi.Constants.CLASSPATH, listConverter);
		converters.put(Constants.EXPORT_PACKAGE, exportPackageConverter);
		converters.put(aQute.bnd.osgi.Constants.SERVICE_COMPONENT, serviceComponentConverter);
		converters.put(Constants.IMPORT_PACKAGE, importPatternConverter);
		converters.put(aQute.bnd.osgi.Constants.RUNFRAMEWORK, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.RUNFW, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.SUB, listConverter);
		converters.put(aQute.bnd.osgi.Constants.RUNPROPERTIES, propertiesConverter);
		converters.put(aQute.bnd.osgi.Constants.RUNVM, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.RUNPROGRAMARGS, stringConverter);
		// converters.put(BndConstants.RUNVMARGS, stringConverter);
		converters.put(aQute.bnd.osgi.Constants.TESTSUITES, listConverter);
		converters.put(aQute.bnd.osgi.Constants.TESTCASES, listConverter);
		converters.put(aQute.bnd.osgi.Constants.PLUGIN, headerClauseListConverter);
		converters.put(aQute.bnd.osgi.Constants.RUNREQUIRES, requirementListConverter);
		converters.put(aQute.bnd.osgi.Constants.RUNEE, eeConverter);
		converters.put(aQute.bnd.osgi.Constants.RUNREPOS, listConverter);
		// converters.put(BndConstants.RESOLVE_MODE, resolveModeConverter);
		converters.put(Constants.BUNDLE_BLUEPRINT, headerClauseListConverter);
		converters.put(Constants.INCLUDE_RESOURCE, listConverter);
		converters.put(Constants.STANDALONE, headerClauseListConverter);

		formatters.put(aQute.bnd.osgi.Constants.BUNDLE_LICENSE, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.BUNDLE_CATEGORY, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.BUNDLE_NAME, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.BUNDLE_DESCRIPTION, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.BUNDLE_COPYRIGHT, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.BUNDLE_UPDATELOCATION, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.BUNDLE_VENDOR, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.BUNDLE_CONTACTADDRESS, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.BUNDLE_DOCURL, newlineEscapeFormatter);

		formatters.put(aQute.bnd.osgi.Constants.BUILDPATH, headerClauseListFormatter);
		formatters.put(aQute.bnd.osgi.Constants.BUILDPACKAGES, headerClauseListFormatter);
		formatters.put(aQute.bnd.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.bnd.osgi.Constants.OUTPUT, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.SOURCES, defaultFalseBoolFormatter);
		formatters.put(aQute.bnd.osgi.Constants.PRIVATE_PACKAGE, stringListFormatter);
		formatters.put(aQute.bnd.osgi.Constants.CLASSPATH, stringListFormatter);
		formatters.put(Constants.EXPORT_PACKAGE, headerClauseListFormatter);
		formatters.put(aQute.bnd.osgi.Constants.SERVICE_COMPONENT, headerClauseListFormatter);
		formatters.put(Constants.IMPORT_PACKAGE, headerClauseListFormatter);
		formatters.put(aQute.bnd.osgi.Constants.RUNFRAMEWORK, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.RUNFW, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.SUB, stringListFormatter);
		formatters.put(aQute.bnd.osgi.Constants.RUNPROPERTIES, propertiesFormatter);
		formatters.put(aQute.bnd.osgi.Constants.RUNVM, newlineEscapeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.RUNPROGRAMARGS, newlineEscapeFormatter);
		// formatters.put(BndConstants.RUNVMARGS, newlineEscapeFormatter);
		// formatters.put(BndConstants.TESTSUITES, stringListFormatter);
		formatters.put(aQute.bnd.osgi.Constants.TESTCASES, stringListFormatter);
		formatters.put(aQute.bnd.osgi.Constants.PLUGIN, headerClauseListFormatter);
		formatters.put(aQute.bnd.osgi.Constants.RUNREQUIRES, requirementListFormatter);
		formatters.put(aQute.bnd.osgi.Constants.RUNEE, eeFormatter);
		formatters.put(aQute.bnd.osgi.Constants.RUNREPOS, runReposFormatter);
		// formatters.put(BndConstants.RESOLVE_MODE, resolveModeFormatter);
		formatters.put(Constants.BUNDLE_BLUEPRINT, headerClauseListFormatter);
		formatters.put(Constants.INCLUDE_RESOURCE, stringListFormatter);
		formatters.put(Constants.STANDALONE, standaloneLinkListFormatter);
	}

	public BndEditModel(BndEditModel model) {
		this();
		this.bndResource = model.bndResource;
		this.workspace = model.workspace;
		this.properties.putAll(model.properties);
		this.changesToSave.putAll(model.changesToSave);
	}

	public BndEditModel(Workspace workspace) {
		this();
		this.workspace = workspace;
	}

	public void loadFrom(IDocument document) throws IOException {
		try (InputStream in = toEscaped(document.get())) {
			loadFrom(in);
		}
	}

	public InputStream toEscaped(String text) throws IOException {
		StringReader unicode = new StringReader(text);
		ByteArrayOutputStream bout = new ByteArrayOutputStream();

		while (true) {
			int c = unicode.read();
			if (c < 0)
				break;
			if (c >= 0x7F)
				bout.write(String.format("\\u%04X", c).getBytes());
			else
				bout.write((char) c);
		}

		return new ByteArrayInputStream(bout.toByteArray());
	}

	public InputStream toAsciiStream(IDocument doc) throws IOException {
		saveChangesTo(doc);
		return toEscaped(doc.get());
	}

	public void loadFrom(File file) throws IOException {
		loadFrom(IO.stream(file));
	}

	public void loadFrom(InputStream inputStream) throws IOException {
		try {
			// Clear and load
			if (this.workspace != null) {
				properties = (Properties) this.workspace.getProperties().clone();
			} else {
				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();

			String propertyName = entry.getKey();
			String stringValue = entry.getValue();

			updateDocument(document, propertyName, stringValue);

			//
			// Ensure that properties keeps reflecting the current document
			// value
			//
			String value = cleanup(stringValue);
			if (value == null)
				value = "";

			if (propertyName != null)
				properties.setProperty(propertyName, value);

			iter.remove();
		}
	}

	private 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;
	}

	private 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();
		}
	}

	@SuppressWarnings("unchecked")
	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 Converter<Object,String> lookupConverter(String propertyName) {
		@SuppressWarnings("unchecked")
		Converter<Object,String> converter = (Converter<Object,String>) converters.get(propertyName);
		return converter;
	}

	public Converter<String,Object> lookupFormatter(String propertyName) {
		@SuppressWarnings("unchecked")
		Converter<String,Object> formatter = (Converter<String,Object>) formatters.get(propertyName);
		return formatter;
	}

	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);
		@SuppressWarnings("unchecked")
		Converter<String,Object> formatter = (Converter<String,Object>) formatters.get(propertyName);
		if (formatter == null)
			formatter = new DefaultFormatter();
		doSetObject(propertyName, oldValue, value, formatter);
	}

	public String getBundleLicense() {
		return doGetObject(Constants.BUNDLE_LICENSE, stringConverter);
	}

	public void setBundleLicense(String bundleLicense) {
		doSetObject(Constants.BUNDLE_LICENSE, getBundleLicense(), bundleLicense, newlineEscapeFormatter);
	}

	public String getBundleCategory() {
		return doGetObject(Constants.BUNDLE_CATEGORY, stringConverter);
	}

	public void setBundleCategory(String bundleCategory) {
		doSetObject(Constants.BUNDLE_CATEGORY, getBundleCategory(), bundleCategory, newlineEscapeFormatter);
	}

	public String getBundleName() {
		return doGetObject(Constants.BUNDLE_NAME, stringConverter);
	}

	public void setBundleName(String bundleName) {
		doSetObject(Constants.BUNDLE_NAME, getBundleName(), bundleName, newlineEscapeFormatter);
	}

	public String getBundleDescription() {
		return doGetObject(Constants.BUNDLE_DESCRIPTION, stringConverter);
	}

	public void setBundleDescription(String bundleDescription) {
		doSetObject(Constants.BUNDLE_DESCRIPTION, getBundleDescription(), bundleDescription, newlineEscapeFormatter);
	}

	public String getBundleCopyright() {
		return doGetObject(Constants.BUNDLE_COPYRIGHT, stringConverter);
	}

	public void setBundleCopyright(String bundleCopyright) {
		doSetObject(Constants.BUNDLE_COPYRIGHT, getBundleCopyright(), bundleCopyright, newlineEscapeFormatter);
	}

	public String getBundleUpdateLocation() {
		return doGetObject(Constants.BUNDLE_UPDATELOCATION, stringConverter);
	}

	public void setBundleUpdateLocation(String bundleUpdateLocation) {
		doSetObject(Constants.BUNDLE_UPDATELOCATION, getBundleUpdateLocation(), bundleUpdateLocation,
				newlineEscapeFormatter);
	}

	public String getBundleVendor() {
		return doGetObject(Constants.BUNDLE_VENDOR, stringConverter);
	}

	public void setBundleVendor(String bundleVendor) {
		doSetObject(Constants.BUNDLE_VENDOR, getBundleVendor(), bundleVendor, newlineEscapeFormatter);
	}

	public String getBundleContactAddress() {
		return doGetObject(Constants.BUNDLE_CONTACTADDRESS, stringConverter);
	}

	public void setBundleContactAddress(String bundleContactAddress) {
		doSetObject(Constants.BUNDLE_CONTACTADDRESS, getBundleContactAddress(), bundleContactAddress,
				newlineEscapeFormatter);
	}

	public String getBundleDocUrl() {
		return doGetObject(Constants.BUNDLE_DOCURL, stringConverter);
	}

	public void setBundleDocUrl(String bundleDocUrl) {
		doSetObject(Constants.BUNDLE_DOCURL, getBundleDocUrl(), bundleDocUrl, newlineEscapeFormatter);
	}

	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.bnd.osgi.Constants.OUTPUT, stringConverter);
	}

	public void setOutputFile(String name) {
		doSetObject(aQute.bnd.osgi.Constants.OUTPUT, getOutputFile(), name, newlineEscapeFormatter);
	}

	public boolean isIncludeSources() {
		return doGetObject(aQute.bnd.osgi.Constants.SOURCES, includedSourcesConverter);
	}

	public void setIncludeSources(boolean includeSources) {
		boolean oldValue = isIncludeSources();
		doSetObject(aQute.bnd.osgi.Constants.SOURCES, oldValue, includeSources, defaultFalseBoolFormatter);
	}

	public List<String> getPrivatePackages() {
		return doGetObject(aQute.bnd.osgi.Constants.PRIVATE_PACKAGE, listConverter);
	}

	public void setPrivatePackages(List< ? extends String> packages) {
		List<String> oldPackages = getPrivatePackages();
		doSetObject(aQute.bnd.osgi.Constants.PRIVATE_PACKAGE, oldPackages, packages, stringListFormatter);
	}

	public List<ExportedPackage> getSystemPackages() {
		return doGetObject(aQute.bnd.osgi.Constants.RUNSYSTEMPACKAGES, exportPackageConverter);
	}

	public void setSystemPackages(List< ? extends ExportedPackage> packages) {
		List<ExportedPackage> oldPackages = getSystemPackages();
		doSetObject(aQute.bnd.osgi.Constants.RUNSYSTEMPACKAGES, oldPackages, packages, headerClauseListFormatter);
	}

	public List<String> getClassPath() {
		return doGetObject(aQute.bnd.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.bnd.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(Version.emptyVersion.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.bnd.osgi.Constants.DSANNOTATIONS, listConverter);
	}

	public void setDSAnnotationPatterns(List< ? extends String> patterns) {
		List<String> oldValue = getDSAnnotationPatterns();
		doSetObject(aQute.bnd.osgi.Constants.DSANNOTATIONS, oldValue, patterns, stringListFormatter);
	}

	public List<ServiceComponent> getServiceComponents() {
		return doGetObject(aQute.bnd.osgi.Constants.SERVICE_COMPONENT, serviceComponentConverter);
	}

	public void setServiceComponents(List< ? extends ServiceComponent> components) {
		List<ServiceComponent> oldValue = getServiceComponents();
		doSetObject(aQute.bnd.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.bnd.osgi.Constants.BUILDPATH, buildPathConverter);
	}

	public void setBuildPath(List< ? extends VersionedClause> paths) {
		List<VersionedClause> oldValue = getBuildPath();
		doSetObject(aQute.bnd.osgi.Constants.BUILDPATH, oldValue, paths, headerClauseListFormatter);
	}

	@Deprecated
	public List<VersionedClause> getBuildPackages() {
		return doGetObject(aQute.bnd.osgi.Constants.BUILDPACKAGES, buildPackagesConverter);
	}

	@Deprecated
	public void setBuildPackages(List< ? extends VersionedClause> paths) {
		List<VersionedClause> oldValue = getBuildPackages();
		doSetObject(aQute.bnd.osgi.Constants.BUILDPACKAGES, oldValue, paths, headerClauseListFormatter);
	}

	public List<VersionedClause> getRunBundles() {
		return doGetObject(aQute.bnd.osgi.Constants.RUNBUNDLES, clauseListConverter);
	}

	public void setRunBundles(List< ? extends VersionedClause> paths) {
		List<VersionedClause> oldValue = getRunBundles();
		doSetObject(aQute.bnd.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.bnd.osgi.Constants.SUB, listConverter);
	}

	public void setSubBndFiles(List<String> subBndFiles) {
		List<String> oldValue = getSubBndFiles();
		doSetObject(aQute.bnd.osgi.Constants.SUB, oldValue, subBndFiles, stringListFormatter);
	}

	public Map<String,String> getRunProperties() {
		return doGetObject(aQute.bnd.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.bnd.osgi.Constants.RUNPROPERTIES, old, props, propertiesFormatter);
	}

	/*
	 * (non-Javadoc)
	 * @see bndtools.editor.model.IBndModel#getRunVMArgs()
	 */
	public String getRunVMArgs() {
		return doGetObject(aQute.bnd.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.bnd.osgi.Constants.RUNVM, old, args, newlineEscapeFormatter);
	}

	/*
	 * (non-Javadoc)
	 * @see bndtools.editor.model.IBndModel#getRunProgramArgs()
	 */
	public String getRunProgramArgs() {
		return doGetObject(aQute.bnd.osgi.Constants.RUNPROGRAMARGS, stringConverter);
	}

	/*
	 * (non-Javadoc)
	 * @see bndtools.editor.model.IBndModel#setRunProgramArgs(java.lang.String)
	 */
	public void setRunProgramArgs(String args) {
		String old = getRunProgramArgs();
		doSetObject(aQute.bnd.osgi.Constants.RUNPROGRAMARGS, old, args, newlineEscapeFormatter);
	}

	@SuppressWarnings("deprecation")
	public List<String> getTestSuites() {
		List<String> testCases = doGetObject(aQute.bnd.osgi.Constants.TESTCASES, listConverter);
		testCases = testCases != null ? testCases : Collections.<String> emptyList();

		List<String> testSuites = doGetObject(aQute.bnd.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.bnd.osgi.Constants.TESTCASES, old, suites, stringListFormatter);
		doSetObject(aQute.bnd.osgi.Constants.TESTSUITES, null, null, stringListFormatter);
	}

	public List<HeaderClause> getPlugins() {
		return doGetObject(aQute.bnd.osgi.Constants.PLUGIN, headerClauseListConverter);
	}

	public void setPlugins(List<HeaderClause> plugins) {
		List<HeaderClause> old = getPlugins();
		doSetObject(aQute.bnd.osgi.Constants.PLUGIN, old, plugins, headerClauseListFormatter);
	}

	public List<String> getPluginPath() {
		return doGetObject(aQute.bnd.osgi.Constants.PLUGINPATH, listConverter);
	}

	public void setPluginPath(List<String> pluginPath) {
		List<String> old = getPluginPath();
		doSetObject(aQute.bnd.osgi.Constants.PLUGINPATH, old, pluginPath, stringListFormatter);
	}

	public List<String> getDistro() {
		return doGetObject(aQute.bnd.osgi.Constants.DISTRO, listConverter);
	}

	public void setDistro(List<String> distros) {
		List<String> old = getPluginPath();
		doSetObject(aQute.bnd.osgi.Constants.DISTRO, old, distros, stringListFormatter);
	}

	public List<String> getRunRepos() {
		return doGetObject(aQute.bnd.osgi.Constants.RUNREPOS, listConverter);
	}

	public void setRunRepos(List<String> repos) {
		List<String> old = getRunRepos();
		doSetObject(aQute.bnd.osgi.Constants.RUNREPOS, old, repos, runReposFormatter);
	}

	public String getRunFramework() {
		return doGetObject(aQute.bnd.osgi.Constants.RUNFRAMEWORK, stringConverter);
	}

	public String getRunFw() {
		return doGetObject(aQute.bnd.osgi.Constants.RUNFW, stringConverter);
	}

	public EE getEE() {
		return doGetObject(aQute.bnd.osgi.Constants.RUNEE, eeConverter);
	}

	public void setEE(EE ee) {
		EE old = getEE();
		doSetObject(aQute.bnd.osgi.Constants.RUNEE, old, ee, eeFormatter);
	}

	public void setRunFramework(String clause) {
		assert (Constants.RUNFRAMEWORK_SERVICES.equals(clause.toLowerCase().trim())
				|| Constants.RUNFRAMEWORK_NONE.equals(clause.toLowerCase().trim()));
		String oldValue = getRunFramework();
		doSetObject(aQute.bnd.osgi.Constants.RUNFRAMEWORK, oldValue, clause, newlineEscapeFormatter);
	}

	public void setRunFw(String clause) {
		String oldValue = getRunFw();
		doSetObject(aQute.bnd.osgi.Constants.RUNFW, oldValue, clause, newlineEscapeFormatter);
	}

	public List<Requirement> getRunRequires() {
		return doGetObject(aQute.bnd.osgi.Constants.RUNREQUIRES, requirementListConverter);
	}

	public void setRunRequires(List<Requirement> requires) {
		List<Requirement> oldValue = getRunRequires();
		doSetObject(aQute.bnd.osgi.Constants.RUNREQUIRES, oldValue, requires, requirementListFormatter);
	}

	public List<Requirement> getRunBlacklist() {
		return doGetObject(aQute.bnd.osgi.Constants.RUNBLACKLIST, requirementListConverter);
	}

	public void setRunBlacklist(List<Requirement> requires) {
		List<Requirement> oldValue = getRunBlacklist();
		doSetObject(aQute.bnd.osgi.Constants.RUNBLACKLIST, oldValue, requires, requirementListFormatter);
	}

	public List<HeaderClause> getStandaloneLinks() {
		return doGetObject(Constants.STANDALONE, headerClauseListConverter);
	}

	public void setStandaloneLinks(List<HeaderClause> headers) {
		List<HeaderClause> old = getStandaloneLinks();
		doSetObject(Constants.STANDALONE, old, headers, standaloneLinkListFormatter);
	}

	public List<HeaderClause> getIgnoreStandalone() {
		List<HeaderClause> v = doGetObject(Constants.IGNORE_STANDALONE, headerClauseListConverter);
		if (v != null)
			return v;

		//
		// compatibility fixup
		v = doGetObject("x-ignore-standalone", headerClauseListConverter);
		if (v == null)
			return null;

		setIgnoreStandalone(v);
		doSetObject("x-ignore-standalone", v, null, standaloneLinkListFormatter);

		return doGetObject(Constants.IGNORE_STANDALONE, headerClauseListConverter);
	}

	public void setIgnoreStandalone(List<HeaderClause> headers) {
		List<HeaderClause> old = getIgnoreStandalone();
		doSetObject(Constants.IGNORE_STANDALONE, old, headers, standaloneLinkListFormatter);
	}

	private <R> R doGetObject(String name, Converter< ? extends R, ? super String> converter) {
		try {
			R result;
			if (objectProperties.containsKey(name)) {
				@SuppressWarnings("unchecked")
				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 = converter.convert(null);
			}

			return result;
		} catch (Exception e) {
			return converter.error(e.getMessage());
		}
	}

	private <T> void doSetObject(String name, T oldValue, T newValue, Converter<String, ? super T> formatter) {
		objectProperties.put(name, newValue);
		String v = formatter.convert(newValue);
		changesToSave.put(name, v);

		propChangeSupport.firePropertyChange(name, oldValue, newValue);
	}

	public boolean isProjectFile() {
		return Project.BNDFILE.equals(getBndResourceName());
	}

	public boolean isBndrun() {
		return getBndResourceName().endsWith(Constants.DEFAULT_BNDRUN_EXTENSION);
	}

	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;
	}

	public String getBndResourceName() {
		if (bndResourceName == null)
			return "";
		return bndResourceName;
	}

	public void setBndResourceName(String bndResourceName) {
		this.bndResourceName = bndResourceName;
	}

	public List<HeaderClause> getBundleBlueprint() {
		return doGetObject(aQute.bnd.osgi.Constants.BUNDLE_BLUEPRINT, headerClauseListConverter);
	}

	public void setBundleBlueprint(List<HeaderClause> bundleBlueprint) {
		List<HeaderClause> old = getPlugins();
		doSetObject(aQute.bnd.osgi.Constants.BUNDLE_BLUEPRINT, old, bundleBlueprint, headerClauseListFormatter);
	}

	public void addBundleBlueprint(String location) {
		List<HeaderClause> bpLocations = getBundleBlueprint();
		if (bpLocations == null)
			bpLocations = new ArrayList<HeaderClause>();
		else
			bpLocations = new ArrayList<HeaderClause>(bpLocations);
		bpLocations.add(new HeaderClause(location, null));
		setBundleBlueprint(bpLocations);
	}

	public List<String> getIncludeResource() {
		return doGetObject(aQute.bnd.osgi.Constants.INCLUDE_RESOURCE, listConverter);
	}

	public void setIncludeResource(List<String> includeResource) {
		List<String> old = getIncludeResource();
		doSetObject(aQute.bnd.osgi.Constants.INCLUDE_RESOURCE, old, includeResource, stringListFormatter);
	}

	public void addIncludeResource(String resource) {
		List<String> includeResource = getIncludeResource();
		if (includeResource == null)
			includeResource = new ArrayList<String>();
		else
			includeResource = new ArrayList<String>(includeResource);
		includeResource.add(resource);
		setIncludeResource(includeResource);
	}

	public void setProject(Project project) {
		this.project = project;
	}

	public Project getProject() {
		return project;
	}

	public Workspace getWorkspace() {
		return workspace;
	}

	public void setWorkspace(Workspace workspace) {
		Workspace old = this.workspace;
		this.workspace = workspace;
		propChangeSupport.firePropertyChange(PROP_WORKSPACE, old, workspace);
	}

	public String getGenericString(String name) {
		return doGetObject(name, stringConverter);
	}

	public void setGenericString(String name, String value) {
		doSetObject(name, getGenericString(name), value, stringConverter);
	}

	/**
	 * Return a processor for this model. This processor is based on the parent
	 * project or the bndrun file. It will contain the properties of the project
	 * file and the changes from the model.
	 * 
	 * @return a processor that reflects the actual project or bndrun file setup
	 */
	public Processor getProperties() throws Exception {
		Processor parent = null;

		if (isProjectFile() && project != null)
			parent = project;
		else if (getBndResource() != null) {
			parent = Workspace.getRun(getBndResource());
			if (parent == null) {
				parent = new Processor();
				parent.setProperties(getBndResource(), getBndResource().getParentFile());
			}
		}

		Processor result;
		if (parent == null)
			result = new Processor();
		else
			result = new Processor(parent);

		StringBuilder sb = new StringBuilder();

		for (Entry<String,String> e : changesToSave.entrySet()) {
			sb.append(e.getKey()).append(": ").append(e.getValue()).append("\n\n");
		}
		UTF8Properties p = new UTF8Properties();
		p.load(new StringReader(sb.toString()));

		result.getProperties().putAll(properties);
		result.getProperties().putAll(p);
		return result;
	}

	private String cleanup(String value) {
		if (value == null)
			return null;

		return value.replaceAll("\\\\\n", "");
	}

	/**
	 * Return the saved changes in document format.
	 */

	public Map<String,String> getDocumentChanges() {
		return changesToSave;
	}
}
