Bug 528387 - Dedicated xml elements for generic requirements

- Renamed IProvidedCapability.getAttributes() to getProperties(). This
is in sync with other parts of p2 that use "properties" for such data.
E.g. the IU properties.

- Extended content.xml read/write with a new serialization format for
requirements with the "requiredProperties" element. It is used to build
an IRequirement that has a standard match expression that applies an
LDAP filter to the capability properties. It was not possible to extend
the existing "required" element in a way that will make old p2 builds
ignore the extended version.

- Extended the content.xml read/write shared properties handling logic
with "type" attribute. Older p2 builds will ignore it and use strings.
Also the "type" attribute only appears in the "properties" extension of
"provides" which will be ignored by old p2 builds to begin with.

- Some additional cleanup to the content.xml read/write

- Increased the current content.xml format version from 1.1.0 to 1.2.0.
Old p2 builds declare compatibility with [1.0.0, 2).

- Made the handling of the property types supported by a capability
safer. E.g. unknown types are converted to Strings. E.g.
ProvidedCapability verifies that only supported types are used.

- Made the handling of generic requirements reflect the real semantics.
E.g. with correct handling of the resolution:=optional|mandatory flag
rather than to declare everything optional to avoid breakages in older
p2 builds. This is possible because the "requiredProperties" element is
completely ignored by older builds. E.g. now if an extender or service
is missing provisioning will fail as it is supposed to.

- Added factory methods to MetadataFactory for LDAP based requirements.

- Added a planner test for LDAP requirements

- Cleaned up the AutomatedDirectorTest

- Cleaned up some LDAP matching test cases.

Change-Id: Ifff77b3ea4c9cea33fd236ed101b1f33c173891d
Signed-off-by: Todor Boev <rinsvind@gmail.com>
diff --git a/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/MetadataRepositoryIO.java b/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/MetadataRepositoryIO.java
index ed67c08..4243ad5 100644
--- a/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/MetadataRepositoryIO.java
+++ b/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/MetadataRepositoryIO.java
@@ -104,7 +104,7 @@
 
 		// A format version number for metadata repository XML.
 		public static final Version COMPATIBLE_VERSION = Version.createOSGi(1, 0, 0);
-		public static final Version CURRENT_VERSION = Version.createOSGi(1, 1, 0);
+		public static final Version CURRENT_VERSION = Version.createOSGi(1, 2, 0);
 		public static final VersionRange XML_TOLERANCE = new VersionRange(COMPATIBLE_VERSION, true, Version.createOSGi(2, 0, 0), false);
 
 		// Constants for processing Instructions
diff --git a/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/MetadataParser.java b/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/MetadataParser.java
index 20fc56a..0246317 100644
--- a/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/MetadataParser.java
+++ b/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/MetadataParser.java
@@ -12,11 +12,14 @@
  *******************************************************************************/
 package org.eclipse.equinox.internal.p2.metadata.repository.io;
 
+import static java.util.stream.Collectors.toList;
+
 import java.net.URI;
 import java.util.*;
 import java.util.Map.Entry;
 import org.eclipse.equinox.internal.p2.core.helpers.OrderedProperties;
-import org.eclipse.equinox.internal.p2.metadata.*;
+import org.eclipse.equinox.internal.p2.metadata.ArtifactKey;
+import org.eclipse.equinox.internal.p2.metadata.InstallableUnit;
 import org.eclipse.equinox.internal.p2.persistence.XMLParser;
 import org.eclipse.equinox.p2.metadata.*;
 import org.eclipse.equinox.p2.metadata.MetadataFactory.*;
@@ -30,18 +33,6 @@
 public abstract class MetadataParser extends XMLParser implements XMLConstants {
 	static final ILicense[] NO_LICENSES = new ILicense[0];
 
-	static final String ATTR_TYPE_LIST_HEAD = "List<"; //$NON-NLS-1$
-	static final String ATTR_TYPE_STRING = String.class.getSimpleName();
-	static final String ATTR_TYPE_INTEGER = Integer.class.getSimpleName();
-	static final String ATTR_TYPE_LONG = Long.class.getSimpleName();
-	static final String ATTR_TYPE_FLOAT = Float.class.getSimpleName();
-	static final String ATTR_TYPE_DOUBLE = Double.class.getSimpleName();
-	static final String ATTR_TYPE_BYTE = Byte.class.getSimpleName();
-	static final String ATTR_TYPE_SHORT = Short.class.getSimpleName();
-	static final String ATTR_TYPE_CHARACTER = Character.class.getSimpleName();
-	static final String ATTR_TYPE_BOOLEAN = Boolean.class.getSimpleName();
-	static final String ATTR_TYPE_VERSION = Version.class.getSimpleName();
-
 	public MetadataParser(BundleContext context, String bundleId) {
 		super(context, bundleId);
 	}
@@ -322,7 +313,7 @@
 
 				IProvidedCapability[] providedCapabilities = (providedCapabilitiesHandler == null ? new IProvidedCapability[0] : providedCapabilitiesHandler.getProvidedCapabilities());
 				currentUnit.setCapabilities(providedCapabilities);
-				IRequirement[] requiredCapabilities = (requiredCapabilitiesHandler == null ? new IRequirement[0] : requiredCapabilitiesHandler.getRequiredCapabilities());
+				IRequirement[] requiredCapabilities = (requiredCapabilitiesHandler == null ? new IRequirement[0] : requiredCapabilitiesHandler.getRequirements());
 				currentUnit.setRequirements(requiredCapabilities);
 				IRequirement[] metaRequiredCapabilities = (metaRequiredCapabilitiesHandler == null ? new IRequirement[0] : metaRequiredCapabilitiesHandler.getMetaRequiredCapabilities());
 				currentUnit.setMetaRequirements(metaRequiredCapabilities);
@@ -389,7 +380,7 @@
 		@Override
 		protected void finished() {
 			if (children != null) {
-				scopes.add(children.getRequiredCapabilities());
+				scopes.add(children.getRequirements());
 			}
 		}
 	}
@@ -517,7 +508,7 @@
 		private String namespace;
 		private String name;
 		private Version version;
-		private ProvidedCapabilityAttributesHandler attributesHandler;
+		private ProvidedCapabilityPropertiesHandler propertiesHandler;
 
 		private List<IProvidedCapability> capabilities;
 
@@ -534,63 +525,68 @@
 
 		@Override
 		public void startElement(String elem, Attributes attributes) {
-			if (elem.equals(CAPABILITY_ATTRIBUTES_ELEMENT)) {
-				this.attributesHandler = new ProvidedCapabilityAttributesHandler(this, attributes);
-			} else {
-				invalidElement(elem, attributes);
+			switch (elem) {
+				case PROPERTIES_ELEMENT :
+					this.propertiesHandler = new ProvidedCapabilityPropertiesHandler(this, attributes);
+					break;
+				default :
+					invalidElement(elem, attributes);
+					break;
 			}
 		}
 
 		@Override
 		protected void finished() {
-			Map<String, Object> capAttrs = (attributesHandler != null)
-					? attributesHandler.getAttributes()
+			Map<String, Object> properties = (propertiesHandler != null)
+					? propertiesHandler.getProperties()
 					: new HashMap<>();
 
-			capAttrs.put(namespace, name);
-			capAttrs.put(ProvidedCapability.ATTRIBUTE_VERSION, version);
-			IProvidedCapability cap = MetadataFactory.createProvidedCapability(namespace, capAttrs);
+			properties.put(namespace, name);
+			properties.put(IProvidedCapability.PROPERTY_VERSION, version);
+			IProvidedCapability cap = MetadataFactory.createProvidedCapability(namespace, properties);
 			capabilities.add(cap);
 		}
 	}
 
-	protected class ProvidedCapabilityAttributesHandler extends AbstractMetadataHandler {
-		private Map<String, Object> capAttributes;
+	protected class ProvidedCapabilityPropertiesHandler extends AbstractMetadataHandler {
+		private Map<String, Object> properties;
 
-		public ProvidedCapabilityAttributesHandler(AbstractHandler parentHandler, Attributes attributes) {
-			super(parentHandler, CAPABILITY_ATTRIBUTES_ELEMENT);
-			// TODO add getOptionalSize(attributes, 4)
-			this.capAttributes = new HashMap<>();
+		public ProvidedCapabilityPropertiesHandler(AbstractHandler parentHandler, Attributes attributes) {
+			super(parentHandler, PROPERTIES_ELEMENT);
+			this.properties = new HashMap<>(getOptionalSize(attributes, 2));
 		}
 
-		public Map<String, Object> getAttributes() {
-			return capAttributes;
+		public Map<String, Object> getProperties() {
+			return properties;
 		}
 
 		@Override
-		public void startElement(String name, Attributes attributes) {
-			if (name.equals(CAPABILITY_ATTRIBUTE_ELEMENT)) {
-				new ProvidedCapabilityAttributeHandler(this, attributes, capAttributes);
-			} else {
-				invalidElement(name, attributes);
+		public void startElement(String elem, Attributes attributes) {
+			switch (elem) {
+				case PROPERTY_ELEMENT :
+					new ProvidedCapabilityPropertyHandler(this, attributes, properties);
+					break;
+				default :
+					invalidElement(elem, attributes);
+					break;
 			}
 		}
 	}
 
-	protected class ProvidedCapabilityAttributeHandler extends AbstractMetadataHandler {
-		public ProvidedCapabilityAttributeHandler(AbstractHandler parentHandler, Attributes attributes, Map<String, Object> capAttributes) {
-			super(parentHandler, CAPABILITY_ATTRIBUTE_ELEMENT);
+	protected class ProvidedCapabilityPropertyHandler extends AbstractMetadataHandler {
+		public ProvidedCapabilityPropertyHandler(AbstractHandler parentHandler, Attributes attributes, Map<String, Object> properties) {
+			super(parentHandler, PROPERTY_ELEMENT);
 
-			String[] values = parseRequiredAttributes(attributes, CAPABILITY_ATTRIBUTE_REQUIRED_ATTRIBUTES);
+			String[] values = parseAttributes(attributes, PROPERTY_ATTRIBUTES, PROPERTY_OPTIONAL_ATTRIBUTES);
 
 			String name = values[0];
 			String value = values[1];
-			String type = values[2];
+			String type = values[2] == null ? PROPERTY_TYPE_STRING : values[2];
 
-			if (type.startsWith(ATTR_TYPE_LIST_HEAD)) {
-				capAttributes.put(name, parseList(type, value));
+			if (type.startsWith(PROPERTY_TYPE_LIST)) {
+				properties.put(name, parseList(type, value));
 			} else {
-				capAttributes.put(name, parseScalar(type, value));
+				properties.put(name, parseScalar(type, value));
 			}
 		}
 
@@ -600,49 +596,53 @@
 		}
 
 		private List<Object> parseList(String type, String value) {
-			String elType = type.substring(ATTR_TYPE_LIST_HEAD.length(), type.length() - 1);
-
-			List<Object> res = new ArrayList<>();
-			for (String el : value.split("\\s*,\\s*")) { //$NON-NLS-1$
-				res.add(parseScalar(elType, el));
+			final String elType;
+			if (type.length() > PROPERTY_TYPE_LIST.length()) {
+				// Strip the leading "List<" and trailing ">"
+				elType = type.substring(PROPERTY_TYPE_LIST.length() + 1, type.length() - 1);
+			} else {
+				elType = PROPERTY_TYPE_STRING;
 			}
 
-			return res;
+			return Arrays.stream(value.split("\\s*,\\s*")) //$NON-NLS-1$
+					.map(val -> parseScalar(elType, val))
+					.collect(toList());
 		}
 
 		private Object parseScalar(String type, String value) {
-			if (ATTR_TYPE_STRING.equals(type)) {
+			if (PROPERTY_TYPE_STRING.equals(type)) {
 				return value;
 			}
-			if (ATTR_TYPE_INTEGER.equals(type)) {
+			if (PROPERTY_TYPE_INTEGER.equals(type)) {
 				return Integer.parseInt(value);
 			}
-			if (ATTR_TYPE_LONG.equals(type)) {
+			if (PROPERTY_TYPE_LONG.equals(type)) {
 				return Long.parseLong(value);
 			}
-			if (ATTR_TYPE_FLOAT.equals(type)) {
+			if (PROPERTY_TYPE_FLOAT.equals(type)) {
 				return Float.parseFloat(value);
 			}
-			if (ATTR_TYPE_DOUBLE.equals(type)) {
+			if (PROPERTY_TYPE_DOUBLE.equals(type)) {
 				return Double.parseDouble(value);
 			}
-			if (ATTR_TYPE_BYTE.equals(type)) {
+			if (PROPERTY_TYPE_BYTE.equals(type)) {
 				return Byte.parseByte(value);
 			}
-			if (ATTR_TYPE_SHORT.equals(type)) {
+			if (PROPERTY_TYPE_SHORT.equals(type)) {
 				return Short.parseShort(value);
 			}
-			if (ATTR_TYPE_CHARACTER.equals(type)) {
+			if (PROPERTY_TYPE_CHARACTER.equals(type)) {
 				return value.charAt(0);
 			}
-			if (ATTR_TYPE_BOOLEAN.equals(type)) {
+			if (PROPERTY_TYPE_BOOLEAN.equals(type)) {
 				return Boolean.parseBoolean(value);
 			}
-			if (ATTR_TYPE_VERSION.equals(type)) {
+			if (PROPERTY_TYPE_VERSION.equals(type)) {
 				return Version.create(value);
 			}
-			// TODO Throw what?
-			return value.toString();
+
+			// String is the default
+			return value;
 		}
 	}
 
@@ -691,23 +691,29 @@
 	}
 
 	protected class RequirementsHandler extends AbstractMetadataHandler {
-		private List<IRequirement> requiredCapabilities;
+		private List<IRequirement> requirements;
 
 		public RequirementsHandler(AbstractHandler parentHandler, Attributes attributes) {
 			super(parentHandler, REQUIREMENTS_ELEMENT);
-			requiredCapabilities = new ArrayList<>(getOptionalSize(attributes, 4));
+			requirements = new ArrayList<>(getOptionalSize(attributes, 4));
 		}
 
-		public IRequirement[] getRequiredCapabilities() {
-			return requiredCapabilities.toArray(new IRequirement[requiredCapabilities.size()]);
+		public IRequirement[] getRequirements() {
+			return requirements.toArray(new IRequirement[requirements.size()]);
 		}
 
 		@Override
 		public void startElement(String name, Attributes attributes) {
-			if (name.equals(REQUIREMENT_ELEMENT)) {
-				new RequirementHandler(this, attributes, requiredCapabilities);
-			} else {
-				invalidElement(name, attributes);
+			switch (name) {
+				case REQUIREMENT_ELEMENT :
+					new RequirementHandler(this, attributes, requirements);
+					break;
+				case REQUIREMENT_PROPERTIES_ELEMENT :
+					new RequirementPropertiesHandler(this, attributes, requirements);
+					break;
+				default :
+					invalidElement(name, attributes);
+					break;
 			}
 		}
 	}
@@ -734,31 +740,34 @@
 		public RequirementHandler(AbstractHandler parentHandler, Attributes attributes, List<IRequirement> capabilities) {
 			super(parentHandler, REQUIREMENT_ELEMENT);
 			this.capabilities = capabilities;
+
+			// Version range requirement
 			if (attributes.getIndex(NAMESPACE_ATTRIBUTE) >= 0) {
-				String[] values = parseAttributes(attributes, REQIURED_CAPABILITY_ATTRIBUTES, OPTIONAL_CAPABILITY_ATTRIBUTES);
+				String[] values = parseAttributes(attributes, REQIURED_CAPABILITY_ATTRIBUTES, REQUIRED_CAPABILITY_OPTIONAL_ATTRIBUTES);
 				namespace = values[0];
 				name = values[1];
 				range = checkVersionRange(REQUIREMENT_ELEMENT, VERSION_RANGE_ATTRIBUTE, values[2]);
-				boolean isOptional = checkBoolean(REQUIREMENT_ELEMENT, CAPABILITY_OPTIONAL_ATTRIBUTE, values[3], false).booleanValue();
+				boolean isOptional = checkBoolean(REQUIREMENT_ELEMENT, REQUIRED_CAPABILITY_OPTIONAL_ATTRIBUTE, values[3], false).booleanValue();
 				min = isOptional ? 0 : 1;
-				boolean isMultiple = checkBoolean(REQUIREMENT_ELEMENT, CAPABILITY_MULTIPLE_ATTRIBUTE, values[4], false).booleanValue();
+				boolean isMultiple = checkBoolean(REQUIREMENT_ELEMENT, REQUIRED_CAPABILITY_MULTIPLE_ATTRIBUTE, values[4], false).booleanValue();
 				max = isMultiple ? Integer.MAX_VALUE : 1;
-				greedy = checkBoolean(REQUIREMENT_ELEMENT, CAPABILITY_GREED_ATTRIBUTE, values[5], true).booleanValue();
-			} else {
-				// Expression based requirement
-				String[] values = parseAttributes(attributes, REQIUREMENT_ATTRIBUTES, OPTIONAL_REQUIREMENT_ATTRIBUTES);
+				greedy = checkBoolean(REQUIREMENT_ELEMENT, REQUIREMENT_GREED_ATTRIBUTE, values[5], true).booleanValue();
+			}
+			// IU match expression requirement
+			else {
+				String[] values = parseAttributes(attributes, REQUIRED_IU_MATCH_ATTRIBUTES, REQUIRED_IU_MATCH_OPTIONAL_ATTRIBUTES);
 				match = values[0];
 				matchParams = values[1];
 				min = values[2] == null ? 1 : checkInteger(REQUIREMENT_ELEMENT, MIN_ATTRIBUTE, values[2]);
 				max = values[3] == null ? 1 : checkInteger(REQUIREMENT_ELEMENT, MAX_ATTRIBUTE, values[3]);
-				greedy = checkBoolean(REQUIREMENT_ELEMENT, CAPABILITY_GREED_ATTRIBUTE, values[4], true).booleanValue();
+				greedy = checkBoolean(REQUIREMENT_ELEMENT, REQUIREMENT_GREED_ATTRIBUTE, values[4], true).booleanValue();
 			}
 		}
 
 		@Override
 		public void startElement(String elem, Attributes attributes) {
-			if (elem.equals(CAPABILITY_FILTER_ELEMENT)) {
-				filterHandler = new TextHandler(this, CAPABILITY_FILTER_ELEMENT, attributes);
+			if (elem.equals(REQUIREMENT_FILTER_ELEMENT)) {
+				filterHandler = new TextHandler(this, REQUIREMENT_FILTER_ELEMENT, attributes);
 			} else if (elem.equals(REQUIREMENT_DESCRIPTION_ELEMENT)) {
 				descriptionHandler = new TextHandler(this, REQUIREMENT_DESCRIPTION_ELEMENT, attributes);
 			} else {
@@ -805,6 +814,83 @@
 		}
 	}
 
+	protected class RequirementPropertiesHandler extends AbstractHandler {
+		private List<IRequirement> requirements;
+
+		private String namespace;
+		private String match;
+		private int min;
+		private int max;
+		private boolean greedy;
+
+		private TextHandler filterHandler;
+		private TextHandler descriptionHandler;
+
+		public RequirementPropertiesHandler(AbstractHandler parentHandler, Attributes attributes, List<IRequirement> requirements) {
+			super(parentHandler, REQUIREMENT_PROPERTIES_ELEMENT);
+			this.requirements = requirements;
+
+			String[] values = parseAttributes(attributes, REQIURED_PROPERTIES_MATCH_ATTRIBUTES, REQIURED_PROPERTIES_MATCH_OPTIONAL_ATTRIBUTES);
+			namespace = values[0];
+			match = values[1];
+			min = (values[2] == null) ? 1 : checkInteger(REQUIREMENT_PROPERTIES_ELEMENT, MIN_ATTRIBUTE, values[2]);
+			max = (values[3] == null) ? 1 : checkInteger(REQUIREMENT_PROPERTIES_ELEMENT, MAX_ATTRIBUTE, values[3]);
+			greedy = checkBoolean(REQUIREMENT_PROPERTIES_ELEMENT, REQUIREMENT_GREED_ATTRIBUTE, values[4], true).booleanValue();
+		}
+
+		@Override
+		public void startElement(String elem, Attributes attributes) {
+			switch (elem) {
+				case REQUIREMENT_FILTER_ELEMENT :
+					filterHandler = new TextHandler(this, REQUIREMENT_FILTER_ELEMENT, attributes);
+					break;
+				case REQUIREMENT_DESCRIPTION_ELEMENT :
+					descriptionHandler = new TextHandler(this, REQUIREMENT_DESCRIPTION_ELEMENT, attributes);
+					break;
+				default :
+					invalidElement(elem, attributes);
+					break;
+			}
+		}
+
+		@Override
+		protected void finished() {
+			if (!isValidXML()) {
+				return;
+			}
+
+			IMatchExpression<IInstallableUnit> filter = null;
+			if (filterHandler != null) {
+				try {
+					filter = InstallableUnit.parseFilter(filterHandler.getText());
+				} catch (ExpressionParseException e) {
+					if (removeWhiteSpace(filterHandler.getText()).equals("(&(|)(|)(|))")) {//$NON-NLS-1$
+						// We could log this I guess
+					} else {
+						throw e;
+					}
+				}
+			}
+
+			String description = (descriptionHandler != null) ? descriptionHandler.getText() : null;
+
+			IFilterExpression attrMatch = ExpressionUtil.parseLDAP(match);
+			IRequirement requirement = MetadataFactory.createRequirement(namespace, attrMatch, filter, min, max, greedy, description);
+			requirements.add(requirement);
+		}
+
+		private String removeWhiteSpace(String s) {
+			if (s == null)
+				return ""; //$NON-NLS-1$
+			StringBuffer builder = new StringBuffer();
+			for (int i = 0; i < s.length(); i++) {
+				if (s.charAt(i) != ' ')
+					builder.append(s.charAt(i));
+			}
+			return builder.toString();
+		}
+	}
+
 	protected class ArtifactsHandler extends AbstractHandler {
 
 		private List<IArtifactKey> artifacts;
diff --git a/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/MetadataWriter.java b/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/MetadataWriter.java
index 2b7686e..f88d52b 100644
--- a/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/MetadataWriter.java
+++ b/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/MetadataWriter.java
@@ -14,11 +14,10 @@
 import java.io.OutputStream;
 import java.net.MalformedURLException;
 import java.util.*;
-import java.util.Map.Entry;
 import org.eclipse.core.runtime.*;
 import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
-import org.eclipse.equinox.internal.p2.metadata.ProvidedCapability;
 import org.eclipse.equinox.internal.p2.metadata.RequiredCapability;
+import org.eclipse.equinox.internal.p2.metadata.RequiredPropertiesMatch;
 import org.eclipse.equinox.internal.p2.metadata.repository.Activator;
 import org.eclipse.equinox.internal.p2.persistence.XMLWriter;
 import org.eclipse.equinox.p2.metadata.*;
@@ -96,23 +95,23 @@
 
 	private boolean hasOnlySimpleRequirements(IInstallableUnit iu) {
 		for (IRequirement r : iu.getRequirements())
-			if (r.getMax() == 0 || !RequiredCapability.isSimpleRequirement(r.getMatches()))
+			if (r.getMax() == 0 || !RequiredCapability.isVersionRangeRequirement(r.getMatches()))
 				return false;
 
 		if (iu.getUpdateDescriptor() != null) {
 			for (IMatchExpression<IInstallableUnit> m : iu.getUpdateDescriptor().getIUsBeingUpdated()) {
-				if (!RequiredCapability.isSimpleRequirement(m))
+				if (!RequiredCapability.isVersionRangeRequirement(m))
 					return false;
 			}
 		}
 
 		for (IRequirement r : iu.getMetaRequirements())
-			if (r.getMax() == 0 || !RequiredCapability.isSimpleRequirement(r.getMatches()))
+			if (r.getMax() == 0 || !RequiredCapability.isVersionRangeRequirement(r.getMatches()))
 				return false;
 
 		if (iu instanceof IInstallableUnitFragment) {
 			for (IRequirement r : ((IInstallableUnitFragment) iu).getHost())
-				if (!RequiredCapability.isSimpleRequirement(r.getMatches()))
+				if (!RequiredCapability.isVersionRangeRequirement(r.getMatches()))
 					return false;
 		}
 
@@ -120,11 +119,11 @@
 			IInstallableUnitPatch iuPatch = (IInstallableUnitPatch) iu;
 			for (IRequirement[] rArr : iuPatch.getApplicabilityScope())
 				for (IRequirement r : rArr)
-					if (!RequiredCapability.isSimpleRequirement(r.getMatches()))
+					if (!RequiredCapability.isVersionRangeRequirement(r.getMatches()))
 						return false;
 
 			IRequirement lifeCycle = iuPatch.getLifeCycle();
-			if (lifeCycle != null && !RequiredCapability.isSimpleRequirement(lifeCycle.getMatches()))
+			if (lifeCycle != null && !RequiredCapability.isVersionRangeRequirement(lifeCycle.getMatches()))
 				return false;
 		}
 		return true;
@@ -167,50 +166,12 @@
 		attribute(NAME_ATTRIBUTE, capability.getName());
 		attribute(VERSION_ATTRIBUTE, capability.getVersion());
 
-		Map<String, Object> attrs = new HashMap<>(capability.getAttributes());
-		attrs.remove(capability.getNamespace());
-		attrs.remove(ProvidedCapability.ATTRIBUTE_VERSION);
+		Map<String, Object> props = new HashMap<>(capability.getProperties());
+		props.remove(capability.getNamespace());
+		props.remove(IProvidedCapability.PROPERTY_VERSION);
 
-		if (!attrs.isEmpty()) {
-			start(CAPABILITY_ATTRIBUTES_ELEMENT);
-			attribute(COLLECTION_SIZE_ATTRIBUTE, attrs.size());
-
-			for (Entry<String, Object> attr : attrs.entrySet()) {
-				start(CAPABILITY_ATTRIBUTE_ELEMENT);
-
-				String name = attr.getKey();
-				Object val = attr.getValue();
-				String type;
-
-				if (Collection.class.isAssignableFrom(val.getClass())) {
-					Collection<?> coll = (Collection<?>) val;
-
-					String elType = coll.iterator().next().getClass().getSimpleName();
-					type = String.format("List<%s>", elType); //$NON-NLS-1$
-
-					StringBuilder valBuff = new StringBuilder();
-					for (Iterator<?> iter = coll.iterator(); iter.hasNext();) {
-						String el = iter.next().toString();
-
-						valBuff.append(el);
-						if (iter.hasNext()) {
-							valBuff.append(","); //$NON-NLS-1$
-						}
-					}
-
-					val = valBuff.toString();
-				} else {
-					type = val.getClass().getSimpleName();
-					val = val.toString();
-				}
-
-				attribute(CAPABILITY_ATTRIBUTE_NAME_ATTRIBUTE, name);
-				attribute(CAPABILITY_ATTRIBUTE_TYPE_ATTRIBUTE, type);
-				attribute(CAPABILITY_ATTRIBUTE_VALUE_ATTRIBUTE, val);
-
-				end(CAPABILITY_ATTRIBUTE_ELEMENT);
-			}
-			end(CAPABILITY_ATTRIBUTES_ELEMENT);
+		if (!props.isEmpty()) {
+			writeProperties(props);
 		}
 
 		end(PROVIDED_CAPABILITY_ELEMENT);
@@ -246,7 +207,7 @@
 			throw new IllegalStateException();
 		IMatchExpression<IInstallableUnit> singleUD = descriptor.getIUsBeingUpdated().iterator().next();
 		start(UPDATE_DESCRIPTOR_ELEMENT);
-		if (RequiredCapability.isSimpleRequirement(singleUD)) {
+		if (RequiredCapability.isVersionRangeRequirement(singleUD)) {
 			attribute(ID_ATTRIBUTE, RequiredCapability.extractName(singleUD));
 			attribute(VERSION_RANGE_ATTRIBUTE, RequiredCapability.extractRange(singleUD));
 		} else {
@@ -291,27 +252,59 @@
 	}
 
 	protected void writeRequirement(IRequirement requirement) {
-		start(REQUIREMENT_ELEMENT);
 		IMatchExpression<IInstallableUnit> match = requirement.getMatches();
-		if (requirement.getMax() > 0 && RequiredCapability.isSimpleRequirement(match)) {
+
+		// A (namespace, name, version-range) type of requirement
+		if (requirement.getMax() > 0 && RequiredCapability.isVersionRangeRequirement(match)) {
+			start(REQUIREMENT_ELEMENT);
+
 			attribute(NAMESPACE_ATTRIBUTE, RequiredCapability.extractNamespace(match));
 			attribute(NAME_ATTRIBUTE, RequiredCapability.extractName(match));
 			attribute(VERSION_RANGE_ATTRIBUTE, RequiredCapability.extractRange(match));
-			attribute(CAPABILITY_OPTIONAL_ATTRIBUTE, requirement.getMin() == 0, false);
-			attribute(CAPABILITY_MULTIPLE_ATTRIBUTE, requirement.getMax() > 1, false);
-		} else {
-			writeMatchExpression(match);
-			if (requirement.getMin() != 1)
-				attribute(MIN_ATTRIBUTE, requirement.getMin());
-			if (requirement.getMax() != 1)
-				attribute(MAX_ATTRIBUTE, requirement.getMax());
+			attribute(REQUIRED_CAPABILITY_OPTIONAL_ATTRIBUTE, requirement.getMin() == 0, false);
+			attribute(REQUIRED_CAPABILITY_MULTIPLE_ATTRIBUTE, requirement.getMax() > 1, false);
 		}
-		attribute(CAPABILITY_GREED_ATTRIBUTE, requirement.isGreedy(), true);
-		if (requirement.getFilter() != null)
-			writeTrimmedCdata(CAPABILITY_FILTER_ELEMENT, requirement.getFilter().getParameters()[0].toString());
-		if (requirement.getDescription() != null)
+		// A (namespace, attributes-match) type of requirement
+		else if (RequiredPropertiesMatch.isPropertiesMatchRequirement(match)) {
+			start(REQUIREMENT_PROPERTIES_ELEMENT);
+
+			attribute(NAMESPACE_ATTRIBUTE, RequiredPropertiesMatch.extractNamespace(match));
+			attribute(MATCH_ATTRIBUTE, RequiredPropertiesMatch.extractPropertiesMatch(match));
+
+			if (requirement.getMin() != 1) {
+				attribute(MIN_ATTRIBUTE, requirement.getMin());
+			}
+
+			if (requirement.getMax() != 1) {
+				attribute(MAX_ATTRIBUTE, requirement.getMax());
+			}
+		}
+		// A general match expression type of requirement
+		else {
+			start(REQUIREMENT_ELEMENT);
+
+			writeMatchExpression(match);
+
+			if (requirement.getMin() != 1) {
+				attribute(MIN_ATTRIBUTE, requirement.getMin());
+			}
+
+			if (requirement.getMax() != 1) {
+				attribute(MAX_ATTRIBUTE, requirement.getMax());
+			}
+		}
+
+		attribute(REQUIREMENT_GREED_ATTRIBUTE, requirement.isGreedy(), true);
+
+		if (requirement.getFilter() != null) {
+			writeTrimmedCdata(REQUIREMENT_FILTER_ELEMENT, requirement.getFilter().getParameters()[0].toString());
+		}
+
+		if (requirement.getDescription() != null) {
 			writeTrimmedCdata(REQUIREMENT_DESCRIPTION_ELEMENT, requirement.getDescription());
-		end(REQUIREMENT_ELEMENT);
+		}
+
+		end();
 	}
 
 	private void writeMatchExpression(IMatchExpression<IInstallableUnit> match) {
diff --git a/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/XMLConstants.java b/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/XMLConstants.java
index 44db999..621356e 100644
--- a/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/XMLConstants.java
+++ b/bundles/org.eclipse.equinox.p2.metadata.repository/src/org/eclipse/equinox/internal/p2/metadata/repository/io/XMLConstants.java
@@ -39,9 +39,7 @@
 	public static final String REQUIREMENTS_ELEMENT = "requires"; //$NON-NLS-1$
 	public static final String HOST_REQUIREMENTS_ELEMENT = "hostRequirements"; //$NON-NLS-1$
 	public static final String META_REQUIREMENTS_ELEMENT = "metaRequirements"; //$NON-NLS-1$
-	public static final String REQUIREMENT_ELEMENT = "required"; //$NON-NLS-1$
 	public static final String PROVIDED_CAPABILITIES_ELEMENT = "provides"; //$NON-NLS-1$
-	public static final String PROVIDED_CAPABILITY_ELEMENT = "provided"; //$NON-NLS-1$
 	public static final String[] REQUIRED_PROVIDED_CAPABILITY_ATTRIBUTES = new String[] {NAMESPACE_ATTRIBUTE, NAME_ATTRIBUTE, VERSION_ATTRIBUTE};
 	public static final String TOUCHPOINT_TYPE_ELEMENT = "touchpoint"; //$NON-NLS-1$
 	public static final String TOUCHPOINT_DATA_ELEMENT = "touchpointData"; //$NON-NLS-1$
@@ -63,26 +61,26 @@
 	public static final String[] OPTIONAL_IU_ATTRIBUTES = new String[] {SINGLETON_ATTRIBUTE};
 	public static final String GENERATION_ATTRIBUTE = "generation"; //$NON-NLS-1$
 
+	// Constants for the provided capability element
+	public static final String PROVIDED_CAPABILITY_ELEMENT = "provided"; //$NON-NLS-1$
+
 	// Constants for sub-elements of a required capability element
-	public static final String CAPABILITY_FILTER_ELEMENT = "filter"; //$NON-NLS-1$
+	public static final String REQUIREMENT_ELEMENT = "required"; //$NON-NLS-1$
+	public static final String REQUIREMENT_PROPERTIES_ELEMENT = "requiredProperties"; //$NON-NLS-1$
+	public static final String REQUIREMENT_FILTER_ELEMENT = "filter"; //$NON-NLS-1$
 	public static final String REQUIREMENT_DESCRIPTION_ELEMENT = "description"; //$NON-NLS-1$
+	public static final String REQUIREMENT_GREED_ATTRIBUTE = "greedy"; //$NON-NLS-1$
 
-	// Constants for attributes of a required capability element
-	public static final String CAPABILITY_OPTIONAL_ATTRIBUTE = "optional"; //$NON-NLS-1$
-	public static final String CAPABILITY_MULTIPLE_ATTRIBUTE = "multiple"; //$NON-NLS-1$
-	public static final String CAPABILITY_GREED_ATTRIBUTE = "greedy"; //$NON-NLS-1$
-
-	public static final String CAPABILITY_ATTRIBUTES_ELEMENT = "attributes"; //$NON-NLS-1$
-	public static final String CAPABILITY_ATTRIBUTE_ELEMENT = "attribute"; //$NON-NLS-1$
-	public static final String CAPABILITY_ATTRIBUTE_NAME_ATTRIBUTE = "name"; //$NON-NLS-1$
-	public static final String CAPABILITY_ATTRIBUTE_TYPE_ATTRIBUTE = "type"; //$NON-NLS-1$
-	public static final String CAPABILITY_ATTRIBUTE_VALUE_ATTRIBUTE = "value"; //$NON-NLS-1$
-	public static final String[] CAPABILITY_ATTRIBUTE_REQUIRED_ATTRIBUTES = new String[] {CAPABILITY_ATTRIBUTE_NAME_ATTRIBUTE, CAPABILITY_ATTRIBUTE_VALUE_ATTRIBUTE, CAPABILITY_ATTRIBUTE_TYPE_ATTRIBUTE};
-
+	public static final String REQUIRED_CAPABILITY_OPTIONAL_ATTRIBUTE = "optional"; //$NON-NLS-1$
+	public static final String REQUIRED_CAPABILITY_MULTIPLE_ATTRIBUTE = "multiple"; //$NON-NLS-1$
 	public static final String[] REQIURED_CAPABILITY_ATTRIBUTES = new String[] {NAMESPACE_ATTRIBUTE, NAME_ATTRIBUTE, VERSION_RANGE_ATTRIBUTE};
-	public static final String[] REQIUREMENT_ATTRIBUTES = new String[] {MATCH_ATTRIBUTE};
-	public static final String[] OPTIONAL_CAPABILITY_ATTRIBUTES = new String[] {CAPABILITY_OPTIONAL_ATTRIBUTE, CAPABILITY_MULTIPLE_ATTRIBUTE, CAPABILITY_GREED_ATTRIBUTE};
-	public static final String[] OPTIONAL_REQUIREMENT_ATTRIBUTES = new String[] {MATCH_PARAMETERS_ATTRIBUTE, MIN_ATTRIBUTE, MAX_ATTRIBUTE, CAPABILITY_GREED_ATTRIBUTE};
+	public static final String[] REQUIRED_CAPABILITY_OPTIONAL_ATTRIBUTES = new String[] {REQUIRED_CAPABILITY_OPTIONAL_ATTRIBUTE, REQUIRED_CAPABILITY_MULTIPLE_ATTRIBUTE, REQUIREMENT_GREED_ATTRIBUTE};
+
+	public static final String[] REQIURED_PROPERTIES_MATCH_ATTRIBUTES = new String[] {NAMESPACE_ATTRIBUTE, MATCH_ATTRIBUTE};
+	public static final String[] REQIURED_PROPERTIES_MATCH_OPTIONAL_ATTRIBUTES = new String[] {MIN_ATTRIBUTE, MAX_ATTRIBUTE, REQUIREMENT_GREED_ATTRIBUTE};
+
+	public static final String[] REQUIRED_IU_MATCH_ATTRIBUTES = new String[] {MATCH_ATTRIBUTE};
+	public static final String[] REQUIRED_IU_MATCH_OPTIONAL_ATTRIBUTES = new String[] {MATCH_PARAMETERS_ATTRIBUTE, MIN_ATTRIBUTE, MAX_ATTRIBUTE, REQUIREMENT_GREED_ATTRIBUTE};
 
 	// Constants for attributes of an artifact key element
 	public static final String ARTIFACT_KEY_CLASSIFIER_ATTRIBUTE = "classifier"; //$NON-NLS-1$
diff --git a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/ProvidedCapability.java b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/ProvidedCapability.java
index 13eb676..31297ab 100644
--- a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/ProvidedCapability.java
+++ b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/ProvidedCapability.java
@@ -9,10 +9,14 @@
  *     IBM Corporation - initial API and implementation
  *     EclipseSource - ongoing development
  *     SAP - ongoing development
+ *     Todor Boev
  *******************************************************************************/
 package org.eclipse.equinox.internal.p2.metadata;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import org.eclipse.core.runtime.Assert;
@@ -31,44 +35,65 @@
 	public static final String MEMBER_NAME = "name"; //$NON-NLS-1$
 	/** Used for fast access from P2 queries to the {@link #getVersion} method */
 	public static final String MEMBER_VERSION = "version"; //$NON-NLS-1$
-	/** Used for fast access from P2 queries to the {@link #getAttributes} method */
-	public static final String MEMBER_ATTRIBUTES = "attributes"; //$NON-NLS-1$
-
-	// TODO Move this to IProvidedCapability?
-	// The "version" attribute is part of the public contract of getVersion() and getAttributes()
-	public static final String ATTRIBUTE_VERSION = "version"; //$NON-NLS-1$
+	/** Used for fast access from P2 queries to the {@link #getProperties} method */
+	public static final String MEMBER_PROPERTIES = "properties"; //$NON-NLS-1$
 
 	private final String namespace;
-	private final Map<String, Object> attributes;
+	private final Map<String, Object> properties;
 
-	public ProvidedCapability(String namespace, Map<String, Object> attrs) {
+	public ProvidedCapability(String namespace, Map<String, Object> props) {
 		Assert.isNotNull(namespace, NLS.bind(Messages.provided_capability_namespace_not_defined, null));
 		this.namespace = namespace;
 
-		Assert.isNotNull(attrs);
-		Assert.isTrue(!attrs.isEmpty());
+		Assert.isNotNull(props);
+		Assert.isTrue(!props.isEmpty());
 
-		this.attributes = new HashMap<>(attrs);
+		assertValidPropertyTypes(props);
+
+		Map<String, Object> resolvedProps = new HashMap<>(props);
 
 		// Verify the name
-		Assert.isTrue(attributes.containsKey(namespace) && (attributes.get(namespace) instanceof String), NLS.bind(Messages.provided_capability_name_not_defined, namespace));
+		Assert.isTrue(resolvedProps.containsKey(namespace) && (resolvedProps.get(namespace) instanceof String), NLS.bind(Messages.provided_capability_name_not_defined, namespace));
 
 		// Verify the version
-		Object version = attributes.get(ATTRIBUTE_VERSION);
+		Object version = resolvedProps.get(PROPERTY_VERSION);
 		if (version != null) {
-			Assert.isTrue(attributes.get(ATTRIBUTE_VERSION) instanceof Version);
+			Assert.isTrue(props.get(PROPERTY_VERSION) instanceof Version);
 		} else {
-			attributes.put(ATTRIBUTE_VERSION, Version.emptyVersion);
+			resolvedProps.put(PROPERTY_VERSION, Version.emptyVersion);
 		}
+
+		this.properties = Collections.unmodifiableMap(props);
 	}
 
 	public ProvidedCapability(String namespace, String name, Version version) {
 		Assert.isNotNull(namespace, NLS.bind(Messages.provided_capability_namespace_not_defined, null));
 		Assert.isNotNull(name, NLS.bind(Messages.provided_capability_name_not_defined, namespace));
 		this.namespace = namespace;
-		this.attributes = new HashMap<>();
-		attributes.put(namespace, name);
-		attributes.put(ATTRIBUTE_VERSION, version == null ? Version.emptyVersion : version);
+		this.properties = new HashMap<>();
+		properties.put(namespace, name);
+		properties.put(PROPERTY_VERSION, version == null ? Version.emptyVersion : version);
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder str = new StringBuilder();
+		str.append(namespace);
+
+		for (Entry<String, Object> attr : properties.entrySet()) {
+			String key = attr.getKey();
+			Object val = attr.getValue();
+			String type = val.getClass().getSimpleName();
+
+			str.append("; ").append(key).append(":").append(type).append("=").append(val); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+		}
+
+		return str.toString();
+	}
+
+	@Override
+	public int hashCode() {
+		return namespace.hashCode() * properties.hashCode();
 	}
 
 	@Override
@@ -87,7 +112,7 @@
 			return false;
 		}
 
-		if (!(attributes.equals(otherCapability.getAttributes()))) {
+		if (!(properties.equals(otherCapability.getProperties()))) {
 			return false;
 		}
 
@@ -101,38 +126,17 @@
 
 	@Override
 	public String getName() {
-		return (String) attributes.get(namespace);
+		return (String) properties.get(namespace);
 	}
 
 	@Override
 	public Version getVersion() {
-		return (Version) attributes.get(ATTRIBUTE_VERSION);
+		return (Version) properties.get(PROPERTY_VERSION);
 	}
 
 	@Override
-	public Map<String, Object> getAttributes() {
-		return attributes;
-	}
-
-	@Override
-	public int hashCode() {
-		return namespace.hashCode() * attributes.hashCode();
-	}
-
-	@Override
-	public String toString() {
-		StringBuilder str = new StringBuilder();
-		str.append(namespace);
-
-		for (Entry<String, Object> attr : attributes.entrySet()) {
-			String key = attr.getKey();
-			Object val = attr.getValue();
-			String type = val.getClass().getSimpleName();
-
-			str.append("; ").append(key).append(":").append(type).append("=").append(val); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
-		}
-
-		return str.toString();
+	public Map<String, Object> getProperties() {
+		return properties;
 	}
 
 	@Override
@@ -141,13 +145,36 @@
 			case MEMBER_NAMESPACE :
 				return namespace;
 			case MEMBER_NAME :
-				return attributes.get(namespace);
+				return properties.get(namespace);
 			case MEMBER_VERSION :
-				return attributes.get(ATTRIBUTE_VERSION);
-			case MEMBER_ATTRIBUTES :
-				return attributes;
+				return properties.get(PROPERTY_VERSION);
+			case MEMBER_PROPERTIES :
+				return properties;
 			default :
-				throw new IllegalArgumentException("No such member: " + memberName); //$NON-NLS-1$
+				throw new IllegalArgumentException(String.format("No such member: %s", memberName)); //$NON-NLS-1$
 		}
 	}
+
+	private void assertValidPropertyTypes(Map<String, Object> props) {
+		props.forEach(this::assertValidValueType);
+	}
+
+	private void assertValidValueType(String key, Object prop) {
+		if (prop instanceof List<?>) {
+			int idx = 0;
+			for (Object scalar : (List<?>) prop) {
+				assertValidScalarType(String.format("%s[%s]", key, idx++), scalar); //$NON-NLS-1$
+			}
+		} else {
+			assertValidScalarType(key, prop);
+		}
+	}
+
+	private void assertValidScalarType(String key, Object scalar) {
+		Arrays.asList(Version.class, String.class, Long.class, Integer.class, Short.class, Byte.class, Double.class, Float.class, Boolean.class, Character.class)
+				.stream()
+				.filter(t -> t.isAssignableFrom(scalar.getClass()))
+				.findFirst()
+				.orElseThrow(() -> new IllegalArgumentException(String.format("Invalid type %s of property %s", scalar.getClass(), key))); //$NON-NLS-1$
+	}
 }
diff --git a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/RequiredCapability.java b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/RequiredCapability.java
index cd8f8da..c08af57 100644
--- a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/RequiredCapability.java
+++ b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/RequiredCapability.java
@@ -39,27 +39,14 @@
  * @see IInstallableUnit#NAMESPACE_IU_ID
  */
 public class RequiredCapability extends Requirement implements IRequiredCapability {
-	private static final IExpression simpleMatchExpression;
-
-	static {
-		IExpressionFactory factory = ExpressionUtil.getFactory();
-
-		IExpression xVar = factory.variable("cap"); //$NON-NLS-1$
-
-		IExpression name = factory.member(xVar, MEMBER_NAME);
-		IExpression nameEqual = factory.equals(name, factory.indexedParameter(0));
-
-		IExpression namespace = factory.member(xVar, MEMBER_NAMESPACE);
-		IExpression namespaceEqual = factory.equals(namespace, factory.indexedParameter(1));
-
-		IExpression version = factory.member(xVar, MEMBER_VERSION);
-		IExpression versionInRange = factory.matches(version, factory.indexedParameter(2));
-
-		IExpression pvMember = factory.member(factory.thisVariable(), MEMBER_PROVIDED_CAPABILITIES);
-
-		// Place nameEqual first to eliminate quickly most non-matching candidates
-		simpleMatchExpression = factory.exists(pvMember, factory.lambda(xVar, factory.and(nameEqual, namespaceEqual, versionInRange)));
-	}
+	/**
+	 * Argument $0 must evaluate to a String
+	 * Argument $1 must evaluate to a String
+	 * Argument $2 must evaluate to a {@link VersionRange}
+	 */
+	private static final IExpression VERSION_RANGE_MATCH = ExpressionUtil.parse(
+			String.format("%s.exists(cap | cap.%s == $0 && cap.%s == $1 && cap.%s ~= $2)", //$NON-NLS-1$
+					MEMBER_PROVIDED_CAPABILITIES, MEMBER_NAME, MEMBER_NAMESPACE, MEMBER_VERSION));
 
 	/**
 	 * TODO Remove. This is a private impl class. Users must call the analogous MetadataFactory.createRequirement()
@@ -104,17 +91,11 @@
 	public String toString() {
 		StringBuilder result = new StringBuilder();
 
-		// Namespace
 		result.append(getNamespace());
-		result.append(' ');
-
-		// Name
+		result.append("; "); //$NON-NLS-1$
 		result.append(getName());
-		result.append(' ');
-
-		// Version range
-		VersionRange range = getRange();
-		result.append(range);
+		result.append(" "); //$NON-NLS-1$
+		result.append(getRange());
 
 		return result.toString();
 	}
@@ -124,7 +105,7 @@
 		Assert.isNotNull(name);
 		Object resolvedRange = (range != null) ? range : VersionRange.emptyRange;
 		IExpressionFactory factory = ExpressionUtil.getFactory();
-		return factory.matchExpression(simpleMatchExpression, name, namespace, resolvedRange);
+		return factory.matchExpression(VERSION_RANGE_MATCH, name, namespace, resolvedRange);
 	}
 
 	public static String extractNamespace(IMatchExpression<IInstallableUnit> matchExpression) {
@@ -144,7 +125,7 @@
 	}
 
 	public static boolean isVersionStrict(IMatchExpression<IInstallableUnit> matchExpression) {
-		if (!isSimpleRequirement(matchExpression)) {
+		if (!isVersionRangeRequirement(matchExpression)) {
 			return false;
 		}
 
@@ -153,12 +134,12 @@
 		return range.getMinimum().equals(range.getMaximum());
 	}
 
-	public static boolean isSimpleRequirement(IMatchExpression<IInstallableUnit> matchExpression) {
-		return simpleMatchExpression.equals(ExpressionUtil.getOperand(matchExpression));
+	public static boolean isVersionRangeRequirement(IMatchExpression<IInstallableUnit> matchExpression) {
+		return VERSION_RANGE_MATCH.equals(ExpressionUtil.getOperand(matchExpression));
 	}
 
 	private static void assertValid(IMatchExpression<IInstallableUnit> matchExpression) {
-		if (!isSimpleRequirement(matchExpression)) {
+		if (!isVersionRangeRequirement(matchExpression)) {
 			throw new IllegalArgumentException();
 		}
 	}
diff --git a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/RequiredPropertiesMatch.java b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/RequiredPropertiesMatch.java
new file mode 100644
index 0000000..a108fad
--- /dev/null
+++ b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/RequiredPropertiesMatch.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ *  Copyright (c) 2007, 2017 IBM Corporation and others.
+ *  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:
+ *     Todor Boev
+ *******************************************************************************/
+package org.eclipse.equinox.internal.p2.metadata;
+
+import static org.eclipse.equinox.internal.p2.metadata.InstallableUnit.MEMBER_PROVIDED_CAPABILITIES;
+import static org.eclipse.equinox.internal.p2.metadata.ProvidedCapability.MEMBER_NAMESPACE;
+import static org.eclipse.equinox.internal.p2.metadata.ProvidedCapability.MEMBER_PROPERTIES;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.equinox.p2.metadata.IInstallableUnit;
+import org.eclipse.equinox.p2.metadata.IProvidedCapability;
+import org.eclipse.equinox.p2.metadata.IRequirement;
+import org.eclipse.equinox.p2.metadata.expression.ExpressionUtil;
+import org.eclipse.equinox.p2.metadata.expression.IExpression;
+import org.eclipse.equinox.p2.metadata.expression.IExpressionFactory;
+import org.eclipse.equinox.p2.metadata.expression.IFilterExpression;
+import org.eclipse.equinox.p2.metadata.expression.IMatchExpression;
+
+/**
+ * A required capability match represents some external constraint on an {@link IInstallableUnit}.
+ * <p>
+ * This is a flavor of the general {@link IRequirement} that searches for
+ * a capability that has {@link IProvidedCapability#getProperties() properties} that match a given expression.
+ * I.e. this is much more limited that an arbitrary match expression executed over all metadata of the IU.
+ */
+public class RequiredPropertiesMatch extends Requirement {
+	/**
+	 * Argument $0 must evaluate to a String
+	 * Argument $2 must evaluate to an expression compatible with the match operator "~="
+	 */
+	private static final IExpression PROPERTIES_MATCH = ExpressionUtil.parse(
+			String.format("%s.exists(cap | cap.%s == $0 && cap.%s ~= $1)", //$NON-NLS-1$
+					MEMBER_PROVIDED_CAPABILITIES, MEMBER_NAMESPACE, MEMBER_PROPERTIES));
+
+	public RequiredPropertiesMatch(String namespace, IFilterExpression attrFilter, IMatchExpression<IInstallableUnit> envFilter, int min, int max, boolean greedy, String description) {
+		super(createMatchExpressionFromFilter(namespace, attrFilter), envFilter, min, max, greedy, description);
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder result = new StringBuilder();
+
+		result.append(extractNamespace(getMatches()));
+		result.append("; "); //$NON-NLS-1$
+		result.append(extractPropertiesMatch(getMatches()));
+
+		return result.toString();
+	}
+
+	public static IMatchExpression<IInstallableUnit> createMatchExpressionFromFilter(String namespace, IFilterExpression attrFilter) {
+		Assert.isNotNull(namespace);
+		Assert.isNotNull(attrFilter);
+		IExpressionFactory factory = ExpressionUtil.getFactory();
+		return factory.matchExpression(PROPERTIES_MATCH, namespace, attrFilter);
+	}
+
+	public static String extractNamespace(IMatchExpression<IInstallableUnit> matchExpression) {
+		assertValid(matchExpression);
+		return (String) matchExpression.getParameters()[0];
+	}
+
+	public static IFilterExpression extractPropertiesMatch(IMatchExpression<IInstallableUnit> matchExpression) {
+		assertValid(matchExpression);
+		return (IFilterExpression) matchExpression.getParameters()[1];
+	}
+
+	public static boolean isPropertiesMatchRequirement(IMatchExpression<IInstallableUnit> matchExpression) {
+		return PROPERTIES_MATCH.equals(ExpressionUtil.getOperand(matchExpression));
+	}
+
+	private static void assertValid(IMatchExpression<IInstallableUnit> matchExpression) {
+		if (!isPropertiesMatchRequirement(matchExpression)) {
+			throw new IllegalArgumentException();
+		}
+	}
+}
diff --git a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/Requirement.java b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/Requirement.java
index 036721c..5fe660e 100644
--- a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/Requirement.java
+++ b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/Requirement.java
@@ -11,7 +11,11 @@
  *******************************************************************************/
 package org.eclipse.equinox.internal.p2.metadata;
 
-import org.eclipse.equinox.p2.metadata.*;
+import org.eclipse.equinox.p2.metadata.IInstallableUnit;
+import org.eclipse.equinox.p2.metadata.IProvidedCapability;
+import org.eclipse.equinox.p2.metadata.IRequirement;
+import org.eclipse.equinox.p2.metadata.MetadataFactory;
+import org.eclipse.equinox.p2.metadata.VersionRange;
 import org.eclipse.equinox.p2.metadata.expression.IMatchExpression;
 import org.eclipse.equinox.p2.metadata.expression.IMemberProvider;
 
@@ -66,10 +70,14 @@
 		// Parameters
 		Object[] params = matchExpression.getParameters();
 		if (params.length > 0) {
-			result.append(' ');
+			result.append(" ("); //$NON-NLS-1$
 			for (int i = 0; i < params.length; i++) {
-				result.append(params[i]).append(' ');
+				if (i > 0) {
+					result.append(", "); //$NON-NLS-1$
+				}
+				result.append(params[i]);
 			}
+			result.append(')');
 		}
 
 		return result.toString();
diff --git a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/expression/CoercingComparator.java b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/expression/CoercingComparator.java
index 6cb8008..c6d3168 100644
--- a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/expression/CoercingComparator.java
+++ b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/expression/CoercingComparator.java
@@ -229,8 +229,6 @@
 		Version coerce(Object v) {
 			if (v instanceof Version)
 				return (Version) v;
-			if (v instanceof String)
-				return Version.create((String) v);
 			if (v instanceof String) {
 				try {
 					return Version.create((String) v);
@@ -333,7 +331,7 @@
 	 * @return The coercing comparator
 	 */
 	@SuppressWarnings("unchecked")
-	public static <V extends Object> CoercingComparator<V> getComparator(V value, Object v2) {
+	public static <V> CoercingComparator<V> getComparator(V value, Object v2) {
 		Class<V> vClass = (Class<V>) value.getClass();
 		CoercingComparator<?>[] carr = coercers;
 		int idx = carr.length;
diff --git a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/index/CapabilityIndex.java b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/index/CapabilityIndex.java
index d7b2b3c..8d89b2d 100644
--- a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/index/CapabilityIndex.java
+++ b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/index/CapabilityIndex.java
@@ -110,7 +110,7 @@
 		// index usage query
 		//
 		IMatchExpression<IInstallableUnit> rm = ((IRequirement) rhsObj).getMatches();
-		return RequiredCapability.isSimpleRequirement(rm) ? concatenateUnique(queriedKeys, rm.getParameters()[0]) : getRequirementIDs(rm.createContext(), ((Unary) rm).operand, queriedKeys);
+		return RequiredCapability.isVersionRangeRequirement(rm) ? concatenateUnique(queriedKeys, rm.getParameters()[0]) : getRequirementIDs(rm.createContext(), ((Unary) rm).operand, queriedKeys);
 	}
 
 	@Override
@@ -185,7 +185,7 @@
 				// index usage query
 				//
 				IMatchExpression<IInstallableUnit> rm = ((IRequirement) rhsObj).getMatches();
-				queriedKeys = RequiredCapability.isSimpleRequirement(rm) ? concatenateUnique(queriedKeys, rm.getParameters()[0]) : getRequirementIDs(rm.createContext(), ((Unary) rm).operand, queriedKeys);
+				queriedKeys = RequiredCapability.isVersionRangeRequirement(rm) ? concatenateUnique(queriedKeys, rm.getParameters()[0]) : getRequirementIDs(rm.createContext(), ((Unary) rm).operand, queriedKeys);
 				break;
 
 			default :
diff --git a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/messages.properties b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/messages.properties
index aa3b72b..ac979c2 100644
--- a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/messages.properties
+++ b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/internal/p2/metadata/messages.properties
@@ -60,7 +60,7 @@
 premature_end_of_format_expected_0=Premature end of format, "{0}" expected
 premature_EOS_0=Premature end of string in "{0}"
 provided_capability_name_not_defined=the name of provided capability "{0}" is not defined
-provided_capability_namespace_not_defined=the namespace of the provided capabilty is not defined
+provided_capability_namespace_not_defined=the namespace of the provided capability is not defined
 range_defined_more_then_once=Range defined more then once
 range_max_cannot_be_less_then_range_min=The range maximum must not be less then its minimum
 range_max_cannot_be_zero=The range maximum cannot be zero
diff --git a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/p2/metadata/IProvidedCapability.java b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/p2/metadata/IProvidedCapability.java
index de61165..7cf08a1 100644
--- a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/p2/metadata/IProvidedCapability.java
+++ b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/p2/metadata/IProvidedCapability.java
@@ -6,11 +6,13 @@
 *
 * Contributors:
 *   EclipseSource - initial API and implementation
- *   IBM - ongoing development
+*   IBM - ongoing development
+*   Todor Boev
 ******************************************************************************/
 package org.eclipse.equinox.p2.metadata;
 
 import java.util.Map;
+import org.eclipse.equinox.p2.metadata.expression.IFilterExpression;
 
 /**
  * Describes a capability that is exposed by an installable unit. These capabilities
@@ -29,45 +31,56 @@
  */
 public interface IProvidedCapability {
 	/**
+	 * The name of the property under which the capability version is stored.
+	 * 
+	 * Can be used with {@link #getProperties()}. The same value can be obtained with {@link #getVersion()}
+	 * 
+	 * @since 2.4
+	 */
+	String PROPERTY_VERSION = "version"; //$NON-NLS-1$
+
+	/**
 	 * 
 	 * @return String the namespace of this capability.
 	 * @noreference This method is not intended to be referenced by clients.
 	 */
-	public String getNamespace();
+	String getNamespace();
 
 	/**
 	 * 
-	 * @return String the attribute stored under a key equal to {@link #getNamespace()} attribute of this capability.
+	 * @return String the attribute stored under a key equal to the {@link #getNamespace()} attribute of this capability.
 	 * @noreference This method is not intended to be referenced by clients.
 	 */
-	public String getName();
+	String getName();
 
 	/**
 	 * 
-	 * @return String the special <code>version</code> attribute of this capability.
+	 * @return String the special {@link #PROPERTY_VERSION} attribute of this capability.
 	 * @noreference This method is not intended to be referenced by clients.
 	 */
-	public Version getVersion();
+	Version getVersion();
 
 	/**
+	 * A full description of this capability including the name and the version.
+	 * <p>
+	 * Such a description can be used to match this capability with an {@link IFilterExpression LDAP filter} for example.
 	 * 
-	 * @return A full description of this capability
+	 * @return An unmodifiable map
 	 * @noreference This method is not intended to be referenced by clients.
 	 * @since 2.4
 	 */
-	public Map<String, Object> getAttributes();
+	Map<String, Object> getProperties();
 
 	/**
 	 * Returns whether this provided capability is equal to the given object.
 	 * 
 	 * This method returns <i>true</i> if:
 	 * <ul>
-	 *  <li> Both this object and the given object are of type IProvidedCapability
-	 *  <li> The result of <b>getNamespace()</b> on both objects are equal
-	 *  <li> The result of <b>getAttributes()</b> on both objects are equal
+	 *  <li>Both this object and the given object are of type IProvidedCapability</li>
+	 *  <li>The result of {@link #getNamespace()} on both objects are equal</li>
+	 *  <li>The result of {@link #getProperties()} on both objects are equal</li>
 	 * </ul> 
 	 */
 	@Override
-	public boolean equals(Object other);
-
+	boolean equals(Object other);
 }
\ No newline at end of file
diff --git a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/p2/metadata/MetadataFactory.java b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/p2/metadata/MetadataFactory.java
index e3f89b1..6c9d91a 100644
--- a/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/p2/metadata/MetadataFactory.java
+++ b/bundles/org.eclipse.equinox.p2.metadata/src/org/eclipse/equinox/p2/metadata/MetadataFactory.java
@@ -30,6 +30,7 @@
 import org.eclipse.equinox.internal.p2.metadata.License;
 import org.eclipse.equinox.internal.p2.metadata.ProvidedCapability;
 import org.eclipse.equinox.internal.p2.metadata.RequiredCapability;
+import org.eclipse.equinox.internal.p2.metadata.RequiredPropertiesMatch;
 import org.eclipse.equinox.internal.p2.metadata.Requirement;
 import org.eclipse.equinox.internal.p2.metadata.RequirementChange;
 import org.eclipse.equinox.internal.p2.metadata.ResolvedInstallableUnit;
@@ -37,6 +38,8 @@
 import org.eclipse.equinox.internal.p2.metadata.TouchpointInstruction;
 import org.eclipse.equinox.internal.p2.metadata.TouchpointType;
 import org.eclipse.equinox.internal.p2.metadata.UpdateDescriptor;
+import org.eclipse.equinox.p2.metadata.expression.ExpressionUtil;
+import org.eclipse.equinox.p2.metadata.expression.IFilterExpression;
 import org.eclipse.equinox.p2.metadata.expression.IMatchExpression;
 
 /**
@@ -461,11 +464,29 @@
 	 * Returns a {@link IProvidedCapability} with the given values.
 	 * 
 	 * @param namespace The capability namespace
-	 * @param attributes The description of the capability
+	 * @param properties The description of the capability
 	 * @since 2.4
 	 */
-	public static IProvidedCapability createProvidedCapability(String namespace, Map<String, Object> attributes) {
-		return new ProvidedCapability(namespace, attributes);
+	public static IProvidedCapability createProvidedCapability(String namespace, Map<String, Object> properties) {
+		return new ProvidedCapability(namespace, properties);
+	}
+
+	/**
+	 * Create and return a new requirement ({@link IRequirement}) with the specified values.
+	 * 
+	 * @param namespace the namespace for the requirement. Must not be <code>null</code>.
+	 * @param name the name for the requirement. Must not be <code>null</code>.
+	 * @param range the version range. A value of <code>null</code> is equivalent to {@link VersionRange#emptyRange} and matches all versions.
+	 * @param filter The filter used to evaluate whether this capability is applicable in the
+	 * 	current environment, or <code>null</code> to indicate this capability is always applicable
+	 * @param optional <code>true</code> if this requirement is optional, and <code>false</code> otherwise.
+	 * @param multiple <code>true</code> if this requirement can be satisfied by multiple provided capabilities, or <code>false</code> 
+	 * 	if it requires exactly one match
+	 * @param greedy <code>true</code> if the requirement should be considered greedy and <code>false</code> otherwise
+	 * @return the requirement
+	 */
+	public static IRequirement createRequirement(String namespace, String name, VersionRange range, String filter, boolean optional, boolean multiple, boolean greedy) {
+		return new RequiredCapability(namespace, name, range, InstallableUnit.parseFilter(filter), optional ? 0 : 1, multiple ? Integer.MAX_VALUE : 1, greedy, null);
 	}
 
 	/**
@@ -507,49 +528,6 @@
 	/**
 	 * Create and return a new requirement ({@link IRequirement}) with the specified values.
 	 * 
-	 * @param requirement the match expression
-	 * @param filter The filter used to evaluate whether this capability is applicable in the
-	 * 	current environment, or <code>null</code> to indicate this capability is always applicable
-	 * @param minCard minimum cardinality
-	 * @param maxCard maximum cardinality
-	 * @param greedy <code>true</code> if the requirement should be considered greedy and <code>false</code> otherwise
-	 * @return the requirement
-	 */
-	public static IRequirement createRequirement(IMatchExpression<IInstallableUnit> requirement, IMatchExpression<IInstallableUnit> filter, int minCard, int maxCard, boolean greedy) {
-		// IRequiredCapability is simply a requirement with a match expression derived from a  (namespace, name, version) tripet.
-		// However the xml format also requires that maxCard > 1 or it is serialized in the generic format.
-		// When parsing back from xml try to convert to an IRequiredCapability to retain the representation prior to serialization
-		if (RequiredCapability.isSimpleRequirement(requirement)) {
-			String namespace = RequiredCapability.extractNamespace(requirement);
-			String name = RequiredCapability.extractName(requirement);
-			VersionRange range = RequiredCapability.extractRange(requirement);
-			return new RequiredCapability(namespace, name, range, filter, minCard, maxCard, greedy, null);
-		}
-
-		return new Requirement(requirement, filter, minCard, maxCard, greedy, null);
-	}
-
-	/**
-	 * Create and return a new requirement ({@link IRequirement}) with the specified values.
-	 * 
-	 * @param namespace the namespace for the requirement. Must not be <code>null</code>.
-	 * @param name the name for the requirement. Must not be <code>null</code>.
-	 * @param range the version range. A value of <code>null</code> is equivalent to {@link VersionRange#emptyRange} and matches all versions.
-	 * @param filter The filter used to evaluate whether this capability is applicable in the
-	 * 	current environment, or <code>null</code> to indicate this capability is always applicable
-	 * @param optional <code>true</code> if this requirement is optional, and <code>false</code> otherwise.
-	 * @param multiple <code>true</code> if this requirement can be satisfied by multiple provided capabilities, or <code>false</code> 
-	 * 	if it requires exactly one match
-	 * @param greedy <code>true</code> if the requirement should be considered greedy and <code>false</code> otherwise
-	 * @return the requirement
-	 */
-	public static IRequirement createRequirement(String namespace, String name, VersionRange range, String filter, boolean optional, boolean multiple, boolean greedy) {
-		return new RequiredCapability(namespace, name, range, InstallableUnit.parseFilter(filter), optional ? 0 : 1, multiple ? Integer.MAX_VALUE : 1, greedy, null);
-	}
-
-	/**
-	 * Create and return a new requirement ({@link IRequirement}) with the specified values.
-	 * 
 	 * @param namespace the namespace for the requirement. Must not be <code>null</code>.
 	 * @param name the name for the requirement. Must not be <code>null</code>.
 	 * @param range the version range. A value of <code>null</code> is equivalent to {@link VersionRange#emptyRange} and matches all versions.
@@ -566,28 +544,65 @@
 	}
 
 	/**
+	 * 
+	 * @param namespace
+	 * @param propsFilter filter applied on {@link IProvidedCapability#getProperties()} of every {@link IInstallableUnit#getProvidedCapabilities()}
+	 * @param envFilter matcher over {@link IInstallableUnit#getProperties()}
+	 * @param minCard
+	 * @param maxCard
+	 * @param greedy
+	 * @return the requirement
+	 * @since 2.4
+	 */
+	public static IRequirement createRequirement(String namespace, String propsFilter, IMatchExpression<IInstallableUnit> envFilter, int minCard, int maxCard, boolean greedy) {
+		IFilterExpression attrFilterExpr = ExpressionUtil.parseLDAP(propsFilter);
+		return new RequiredPropertiesMatch(namespace, attrFilterExpr, envFilter, minCard, maxCard, greedy, null);
+	}
+
+	/**
+	 * 
+	 * @param namespace
+	 * @param propsFilter
+	 * @param envFilter
+	 * @param minCard
+	 * @param maxCard
+	 * @param greedy
+	 * @param description
+	 * @return the requirement
+	 * @since 2.4
+	 */
+	public static IRequirement createRequirement(String namespace, IFilterExpression propsFilter, IMatchExpression<IInstallableUnit> envFilter, int minCard, int maxCard, boolean greedy, String description) {
+		return new RequiredPropertiesMatch(namespace, propsFilter, envFilter, minCard, maxCard, greedy, null);
+	}
+
+	/**
+	 * Create and return a new requirement ({@link IRequirement}) with the specified values.
+	 * 
+	 * @param requirement the match expression
+	 * @param envFilter The filter used to evaluate whether this capability is applicable in the
+	 * 	current environment, or <code>null</code> to indicate this capability is always applicable
+	 * @param minCard minimum cardinality
+	 * @param maxCard maximum cardinality
+	 * @param greedy <code>true</code> if the requirement should be considered greedy and <code>false</code> otherwise
+	 * @return the requirement
+	 */
+	public static IRequirement createRequirement(IMatchExpression<IInstallableUnit> requirement, IMatchExpression<IInstallableUnit> envFilter, int minCard, int maxCard, boolean greedy) {
+		return createRequirementInternal(requirement, envFilter, minCard, maxCard, greedy, null);
+	}
+
+	/**
 	 * Create and return a new requirement ({@link IRequirement}) with the specified values.
 	 *  
 	 * @param requirement the match expression
-	 * @param filter the filter, or <code>null</code>
+	 * @param envFilter the filter, or <code>null</code>
 	 * @param minCard minimum cardinality
 	 * @param maxCard maximum cardinality
 	 * @param greedy <code>true</code> if the requirement should be considered greedy and <code>false</code> otherwise
 	 * @param description a <code>String</code> description of the requirement, or <code>null</code>
 	 * @return the requirement
 	 */
-	public static IRequirement createRequirement(IMatchExpression<IInstallableUnit> requirement, IMatchExpression<IInstallableUnit> filter, int minCard, int maxCard, boolean greedy, String description) {
-		// IRequiredCapability is simply a requirement with a match expression derived from a  (namespace, name, version) tripet.
-		// However the xml format also requires that maxCard > 1 or it is serialized in the generic format.
-		// When parsing back from xml try to convert to an IRequiredCapability to retain the representation prior to serialization
-		if (RequiredCapability.isSimpleRequirement(requirement)) {
-			String namespace = RequiredCapability.extractNamespace(requirement);
-			String name = RequiredCapability.extractName(requirement);
-			VersionRange range = RequiredCapability.extractRange(requirement);
-			return new RequiredCapability(namespace, name, range, filter, minCard, maxCard, greedy, description);
-		}
-
-		return new Requirement(requirement, filter, minCard, maxCard, greedy, description);
+	public static IRequirement createRequirement(IMatchExpression<IInstallableUnit> requirement, IMatchExpression<IInstallableUnit> envFilter, int minCard, int maxCard, boolean greedy, String description) {
+		return createRequirementInternal(requirement, envFilter, minCard, maxCard, greedy, description);
 	}
 
 	/**
@@ -737,6 +752,14 @@
 		}
 	}
 
+	/**
+	 * 
+	 * @param descriptors
+	 * @param severity
+	 * @param description
+	 * @param location
+	 * @return A new update descriptor
+	 */
 	public static IUpdateDescriptor createUpdateDescriptor(Collection<IMatchExpression<IInstallableUnit>> descriptors, int severity, String description, URI location) {
 		return new UpdateDescriptor(descriptors, severity, description, location);
 	}
@@ -770,6 +793,26 @@
 		return createUpdateDescriptor(descriptors, severity, description, location);
 	}
 
+	private static IRequirement createRequirementInternal(IMatchExpression<IInstallableUnit> requirement, IMatchExpression<IInstallableUnit> envFilter, int minCard, int maxCard, boolean greedy, String description) {
+		// IRequiredCapability is simply a requirement with a match expression derived from a  (namespace, name, version) tripet.
+		// However the xml format also requires that maxCard > 1 or it is serialized in the generic format.
+		// When parsing back from xml try to convert to an IRequiredCapability to retain the representation prior to serialization
+		if (RequiredCapability.isVersionRangeRequirement(requirement)) {
+			String namespace = RequiredCapability.extractNamespace(requirement);
+			String name = RequiredCapability.extractName(requirement);
+			VersionRange range = RequiredCapability.extractRange(requirement);
+			return new RequiredCapability(namespace, name, range, envFilter, minCard, maxCard, greedy, description);
+		}
+
+		if (RequiredPropertiesMatch.isPropertiesMatchRequirement(requirement)) {
+			String namespace = RequiredPropertiesMatch.extractNamespace(requirement);
+			IFilterExpression attrMatch = RequiredPropertiesMatch.extractPropertiesMatch(requirement);
+			return new RequiredPropertiesMatch(namespace, attrMatch, envFilter, minCard, maxCard, greedy, description);
+		}
+
+		return new Requirement(requirement, envFilter, minCard, maxCard, greedy, description);
+	}
+
 	private static ITouchpointType getCachedTouchpointType(String id, Version version) {
 		for (int i = 0; i < typeCache.length; i++) {
 			if (typeCache[i] != null && typeCache[i].getId().equals(id) && typeCache[i].getVersion().equals(version))
diff --git a/bundles/org.eclipse.equinox.p2.publisher.eclipse/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.publisher.eclipse/META-INF/MANIFEST.MF
index 5ff218a..36dbe0f 100644
--- a/bundles/org.eclipse.equinox.p2.publisher.eclipse/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.publisher.eclipse/META-INF/MANIFEST.MF
@@ -39,6 +39,8 @@
  org.eclipse.osgi.service.resolver;version="1.5.0",
  org.eclipse.osgi.util;version="1.1.0",
  org.osgi.framework;version="1.3.0",
+ org.osgi.framework.wiring;version="1.2.0",
+ org.osgi.resource;version="1.0.0",
  org.osgi.service.application;version="1.1.0",
  org.osgi.service.packageadmin;version="1.2.0"
 Export-Package: org.eclipse.equinox.internal.p2.publisher.compatibility;x-internal:=true,
diff --git a/bundles/org.eclipse.equinox.p2.publisher.eclipse/src/org/eclipse/equinox/p2/publisher/eclipse/BundlesAction.java b/bundles/org.eclipse.equinox.p2.publisher.eclipse/src/org/eclipse/equinox/p2/publisher/eclipse/BundlesAction.java
index b3411b0..c2ec094 100644
--- a/bundles/org.eclipse.equinox.p2.publisher.eclipse/src/org/eclipse/equinox/p2/publisher/eclipse/BundlesAction.java
+++ b/bundles/org.eclipse.equinox.p2.publisher.eclipse/src/org/eclipse/equinox/p2/publisher/eclipse/BundlesAction.java
@@ -12,6 +12,9 @@
  ******************************************************************************/
 package org.eclipse.equinox.p2.publisher.eclipse;
 
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
 import java.io.*;
 import java.util.*;
 import java.util.Map.Entry;
@@ -28,7 +31,7 @@
 import org.eclipse.equinox.p2.metadata.MetadataFactory.InstallableUnitDescription;
 import org.eclipse.equinox.p2.metadata.MetadataFactory.InstallableUnitFragmentDescription;
 import org.eclipse.equinox.p2.metadata.VersionRange;
-import org.eclipse.equinox.p2.metadata.expression.*;
+import org.eclipse.equinox.p2.metadata.expression.IMatchExpression;
 import org.eclipse.equinox.p2.publisher.*;
 import org.eclipse.equinox.p2.publisher.actions.*;
 import org.eclipse.equinox.p2.query.IQueryResult;
@@ -43,6 +46,8 @@
 import org.eclipse.pde.internal.publishing.Activator;
 import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
+import org.osgi.framework.wiring.BundleRequirement;
+import org.osgi.resource.Namespace;
 
 /**
  * Publish IUs for all of the bundles in a given set of locations or described by a set of
@@ -78,7 +83,6 @@
 	public static final String OSGI_BUNDLE_CLASSIFIER = "osgi.bundle"; //$NON-NLS-1$
 	public static final String CAPABILITY_NS_OSGI_BUNDLE = "osgi.bundle"; //$NON-NLS-1$
 	public static final String CAPABILITY_NS_OSGI_FRAGMENT = "osgi.fragment"; //$NON-NLS-1$
-	public static final String CAPABILITY_ATTR_VERSION = "version"; //$NON-NLS-1$
 
 	public static final IProvidedCapability BUNDLE_CAPABILITY = MetadataFactory.createProvidedCapability(PublisherHelper.NAMESPACE_ECLIPSE_TYPE, TYPE_ECLIPSE_BUNDLE, Version.createOSGi(1, 0, 0));
 	public static final IProvidedCapability SOURCE_BUNDLE_CAPABILITY = MetadataFactory.createProvidedCapability(PublisherHelper.NAMESPACE_ECLIPSE_TYPE, TYPE_ECLIPSE_SOURCE, Version.createOSGi(1, 0, 0));
@@ -194,7 +198,7 @@
 		// Process generic requirements
 		ManifestElement[] rawRequireCapHeader = parseManifestHeader(Constants.REQUIRE_CAPABILITY, manifest, bd.getLocation());
 		for (GenericSpecification requiredCap : bd.getGenericRequires()) {
-			addGenericRequirement(requirements, requiredCap, rawRequireCapHeader);
+			addRequirement(requirements, requiredCap, rawRequireCapHeader);
 		}
 
 		iu.setRequirements(requirements.toArray(new IRequirement[requirements.size()]));
@@ -223,7 +227,7 @@
 
 		int capNo = 0;
 		for (GenericDescription genericCap : bd.getGenericCapabilities()) {
-			addGenericCapability(providedCapabilities, genericCap, iu, capNo);
+			addCapability(providedCapabilities, genericCap, iu, capNo);
 			capNo++;
 		}
 
@@ -301,36 +305,45 @@
 		reqsDeps.add(MetadataFactory.createRequirement(PublisherHelper.CAPABILITY_NS_JAVA_PACKAGE, importSpec.getName(), versionRange, null, optional, false, greedy));
 	}
 
-	// TODO Handle all attributes and directives somehow? Especially the "effective" directive.
-	protected void addGenericRequirement(List<IRequirement> reqsDeps, GenericSpecification requireCapSpec, ManifestElement[] rawRequiresPackageHeader) {
-		String ns = requireCapSpec.getType();
-		String ldap = requireCapSpec.getMatchingFilter();
-		String matcher = "providedCapabilities.exists(pc | pc.namespace == '" + ns + "' && pc.attributes ~= filter('" + ldap + "'))"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
-
-		IExpression expr = ExpressionUtil.parse(matcher);
-		IMatchExpression<IInstallableUnit> matchExpr = ExpressionUtil.getFactory().matchExpression(expr);
-
-		// Optional and greedy in order to be backward compatible.
-		IRequirement requireCap = MetadataFactory.createRequirement(matchExpr, null, 0, 1, true);
-
-		reqsDeps.add(requireCap);
-	}
-
 	protected void addRequireBundleRequirement(List<IRequirement> reqsDeps, BundleSpecification requiredBundle, ManifestElement[] rawRequireBundleHeader) {
 		final boolean optional = requiredBundle.isOptional();
 		final boolean greedy;
-		if (optional)
+		if (optional) {
 			greedy = INSTALLATION_GREEDY.equals(getInstallationDirective(requiredBundle.getName(), rawRequireBundleHeader));
-		else
+		} else {
 			greedy = true;
+		}
 		reqsDeps.add(MetadataFactory.createRequirement(CAPABILITY_NS_OSGI_BUNDLE, requiredBundle.getName(), PublisherHelper.fromOSGiVersionRange(requiredBundle.getVersionRange()), null, optional ? 0 : 1, 1, greedy));
 	}
 
-	protected void addGenericCapability(List<IProvidedCapability> caps, GenericDescription provideCapDesc, InstallableUnitDescription iu, int capNo) {
-		String capNs = provideCapDesc.getType();
-		Map<String, Object> capAttrs = new HashMap<>(provideCapDesc.getDeclaredAttributes());
+	// TODO Handle the "effective:=" directive somehow?
+	protected void addRequirement(List<IRequirement> reqsDeps, GenericSpecification requireCapSpec, ManifestElement[] rawRequireCapabilities) {
+		BundleRequirement req = requireCapSpec.getRequirement();
 
-		// Resolve the p2 name
+		String namespace = req.getNamespace();
+		Map<String, String> directives = req.getDirectives();
+
+		String capFilter = directives.get(Namespace.REQUIREMENT_FILTER_DIRECTIVE);
+		boolean optional = directives.get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE) == Namespace.RESOLUTION_OPTIONAL;
+		boolean greedy = optional
+				? INSTALLATION_GREEDY.equals(directives.get(INSTALLATION_DIRECTIVE))
+				: true;
+
+		IRequirement requireCap = MetadataFactory.createRequirement(namespace, capFilter, null, optional ? 0 : 1, 1, greedy);
+		reqsDeps.add(requireCap);
+	}
+
+	protected void addCapability(List<IProvidedCapability> caps, GenericDescription provideCapDesc, InstallableUnitDescription iu, int capNo) {
+		// Convert the values to String, Version, List of String or Version
+		Map<String, Object> capAttrs = provideCapDesc.getDeclaredAttributes()
+				.entrySet()
+				.stream()
+				.collect(toMap(Entry::getKey, e -> convertAttribute(e.getValue())));
+
+		// Resolve the namespace
+		String capNs = provideCapDesc.getType();
+
+		// Resolve the mandatory p2 name
 		// By convention OSGi capabilities have an attribute named like the capability namespace.
 		// If this is not the case synthesize a unique name (e.g. "osgi.service" has an "objectClass" attribute instead).
 		// TODO If present but not a String log a warning somehow that it is ignored? Or fail the publication?
@@ -338,28 +351,32 @@
 				capNs,
 				(k, v) -> (v instanceof String) ? v : String.format("%s_%s-%s", iu.getId(), iu.getVersion(), capNo)); //$NON-NLS-1$
 
-		// Convert all OSGi versions to P2 versions
-		for (String key : new HashSet<>(capAttrs.keySet())) {
-			Object val = capAttrs.get(key);
-			if (!(val instanceof org.osgi.framework.Version)) {
-				continue;
-			}
-			org.osgi.framework.Version osgiVer = (org.osgi.framework.Version) val;
-			Version p2Ver = Version.createOSGi(osgiVer.getMajor(), osgiVer.getMinor(), osgiVer.getMicro(), osgiVer.getQualifier());
-			capAttrs.put(key, p2Ver);
-		}
-
-		// Resolve the version
+		// Resolve the mandatory p2 version
 		// By convention versioned OSGi capabilities have a "version" attribute containing the OSGi Version object
 		// If this is not the case use an empty version (e.g. "osgi.ee" has a list of versions).
 		// TODO If present but not a Version log a warning somehow that it is ignored? Or fail the publication?
 		capAttrs.compute(
-				CAPABILITY_ATTR_VERSION,
+				IProvidedCapability.PROPERTY_VERSION,
 				(k, v) -> (v instanceof Version) ? v : Version.emptyVersion);
 
 		caps.add(MetadataFactory.createProvidedCapability(capNs, capAttrs));
 	}
 
+	private Object convertAttribute(Object attr) {
+		if (attr instanceof Collection<?>) {
+			return ((Collection<?>) attr).stream().map(this::convertScalarAttribute).collect(toList());
+		}
+		return convertScalarAttribute(attr);
+	}
+
+	private Object convertScalarAttribute(Object attr) {
+		if (attr instanceof org.osgi.framework.Version) {
+			org.osgi.framework.Version osgiVer = (org.osgi.framework.Version) attr;
+			return Version.createOSGi(osgiVer.getMajor(), osgiVer.getMinor(), osgiVer.getMicro(), osgiVer.getQualifier());
+		}
+		return attr.toString();
+	}
+
 	static VersionRange computeUpdateRange(org.osgi.framework.Version base) {
 		VersionRange updateRange = null;
 		if (!base.equals(org.osgi.framework.Version.emptyVersion)) {
diff --git a/bundles/org.eclipse.equinox.p2.publisher/src/org/eclipse/equinox/p2/publisher/AbstractPublisherAction.java b/bundles/org.eclipse.equinox.p2.publisher/src/org/eclipse/equinox/p2/publisher/AbstractPublisherAction.java
index 7128848..3382234 100644
--- a/bundles/org.eclipse.equinox.p2.publisher/src/org/eclipse/equinox/p2/publisher/AbstractPublisherAction.java
+++ b/bundles/org.eclipse.equinox.p2.publisher/src/org/eclipse/equinox/p2/publisher/AbstractPublisherAction.java
@@ -395,7 +395,7 @@
 		}
 
 		IRequiredCapability requiredCapability = (IRequiredCapability) requirement;
-		if (!RequiredCapability.isSimpleRequirement(requiredCapability.getMatches())) {
+		if (!RequiredCapability.isVersionRangeRequirement(requiredCapability.getMatches())) {
 			return null;
 		}
 
diff --git a/bundles/org.eclipse.equinox.p2.repository/src/org/eclipse/equinox/internal/p2/persistence/XMLConstants.java b/bundles/org.eclipse.equinox.p2.repository/src/org/eclipse/equinox/internal/p2/persistence/XMLConstants.java
index f1af9f1..de1601d 100644
--- a/bundles/org.eclipse.equinox.p2.repository/src/org/eclipse/equinox/internal/p2/persistence/XMLConstants.java
+++ b/bundles/org.eclipse.equinox.p2.repository/src/org/eclipse/equinox/internal/p2/persistence/XMLConstants.java
@@ -11,6 +11,9 @@
  *******************************************************************************/
 package org.eclipse.equinox.internal.p2.persistence;
 
+import java.util.List;
+import org.eclipse.equinox.p2.metadata.Version;
+
 public interface XMLConstants {
 
 	// Constants used in defining a default processing instruction
@@ -29,7 +32,20 @@
 	public static final String PROPERTY_ELEMENT = "property"; //$NON-NLS-1$
 	public static final String PROPERTY_NAME_ATTRIBUTE = "name"; //$NON-NLS-1$
 	public static final String PROPERTY_VALUE_ATTRIBUTE = "value"; //$NON-NLS-1$
+	public static final String PROPERTY_TYPE_ATTRIBUTE = "type"; //$NON-NLS-1$
 	public static final String[] PROPERTY_ATTRIBUTES = new String[] {PROPERTY_NAME_ATTRIBUTE, PROPERTY_VALUE_ATTRIBUTE};
+	public static final String[] PROPERTY_OPTIONAL_ATTRIBUTES = new String[] {PROPERTY_TYPE_ATTRIBUTE};
+	public static final String PROPERTY_TYPE_LIST = List.class.getSimpleName();
+	public static final String PROPERTY_TYPE_STRING = String.class.getSimpleName();
+	public static final String PROPERTY_TYPE_INTEGER = Integer.class.getSimpleName();
+	public static final String PROPERTY_TYPE_LONG = Long.class.getSimpleName();
+	public static final String PROPERTY_TYPE_FLOAT = Float.class.getSimpleName();
+	public static final String PROPERTY_TYPE_DOUBLE = Double.class.getSimpleName();
+	public static final String PROPERTY_TYPE_BYTE = Byte.class.getSimpleName();
+	public static final String PROPERTY_TYPE_SHORT = Short.class.getSimpleName();
+	public static final String PROPERTY_TYPE_CHARACTER = Character.class.getSimpleName();
+	public static final String PROPERTY_TYPE_BOOLEAN = Boolean.class.getSimpleName();
+	public static final String PROPERTY_TYPE_VERSION = Version.class.getSimpleName();
 
 	// Constants for the names of common general attributes
 	public static final String ID_ATTRIBUTE = "id"; //$NON-NLS-1$
diff --git a/bundles/org.eclipse.equinox.p2.repository/src/org/eclipse/equinox/internal/p2/persistence/XMLWriter.java b/bundles/org.eclipse.equinox.p2.repository/src/org/eclipse/equinox/internal/p2/persistence/XMLWriter.java
index 6973a2f..3a532f6 100644
--- a/bundles/org.eclipse.equinox.p2.repository/src/org/eclipse/equinox/internal/p2/persistence/XMLWriter.java
+++ b/bundles/org.eclipse.equinox.p2.repository/src/org/eclipse/equinox/internal/p2/persistence/XMLWriter.java
@@ -10,11 +10,11 @@
  *******************************************************************************/
 package org.eclipse.equinox.internal.p2.persistence;
 
+import static java.util.stream.Collectors.joining;
+
 import java.io.*;
 import java.nio.charset.StandardCharsets;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Stack;
+import java.util.*;
 import org.eclipse.equinox.p2.metadata.Version;
 
 public class XMLWriter implements XMLConstants {
@@ -220,28 +220,82 @@
 		this.pw.flush();
 	}
 
-	public void writeProperties(Map<String, String> properties) {
+	public void writeProperties(Map<String, ?> properties) {
 		writeProperties(PROPERTIES_ELEMENT, properties);
 	}
 
-	public void writeProperties(String propertiesElement, Map<String, String> properties) {
-		if (properties != null && properties.size() > 0) {
-			start(propertiesElement);
-			attribute(COLLECTION_SIZE_ATTRIBUTE, properties.size());
-			for (Entry<String, String> entry : properties.entrySet()) {
-				writeProperty(entry.getKey(), entry.getValue());
-			}
-			end(propertiesElement);
+	public void writeProperties(String propertiesElement, Map<String, ?> properties) {
+		if (properties == null || properties.isEmpty()) {
+			return;
 		}
+
+		start(propertiesElement);
+		attribute(COLLECTION_SIZE_ATTRIBUTE, properties.size());
+		properties.forEach(this::writeProperty);
+		end();
 	}
 
-	public void writeProperty(String name, String value) {
+	public void writeProperty(String name, Object value) {
+		String type;
+		String valueStr;
+
+		if (Collection.class.isAssignableFrom(value.getClass())) {
+			Collection<?> coll = (Collection<?>) value;
+
+			type = PROPERTY_TYPE_LIST;
+			String elType = resolvePropertyType(coll.iterator().next());
+			if (elType != null) {
+				type += String.format("<%s>", elType); //$NON-NLS-1$
+			}
+
+			valueStr = coll.stream().map(Object::toString).collect(joining(",")); //$NON-NLS-1$
+		} else {
+			type = resolvePropertyType(value);
+			valueStr = value.toString();
+		}
+
 		start(PROPERTY_ELEMENT);
 		attribute(PROPERTY_NAME_ATTRIBUTE, name);
-		attribute(PROPERTY_VALUE_ATTRIBUTE, value);
+		attribute(PROPERTY_VALUE_ATTRIBUTE, valueStr);
+		attributeOptional(PROPERTY_TYPE_ATTRIBUTE, type);
 		end();
 	}
 
+	private String resolvePropertyType(Object value) {
+		if (value instanceof Integer) {
+			return PROPERTY_TYPE_INTEGER;
+		}
+		if (value instanceof Long) {
+			return PROPERTY_TYPE_LONG;
+		}
+		if (value instanceof Float) {
+			return PROPERTY_TYPE_FLOAT;
+		}
+		if (value instanceof Double) {
+			return PROPERTY_TYPE_DOUBLE;
+		}
+		if (value instanceof Byte) {
+			return PROPERTY_TYPE_BYTE;
+		}
+		if (value instanceof Short) {
+			return PROPERTY_TYPE_SHORT;
+		}
+		if (value instanceof Character) {
+			return PROPERTY_TYPE_CHARACTER;
+		}
+		if (value instanceof Boolean) {
+			return PROPERTY_TYPE_BOOLEAN;
+		}
+		if (value instanceof Version) {
+			return PROPERTY_TYPE_VERSION;
+		}
+
+		// Null is read back as String
+		// NOTE: Using string as default is needed for backward compatibility with properties that are always String like
+		// the IU properties
+		return null;
+	}
+
 	protected static String attributeImage(String name, String value) {
 		if (value == null) {
 			return ""; // optional attribute with no value //$NON-NLS-1$
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/director/AutomatedDirectorTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/director/AutomatedDirectorTest.java
index f229b4e..d726d6a 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/director/AutomatedDirectorTest.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/director/AutomatedDirectorTest.java
@@ -10,14 +10,16 @@
 
 import java.util.HashMap;
 import java.util.Map;
-import junit.framework.Test;
-import junit.framework.TestSuite;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
 import org.eclipse.equinox.internal.p2.director.ProfileChangeRequest;
 import org.eclipse.equinox.internal.provisional.p2.director.IDirector;
 import org.eclipse.equinox.p2.engine.IProfile;
-import org.eclipse.equinox.p2.metadata.*;
+import org.eclipse.equinox.p2.metadata.IInstallableUnit;
+import org.eclipse.equinox.p2.metadata.IProvidedCapability;
+import org.eclipse.equinox.p2.metadata.IRequirement;
+import org.eclipse.equinox.p2.metadata.MetadataFactory;
+import org.eclipse.equinox.p2.metadata.Version;
 import org.eclipse.equinox.p2.metadata.expression.IMatchExpression;
 import org.eclipse.equinox.p2.tests.AbstractProvisioningTest;
 
@@ -25,20 +27,6 @@
  * Various automated tests of the {@link IDirector} API.
  */
 public class AutomatedDirectorTest extends AbstractProvisioningTest {
-	//private static Version version = Version.createOSGi(1, 0, 0);
-
-	public static Test suite() {
-		return new TestSuite(AutomatedDirectorTest.class);
-	}
-
-	public AutomatedDirectorTest() {
-		super("");
-	}
-
-	public AutomatedDirectorTest(String name) {
-		super(name);
-	}
-
 	/**
 	 * Tests installing an IU that has a filtered dependency on another IU. When
 	 * the filter is satisfied, the dependency is active and the required IU should
@@ -46,26 +34,39 @@
 	 * and the second IU should not be installed.
 	 */
 	public void testInstallFilteredCapability() {
-		//The IU that is required
+		final String envKey = "filterKey";
+		final String envVal = "true";
+
+		// The IU that is required
 		IInstallableUnit requiredIU = createIU("required." + getName());
 
 		// The IU to be installed
-		IMatchExpression<IInstallableUnit> filter = createFilter("FilterKey", "true");
-		IRequirement capability = MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, requiredIU.getId(), ANY_VERSION, filter, false, false);
-		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), new IRequirement[] {capability});
+		IMatchExpression<IInstallableUnit> requirementFilter = createFilter(envKey, envVal);
+		IRequirement[] requires = new IRequirement[] {
+				MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, requiredIU.getId(), ANY_VERSION, requirementFilter, false, false)
+		};
+		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), requires);
 
+		// Metadata repository
 		IInstallableUnit[] allUnits = new IInstallableUnit[] {requiredIU, toInstallIU};
 		createTestMetdataRepository(allUnits);
-		IDirector director = createDirector();
 
-		//Install into a profile in which the filter is satisfied
+		// Install into a profile in which the requirement filter is satisfied
 		Map<String, String> properties = new HashMap<>();
-		properties.put(IProfile.PROP_ENVIRONMENTS, "FilterKey=true");
+		properties.put(IProfile.PROP_ENVIRONMENTS, envKey + "=" + envVal);
+
+		// Profile
 		IProfile satisfied = createProfile("Satisfied." + getName(), properties);
+
+		// Profile change request
 		ProfileChangeRequest request = new ProfileChangeRequest(satisfied);
 		request.add(toInstallIU);
+
+		// Provision
+		IDirector director = createDirector();
 		IStatus result = director.provision(request, null, null);
-		assertTrue("1.0", result.isOK());
+
+		assertTrue(result.isOK());
 		assertProfileContains("1.1", satisfied, allUnits);
 	}
 
@@ -73,25 +74,41 @@
 	 * Tests installing an IU that has an optional prerequisite that is available.
 	 */
 	public void testInstallOptionalAvailable() {
-		String capabilityId = "test." + getName();
+		final String capabilityNamespace = "test.capability";
+		final String capabilityName = "test." + getName();
+		final Version capabilityVersion = DEFAULT_VERSION;
+
 		//The IU that exports the capability
-		IInstallableUnit requiredIU = createIU("required." + getName(), new IProvidedCapability[] {MetadataFactory.createProvidedCapability("test.capability", capabilityId, DEFAULT_VERSION)});
+		IProvidedCapability[] provides = new IProvidedCapability[] {
+				MetadataFactory.createProvidedCapability(capabilityNamespace, capabilityName, capabilityVersion)
+		};
+		IInstallableUnit requiredIU = createIU("required." + getName(), provides);
 
 		//The IU that optionally requires the capability
-		IRequirement required = MetadataFactory.createRequirement("test.capability", capabilityId, ANY_VERSION, null, /* optional=> */true, /* multiple=> */false, /* greedy=>*/false);
-		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), new IRequirement[] {required});
+		IRequirement[] requires = new IRequirement[] {
+				MetadataFactory.createRequirement(capabilityNamespace, capabilityName, ANY_VERSION, null, /* optional=> */true, /* multiple=> */false, /* greedy=>*/false)
+		};
+		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), requires);
 
+		// Metadata repository
 		IInstallableUnit[] allUnits = new IInstallableUnit[] {toInstallIU, requiredIU};
-		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		createTestMetdataRepository(allUnits);
 
 		IProfile profile = createProfile("TestProfile." + getName());
-		IDirector director = createDirector();
+
+		// Change request
 		ProfileChangeRequest request = new ProfileChangeRequest(profile);
+		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		request.addInstallableUnits(toInstallArray);
+
+		// Provision
+		IDirector director = createDirector();
 		IStatus result = director.provision(request, null, null);
-		if (!result.isOK())
+		if (!result.isOK()) {
 			LogHelper.log(result);
+		}
+
+		// The requiredIu is not installed, because the optional requirement is not greedy
 		assertTrue("1.0", result.isOK());
 		assertProfileContains("1.1", profile, toInstallArray);
 	}
@@ -100,21 +117,34 @@
 	 * Tests installing an IU that has an optional prerequisite that is not available.
 	 */
 	public void testInstallOptionalUnavailable() {
-		String capabilityId = "test." + getName();
-		//no IU will be available that exports this capability
-		IRequirement required = MetadataFactory.createRequirement("test.capability", capabilityId, ANY_VERSION, null, true, false);
-		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), new IRequirement[] {required});
+		final String capabilityNamespace = "test.capability";
+		final String capabilityName = "test." + getName();
 
+		// no IU will be available that exports this capability
+		IRequirement[] requires = new IRequirement[] {
+				MetadataFactory.createRequirement(capabilityNamespace, capabilityName, ANY_VERSION, null, /* optional=> */true, /* multiple=> */false)
+		};
+		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), requires);
+
+		// Metadata repo
 		IInstallableUnit[] allUnits = new IInstallableUnit[] {toInstallIU};
 		createTestMetdataRepository(allUnits);
 
+		// Profile
 		IProfile profile = createProfile("TestProfile." + getName());
-		IDirector director = createDirector();
+
+		// Change request
 		ProfileChangeRequest request = new ProfileChangeRequest(profile);
 		request.addInstallableUnits(allUnits);
+
+		// Provision
+		IDirector director = createDirector();
 		IStatus result = director.provision(request, null, null);
-		if (!result.isOK())
+		if (!result.isOK()) {
 			LogHelper.log(result);
+		}
+
+		// The UI is installed because the requirement is optional
 		assertTrue("1.0", result.isOK());
 		assertProfileContains("1.1", profile, allUnits);
 	}
@@ -124,30 +154,50 @@
 	 * the capability has a platform filter that is not satisfied.
 	 */
 	public void testInstallPlatformFilter() {
-		//The IU that exports the capability
-		String capabilityId = "test." + getName();
-		IProvidedCapability[] provides = new IProvidedCapability[] {MetadataFactory.createProvidedCapability("test.capability", capabilityId, DEFAULT_VERSION)};
-		IInstallableUnit requiredIU = createIU("required." + getName(), createFilter("osgi.os", "blort"), provides);
+		// Profile environment
+		final String osKey = "osgi.os";
+		final String osVal = "blort";
 
-		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), createRequiredCapabilities("test.capability", capabilityId, ANY_VERSION, (IMatchExpression<IInstallableUnit>) null));
+		// Test capability
+		final String capabilityNamespace = "test.capability";
+		final String capabilityName = "test." + getName();
+		final Version capabilityVersion = DEFAULT_VERSION;
 
+		// The IU that exports the capability
+		IProvidedCapability[] provides = new IProvidedCapability[] {MetadataFactory.createProvidedCapability(capabilityNamespace, capabilityName, capabilityVersion)};
+		IInstallableUnit requiredIU = createIU("required." + getName(), createFilter(osKey, osVal), provides);
+
+		// Installed IU
+		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), createRequiredCapabilities(capabilityNamespace, capabilityName, ANY_VERSION, (IMatchExpression<IInstallableUnit>) null));
+
+		// Metadata repo
 		IInstallableUnit[] allUnits = new IInstallableUnit[] {requiredIU, toInstallIU};
-		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		createTestMetdataRepository(allUnits);
 
-		IProfile profile = createProfile("TestProfile." + getName());
 		IDirector director = createDirector();
+
+		// Profile that does not satisfy the OS requirement
+		IProfile profile = createProfile("TestProfile." + getName());
+
+		// Request
 		ProfileChangeRequest request = new ProfileChangeRequest(profile);
+		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		request.addInstallableUnits(toInstallArray);
+
+		// Provision - should fail since requireIU can't be installed on the current environment
 		IStatus result = director.provision(request, null, null);
 		assertTrue("1.0", !result.isOK());
 
-		//try again with the filter satisfied
+		// New profile that satisfies the OS requirement
 		Map<String, String> properties = new HashMap<>();
-		properties.put(IProfile.PROP_ENVIRONMENTS, "osgi.os=blort");
-		IProfile profile2 = createProfile("TestProfile2." + getName(), properties);
-		request = new ProfileChangeRequest(profile2);
+		properties.put(IProfile.PROP_ENVIRONMENTS, osKey + "=" + osVal);
+		profile = createProfile("TestProfile2." + getName(), properties);
+
+		// New request
+		request = new ProfileChangeRequest(profile);
 		request.addInstallableUnits(toInstallArray);
+
+		// New provisioning - should succeed
 		result = director.provision(request, null, null);
 		assertTrue("2.0", result.isOK());
 	}
@@ -156,27 +206,42 @@
 	 * Tests installing an IU that has an unsatisfied platform filter
 	 */
 	public void testInstallPlatformFilterUnsatisfied() {
-		//The IU to install
-		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), createFilter("osgi.os", "blort"), NO_PROVIDES);
+		// Profile environment
+		final String osKey = "osgi.os";
+		final String osVal = "blort";
+
+		// The IU to install that needs a concrete environment
+		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), createFilter(osKey, osVal), NO_PROVIDES);
+
+		// Metadata repo
 		IInstallableUnit[] allUnits = new IInstallableUnit[] {toInstallIU};
-		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		createTestMetdataRepository(allUnits);
 
+		// Profile without a matching environment
 		IProfile profile = createProfile("TestProfile." + getName());
-		IDirector director = createDirector();
-		ProfileChangeRequest request = new ProfileChangeRequest(profile);
-		request.addInstallableUnits(toInstallArray);
-		IStatus result = director.provision(request, null, null);
-		assertTrue("1.0", !result.isOK());
 
-		//try again with the filter satisfied
-		Map<String, String> properties = new HashMap<>();
-		properties.put(IProfile.PROP_ENVIRONMENTS, "osgi.os=blort");
-		IProfile profile2 = createProfile("TestProfile2." + getName(), properties);
-		request = new ProfileChangeRequest(profile2);
+		// Change request
+		ProfileChangeRequest request = new ProfileChangeRequest(profile);
+		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		request.addInstallableUnits(toInstallArray);
+
+		// Provisioning failure: incompatible environment
+		IDirector director = createDirector();
+		IStatus result = director.provision(request, null, null);
+		assertTrue(!result.isOK());
+
+		// Profile with matching environment
+		Map<String, String> properties = new HashMap<>();
+		properties.put(IProfile.PROP_ENVIRONMENTS, osKey + "=" + osVal);
+		profile = createProfile("TestProfile2." + getName(), properties);
+
+		// New change request
+		request = new ProfileChangeRequest(profile);
+		request.addInstallableUnits(toInstallArray);
+
+		// Provisioning success
 		result = director.provision(request, null, null);
-		assertTrue("2.0", result.isOK());
+		assertTrue(result.isOK());
 	}
 
 	/**
@@ -184,24 +249,39 @@
 	 * that the IU providing the capability is installed.
 	 */
 	public void testSimpleInstallRequired() {
-		String capabilityId = "test." + getName();
+		// Test capability
+		String capabilityNamespace = "test.capability";
+		String capabilityName = "test." + getName();
+
 		//The IU that exports the capability
-		IInstallableUnit requiredIU = createIU("required." + getName(), new IProvidedCapability[] {MetadataFactory.createProvidedCapability("test.capability", capabilityId, DEFAULT_VERSION)});
+		IProvidedCapability[] provides = new IProvidedCapability[] {
+				MetadataFactory.createProvidedCapability(capabilityNamespace, capabilityName, DEFAULT_VERSION)
+		};
+		IInstallableUnit requiredIU = createIU("required." + getName(), provides);
 
-		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), createRequiredCapabilities("test.capability", capabilityId, ANY_VERSION, (IMatchExpression<IInstallableUnit>) null));
+		// The IU that requires the capability
+		IRequirement[] requires = createRequiredCapabilities(capabilityNamespace, capabilityName, ANY_VERSION, (IMatchExpression<IInstallableUnit>) null);
+		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), requires);
 
+		// Crate the metadata repo
 		IInstallableUnit[] allUnits = new IInstallableUnit[] {requiredIU, toInstallIU};
-		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		createTestMetdataRepository(allUnits);
 
+		// Provision
 		IProfile profile = createProfile("TestProfile." + getName());
 
-		IDirector director = createDirector();
+		// Create the profile change request
 		ProfileChangeRequest request = new ProfileChangeRequest(profile);
+		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		request.addInstallableUnits(toInstallArray);
+
+		// Provision
+		IDirector director = createDirector();
 		IStatus result = director.provision(request, null, null);
-		if (!result.isOK())
+		if (!result.isOK()) {
 			LogHelper.log(result);
+		}
+
 		assertTrue("1.0", result.isOK());
 		assertProfileContains("1.1", profile, allUnits);
 	}
@@ -211,24 +291,34 @@
 	 * specifying a version range (any version will do).
 	 */
 	public void testInstallRequiredNoVersion() {
-		//The IU that is needed
+		// The IU that is needed
 		IInstallableUnit requiredIU = createIU("required." + getName());
 
-		IRequirement capability = MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, requiredIU.getId(), null, null, false, false);
-		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), new IRequirement[] {capability});
+		// The IU that is installed
+		IRequirement[] requires = new IRequirement[] {
+				MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, requiredIU.getId(), null, null, false, false)
+		};
+		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), requires);
 
+		// Metadata repo
 		IInstallableUnit[] allUnits = new IInstallableUnit[] {requiredIU, toInstallIU};
-		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		createTestMetdataRepository(allUnits);
 
+		// Profile
 		IProfile profile = createProfile("TestProfile." + getName());
 
-		IDirector director = createDirector();
+		// Profile change request
 		ProfileChangeRequest request = new ProfileChangeRequest(profile);
+		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		request.addInstallableUnits(toInstallArray);
+
+		// Provision
+		IDirector director = createDirector();
 		IStatus result = director.provision(request, null, null);
-		if (!result.isOK())
+		if (!result.isOK()) {
 			LogHelper.log(result);
+		}
+
 		assertTrue("1.0", result.isOK());
 		assertProfileContains("1.1", profile, allUnits);
 	}
@@ -239,25 +329,34 @@
 	 * capability on the IU namespace.
 	 */
 	public void testSimpleInstallRequiredIU() {
-		//The IU that exports the capability
+		// The IU that is required. It exports it's identity capability by default.
 		IInstallableUnit requiredIU = createIU("required." + getName());
 
-		IRequirement capability = MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, requiredIU.getId(), ANY_VERSION, null, false, false);
-		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), new IRequirement[] {capability});
+		// The IU that is installed
+		IRequirement[] requires = new IRequirement[] {
+				MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, requiredIU.getId(), ANY_VERSION, null, false, false)
+		};
+		IInstallableUnit toInstallIU = createIU("toInstall." + getName(), requires);
 
+		// Create the metadata repo
 		IInstallableUnit[] allUnits = new IInstallableUnit[] {requiredIU, toInstallIU};
-		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		createTestMetdataRepository(allUnits);
 
 		IProfile profile = createProfile("TestProfile." + getName());
 
-		IDirector director = createDirector();
+		// Create the profile change request
 		ProfileChangeRequest request = new ProfileChangeRequest(profile);
+		IInstallableUnit[] toInstallArray = new IInstallableUnit[] {toInstallIU};
 		request.addInstallableUnits(toInstallArray);
+
+		// Provision
+		IDirector director = createDirector();
 		IStatus result = director.provision(request, null, null);
-		if (!result.isOK())
+		if (!result.isOK()) {
 			LogHelper.log(result);
-		assertTrue("1.0", result.isOK());
+		}
+
+		assertTrue(result.isOK());
 		assertProfileContains("1.1", profile, allUnits);
 	}
 
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/engine/PhaseSetTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/engine/PhaseSetTest.java
index f085f94..609d1d6 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/engine/PhaseSetTest.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/engine/PhaseSetTest.java
@@ -12,21 +12,42 @@
 
 import java.io.File;
 import java.net.URI;
-import java.util.*;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
-import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.equinox.internal.p2.director.ProfileChangeRequest;
-import org.eclipse.equinox.internal.p2.engine.*;
+import org.eclipse.equinox.internal.p2.engine.BeginOperationEvent;
+import org.eclipse.equinox.internal.p2.engine.CommitOperationEvent;
+import org.eclipse.equinox.internal.p2.engine.EngineSession;
+import org.eclipse.equinox.internal.p2.engine.InstallableUnitOperand;
+import org.eclipse.equinox.internal.p2.engine.Phase;
+import org.eclipse.equinox.internal.p2.engine.PhaseEvent;
+import org.eclipse.equinox.internal.p2.engine.PhaseSet;
+import org.eclipse.equinox.internal.p2.engine.ProfileEvent;
+import org.eclipse.equinox.internal.p2.engine.RollbackOperationEvent;
 import org.eclipse.equinox.internal.p2.repository.DownloadProgressEvent;
 import org.eclipse.equinox.internal.p2.touchpoint.natives.Util;
 import org.eclipse.equinox.internal.provisional.p2.core.eventbus.ProvisioningListener;
 import org.eclipse.equinox.internal.provisional.p2.repository.RepositoryEvent;
 import org.eclipse.equinox.p2.core.ProvisionException;
-import org.eclipse.equinox.p2.engine.*;
+import org.eclipse.equinox.p2.engine.IEngine;
+import org.eclipse.equinox.p2.engine.IPhaseSet;
+import org.eclipse.equinox.p2.engine.IProfile;
+import org.eclipse.equinox.p2.engine.IProvisioningPlan;
+import org.eclipse.equinox.p2.engine.PhaseSetFactory;
+import org.eclipse.equinox.p2.engine.ProvisioningContext;
 import org.eclipse.equinox.p2.metadata.IInstallableUnit;
-import org.eclipse.equinox.p2.query.*;
+import org.eclipse.equinox.p2.query.IQuery;
+import org.eclipse.equinox.p2.query.IQueryResult;
+import org.eclipse.equinox.p2.query.QueryUtil;
 import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
 import org.eclipse.equinox.p2.tests.AbstractProvisioningTest;
 import org.eclipse.equinox.p2.tests.TestActivator;
@@ -176,7 +197,7 @@
 			getArtifactRepositoryManager().loadRepository(repoURI, null);
 			doProvisioning(repoURI, phaseSet, query, expectedCode, monitor);
 			// make sure the listener handles all event already that are dispatched asynchronously
-			listener.latch.await(10, TimeUnit.SECONDS);
+			listener.latch.await(15, TimeUnit.SECONDS);
 			assertFalse("Engine still do provisioning after pausing.", listener.hasProvisioningEventAfterPaused);
 			pauseJob.join();
 		} finally {
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/RequirementToString.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/RequirementToString.java
index 1ccc188..476d0da 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/RequirementToString.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/RequirementToString.java
@@ -23,18 +23,23 @@
 public class RequirementToString extends TestCase {
 	public void testRequirementWithEmptyRange() {
 		IRequirement req = MetadataFactory.createRequirement("expectedNameSpace", "expectedName", VersionRange.emptyRange, null, false, false);
-		assertEquals("expectedNameSpace expectedName 0.0.0", req.toString());
+		assertEquals("expectedNameSpace; expectedName 0.0.0", req.toString());
 	}
 
 	public void testStandardRequirement() {
 		IRequirement req = MetadataFactory.createRequirement("expectedNameSpace", "expectedName", new VersionRange("[1.0.0, 2.0.0)"), null, false, false);
-		assertEquals("expectedNameSpace expectedName [1.0.0,2.0.0)", req.toString());
+		assertEquals("expectedNameSpace; expectedName [1.0.0,2.0.0)", req.toString());
+	}
+
+	public void testPropertiesRequirement() {
+		IRequirement req = MetadataFactory.createRequirement("expectedNameSpace", "(key=val)", null, 1, 1, true);
+		assertEquals("expectedNameSpace; (key=val)", req.toString());
 	}
 
 	public void testFancyRequirement() {
-		Object[] expressionParameters = new Object[] {"expectedId1, expectedVersion1", "expectedId2, expectedVersion2"};
+		Object[] expressionParameters = new Object[] {"expectedId1", "expectedVersion1", "expectedId2", "expectedVersion2"};
 		IMatchExpression<IInstallableUnit> iuMatcher = ExpressionUtil.getFactory().<IInstallableUnit> matchExpression(ExpressionUtil.parse("(id == $0 && version == $1) || (id == $2 && version == $3)"), expressionParameters);
 		IRequirement req = MetadataFactory.createRequirement(iuMatcher, null, 1, 1, true);
-		assertEquals("id == $0 && version == $1 || id == $2 && version == $3 expectedId1, expectedVersion1 expectedId2, expectedVersion2", req.toString().trim());
+		assertEquals("id == $0 && version == $1 || id == $2 && version == $3 (expectedId1, expectedVersion1, expectedId2, expectedVersion2)", req.toString().trim());
 	}
 }
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/expression/AllTests.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/expression/AllTests.java
index 27ebc00..8e5be56 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/expression/AllTests.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/expression/AllTests.java
@@ -4,13 +4,16 @@
  *  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:
  *     IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.equinox.p2.tests.metadata.expression;
 
-import junit.framework.*;
+import junit.framework.JUnit4TestAdapter;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
 
 /**
  * Performs all automated director tests.
@@ -20,7 +23,7 @@
 	public static Test suite() {
 		TestSuite suite = new TestSuite(AllTests.class.getName());
 		suite.addTestSuite(ExpressionTest.class);
-		suite.addTestSuite(FilterTest.class);
+		suite.addTest(new JUnit4TestAdapter(FilterTest.class));
 		return suite;
 	}
 
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/expression/FilterTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/expression/FilterTest.java
index 0d6895a..4a51833 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/expression/FilterTest.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/expression/FilterTest.java
@@ -10,19 +10,225 @@
  *******************************************************************************/
 package org.eclipse.equinox.p2.tests.metadata.expression;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
 import java.math.BigDecimal;
 import java.math.BigInteger;
-import java.util.*;
-import junit.framework.*;
-import org.eclipse.equinox.p2.metadata.expression.*;
-import org.osgi.framework.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.equinox.p2.metadata.Version;
+import org.eclipse.equinox.p2.metadata.expression.ExpressionParseException;
+import org.eclipse.equinox.p2.metadata.expression.ExpressionUtil;
+import org.eclipse.equinox.p2.metadata.expression.IFilterExpression;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Filter;
+import org.osgi.framework.ServiceReference;
 
-public class FilterTest extends TestCase {
+public class FilterTest {
+	@Test
+	public void testComparable() throws Exception {
+		IFilterExpression f1 = ExpressionUtil.parseLDAP("(comparable=42)");
+		Object comp;
+		Map<String, Object> hash = new HashMap<>();
+
+		comp = new SampleComparable("42");
+		hash.put("comparable", comp);
+		assertTrue("does not match filter", f1.match(hash));
+		assertTrue("does not match filter", f1.match(new DictionaryServiceReference(hash)));
+
+		comp = Long.valueOf(42);
+		hash.put("comparable", comp);
+		assertTrue("does not match filter", f1.match(hash));
+		assertTrue("does not match filter", f1.match(new DictionaryServiceReference(hash)));
+
+		IFilterExpression f2 = ExpressionUtil.parseLDAP("(comparable=42)");
+		hash = new Hashtable<>();
+
+		comp = new SampleComparable("42");
+		hash.put("comparable", comp);
+		assertTrue("does not match filter", f2.match(hash));
+		assertTrue("does not match filter", f2.match(new DictionaryServiceReference(hash)));
+
+		comp = Long.valueOf(42);
+		hash.put("comparable", comp);
+		assertTrue("does not match filter", f2.match(hash));
+		assertTrue("does not match filter", f2.match(new DictionaryServiceReference(hash)));
+
+		assertEquals("not equal", f1, f2);
+	}
+
+	@Test
+	public void testFilterEquality() {
+		Filter f1 = ExpressionUtil.parseLDAP("( a = bedroom  )");
+		Filter f2 = ExpressionUtil.parseLDAP(" (a= bedroom  ) ");
+		assertEquals("not equal", "(a= bedroom  )", f1.toString());
+		assertEquals("not equal", "(a= bedroom  )", f2.toString());
+		assertEquals("not equal", f1, f2);
+		assertEquals("not equal", f2, f1);
+		assertEquals("not equal", f1.hashCode(), f2.hashCode());
+
+		f1 = ExpressionUtil.parseLDAP("(status =\\28o*\\5c\\29\\2a)");
+		assertEquals("not equal", "(status=\\28o*\\5c\\29\\2a)", f1.toString());
+
+		f1 = ExpressionUtil.parseLDAP("(|(a=1)(&(a=1)(b=1)))");
+		f2 = ExpressionUtil.parseLDAP("(a=1)");
+
+		f1 = ExpressionUtil.parseLDAP("(|(&(os=macos)(ws=cocoa)(arch=x86))(&(ws=cocoa)(os=macos)(arch=ppc)))");
+		f2 = ExpressionUtil.parseLDAP("(&(os=macos)(ws=cocoa)(|(arch=x86)(arch=ppc)))");
+		assertEquals("not equal: f1:" + f1.toString() + ", f2:" + f1.toString(), f1, f2);
+
+		f1 = ExpressionUtil.parseLDAP("(&(|(x=a)(y=b)(z=a))(|(x=a)(y=b)(z=b)))");
+		f2 = ExpressionUtil.parseLDAP("(|(x=a)(y=b)(&(z=a)(z=b)))");
+		assertEquals("not equal: f1:" + f1.toString() + ", f2:" + f1.toString(), f1, f2);
+
+		f1 = ExpressionUtil.parseLDAP("(&(a=1)(|(a=1)(b=1)))");
+		f2 = ExpressionUtil.parseLDAP("(a=1)");
+
+		f1 = ExpressionUtil.parseLDAP("(|(a=1)(&(a=1)(b=1)))");
+		f2 = ExpressionUtil.parseLDAP("(a=1)");
+		assertEquals("not equal: f1:" + f1.toString() + ", f2:" + f1.toString(), f1, f2);
+	}
+
+	@Test
+	public void testFilterMatching() {
+		Dictionary<String, Object> props = new Hashtable<>();
+		props.put("room", "bedroom");
+		props.put("channel", Integer.valueOf(34));
+		props.put("status", "(on\\)*");
+		props.put("max record time", Long.valueOf(150));
+		props.put("canrecord", "true(x)");
+		props.put("shortvalue", Short.valueOf((short) 1000));
+		props.put("bytevalue", Byte.valueOf((byte) 10));
+		props.put("floatvalue", Float.valueOf(1.01f));
+		props.put("doublevalue", Double.valueOf(2.01));
+		props.put("charvalue", Character.valueOf('A'));
+		props.put("booleanvalue", Boolean.FALSE);
+		props.put("listvalue", Arrays.asList(1, 2, 3));
+		props.put("versionlistvalue", Arrays.asList(Version.create("1"), Version.create("2"), Version.create("3")));
+		props.put("weirdvalue", new Hashtable<>());
+		props.put("bigintvalue", new BigInteger("4123456"));
+		props.put("bigdecvalue", new BigDecimal("4.123456"));
+
+		assertMatch("(room=*)", props);
+		assertNoMatch("(rooom=*)", props);
+		assertMatch("(room=bedroom)", props);
+		assertMatch("(room~= B E D R O O M )", props);
+		assertNoMatch("(room=abc)", props);
+		assertMatch(" ( room >=aaaa)", props);
+		assertNoMatch("(room <=aaaa)", props);
+		assertMatch("  ( room =b*) ", props);
+		assertMatch("  ( room =*m) ", props);
+		assertMatch("(room=bed*room)", props);
+		assertMatch("  ( room =b*oo*m) ", props);
+		assertMatch("  ( room =*b*oo*m*) ", props);
+		assertNoMatch("  ( room =b*b*  *m*) ", props);
+		assertMatch("  (& (room =bedroom) (channel = 34))", props);
+		assertNoMatch("  (&  (room =b*)  (room =*x) (channel=34))", props);
+		assertMatch("(| (room =bed*)(channel=222)) ", props);
+		assertMatch("(| (room =boom*)(channel=34)) ", props);
+		assertMatch("  (! (room =ab*b*oo*m*) ) ", props);
+		assertMatch("  (status =\\(o*\\\\\\)\\*) ", props);
+		assertMatch("  (status =\\28o*\\5c\\29\\2a) ", props);
+		assertMatch("  (status =\\28o*\\5C\\29\\2A) ", props);
+		assertMatch("  (canRecord =true\\(x\\)) ", props);
+		assertMatch("(max Record Time <=150) ", props);
+		assertMatch("(shortValue >= 100) ", props);
+		assertMatch("  (  &  (  byteValue <= 100  )  (  byteValue >= 10  )  )  ", props);
+		assertMatch("(bigIntValue = 4123456) ", props);
+		assertMatch("(bigDecValue = 4.123456) ", props);
+		assertMatch("(floatValue >= 1.0) ", props);
+		assertMatch("(doubleValue <= 2.011) ", props);
+		assertMatch("(charValue ~= a) ", props);
+		assertMatch("(booleanValue = false) ", props);
+		assertMatch("(listvalue>=0)", props);
+		assertMatch("(listvalue=3)", props);
+		assertMatch("(!(listvalue>=4))", props);
+		assertMatch("(versionlistvalue>=0)", props);
+		assertMatch("(versionlistvalue=3)", props);
+		assertMatch("(!(versionlistvalue>=4))", props);
+		assertMatch("(& (| (room =d*m) (room =bed*) (room=abc)) (! (channel=999)))", props);
+		assertNoMatch("(room=bedroom)", null);
+		assertNoMatch("(weirdValue = 100) ", props);
+	}
+
+	@Test
+	public void testFilterParserErrors() {
+		assertParseError("()");
+		assertParseError("(=foo)");
+		assertParseError("(");
+		assertParseError("(abc = ))");
+		assertParseError("(& (abc = xyz) (& (345))");
+		assertParseError("  (room = b**oo!*m*) ) ");
+		assertParseError("  (room = b**oo)*m*) ) ");
+		assertParseError("  (room = *=b**oo*m*) ) ");
+		assertParseError("  (room = =b**oo*m*) ) ");
+	}
+
+	private void assertMatch(String query, Dictionary<String, Object> props) {
+		expectMatch(query, props, true);
+	}
+
+	private void assertNoMatch(String query, Dictionary<String, Object> props) {
+		expectMatch(query, props, false);
+	}
+
+	private void expectMatch(String query, Dictionary<String, Object> props, boolean match) {
+		Filter f = ExpressionUtil.parseLDAP(query);
+
+		// TODO Doing raw conversion here for simplicity; could convert to Dictionary<String, ?>
+		// but the filter impl must still handle cases where non String keys are used.
+		assertEquals(match, f.match(props));
+
+		ServiceReference ref = new DictionaryServiceReference((Map<String, ? extends Object>) props);
+		assertEquals(match, f.match(ref));
+	}
+
+	private void assertParseError(String query) {
+		try {
+			ExpressionUtil.parseLDAP(query);
+			fail("expected exception");
+		} catch (ExpressionParseException e) {
+			// Pass
+		}
+	}
+
+	private static class SampleComparable implements Comparable<SampleComparable> {
+		private int value = -1;
+	
+		public SampleComparable(String value) {
+			this.value = Integer.parseInt(value);
+		}
+	
+		@Override
+		public boolean equals(Object o) {
+			return o instanceof SampleComparable && value == ((SampleComparable) o).value;
+		}
+	
+		@Override
+		public int compareTo(SampleComparable o) {
+			return value - o.value;
+		}
+	
+		@Override
+		public String toString() {
+			return String.valueOf(value);
+		}
+	}
+
 	private static class DictionaryServiceReference implements ServiceReference {
 		private final Map<String, ? extends Object> dictionary;
-
+	
 		private final String[] keys;
-
+	
 		DictionaryServiceReference(Map<String, ? extends Object> dictionary) {
 			if (dictionary == null) {
 				this.dictionary = null;
@@ -42,17 +248,17 @@
 			}
 			this.keys = keyList.toArray(new String[keyList.size()]);
 		}
-
+	
 		@Override
 		public int compareTo(Object reference) {
 			throw new UnsupportedOperationException();
 		}
-
+	
 		@Override
 		public Bundle getBundle() {
 			return null;
 		}
-
+	
 		@Override
 		public Object getProperty(String k) {
 			for (int i = 0, length = keys.length; i < length; i++) {
@@ -63,230 +269,25 @@
 			}
 			return null;
 		}
-
+	
 		@Override
 		public String[] getPropertyKeys() {
 			return keys.clone();
 		}
-
+	
 		@Override
 		public Bundle[] getUsingBundles() {
 			throw new UnsupportedOperationException();
 		}
-
+	
 		@Override
 		public boolean isAssignableTo(Bundle bundle, String className) {
 			throw new UnsupportedOperationException();
 		}
-
+	
 		@Override
 		public Dictionary<String, Object> getProperties() {
 			return new Hashtable<>(dictionary);
 		}
 	}
-
-	private static class SampleComparable implements Comparable<SampleComparable> {
-		private int value = -1;
-
-		public SampleComparable(String value) {
-			this.value = Integer.parseInt(value);
-		}
-
-		@Override
-		public boolean equals(Object o) {
-			return o instanceof SampleComparable && value == ((SampleComparable) o).value;
-		}
-
-		@Override
-		public int compareTo(SampleComparable o) {
-			return value - o.value;
-		}
-
-		@Override
-		public String toString() {
-			return String.valueOf(value);
-		}
-	}
-
-	static final int ISTRUE = 1;
-
-	static final int ISFALSE = 2;
-
-	static final int ISILLEGAL = 3;
-
-	public static Test suite() {
-		return new TestSuite(FilterTest.class);
-	}
-
-	public void testComparable() throws Exception {
-		IFilterExpression f1 = ExpressionUtil.parseLDAP("(comparable=42)"); //$NON-NLS-1$
-		Object comp;
-		Map<String, Object> hash = new HashMap<>();
-
-		comp = new SampleComparable("42"); //$NON-NLS-1$
-		hash.put("comparable", comp); //$NON-NLS-1$
-		assertTrue("does not match filter", f1.match(hash)); //$NON-NLS-1$
-		assertTrue("does not match filter", f1.match(new DictionaryServiceReference(hash))); //$NON-NLS-1$
-
-		comp = Long.valueOf(42);
-		hash.put("comparable", comp); //$NON-NLS-1$
-		assertTrue("does not match filter", f1.match(hash)); //$NON-NLS-1$
-		assertTrue("does not match filter", f1.match(new DictionaryServiceReference(hash))); //$NON-NLS-1$
-
-		IFilterExpression f2 = ExpressionUtil.parseLDAP("(comparable=42)"); //$NON-NLS-1$
-		hash = new Hashtable<>();
-
-		comp = new SampleComparable("42"); //$NON-NLS-1$
-		hash.put("comparable", comp); //$NON-NLS-1$
-		assertTrue("does not match filter", f2.match(hash)); //$NON-NLS-1$
-		assertTrue("does not match filter", f2.match(new DictionaryServiceReference(hash))); //$NON-NLS-1$
-
-		comp = Long.valueOf(42);
-		hash.put("comparable", comp); //$NON-NLS-1$
-		assertTrue("does not match filter", f2.match(hash)); //$NON-NLS-1$
-		assertTrue("does not match filter", f2.match(new DictionaryServiceReference(hash))); //$NON-NLS-1$
-
-		assertEquals("not equal", f1, f2); //$NON-NLS-1$
-	}
-
-	public void testFilter() {
-		Dictionary<String, Object> props = new Hashtable<>();
-		props.put("room", "bedroom"); //$NON-NLS-1$ //$NON-NLS-2$
-		props.put("channel", Integer.valueOf(34)); //$NON-NLS-1$
-		props.put("status", "(on\\)*"); //$NON-NLS-1$//$NON-NLS-2$
-		props.put("max record time", Long.valueOf(150)); //$NON-NLS-1$
-		props.put("canrecord", "true(x)"); //$NON-NLS-1$ //$NON-NLS-2$
-		props.put("shortvalue", Short.valueOf((short) 1000)); //$NON-NLS-1$
-		props.put("bytevalue", Byte.valueOf((byte) 10)); //$NON-NLS-1$
-		props.put("floatvalue", Float.valueOf(1.01f)); //$NON-NLS-1$
-		props.put("doublevalue", Double.valueOf(2.01)); //$NON-NLS-1$
-		props.put("charvalue", Character.valueOf('A')); //$NON-NLS-1$
-		props.put("booleanvalue", Boolean.FALSE); //$NON-NLS-1$
-		props.put("weirdvalue", new Hashtable<>()); //$NON-NLS-1$
-		try {
-			props.put("bigintvalue", new BigInteger("4123456")); //$NON-NLS-1$ //$NON-NLS-2$
-		} catch (NoClassDefFoundError e) {
-			// ignore
-		}
-		try {
-			props.put("bigdecvalue", new BigDecimal("4.123456")); //$NON-NLS-1$  //$NON-NLS-2$
-		} catch (NoClassDefFoundError e) {
-			// ignore
-		}
-
-		testFilter("(room=*)", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(rooom=*)", props, ISFALSE); //$NON-NLS-1$
-		testFilter("(room=bedroom)", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(room~= B E D R O O M )", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(room=abc)", props, ISFALSE); //$NON-NLS-1$
-		testFilter(" ( room >=aaaa)", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(room <=aaaa)", props, ISFALSE); //$NON-NLS-1$
-		testFilter("  ( room =b*) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  ( room =*m) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(room=bed*room)", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  ( room =b*oo*m) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  ( room =*b*oo*m*) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  ( room =b*b*  *m*) ", props, ISFALSE); //$NON-NLS-1$
-		testFilter("  (& (room =bedroom) (channel = 34))", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  (&  (room =b*)  (room =*x) (channel=34))", props, ISFALSE); //$NON-NLS-1$
-		testFilter("(| (room =bed*)(channel=222)) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(| (room =boom*)(channel=34)) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  (! (room =ab*b*oo*m*) ) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  (status =\\(o*\\\\\\)\\*) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  (status =\\28o*\\5c\\29\\2a) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  (status =\\28o*\\5C\\29\\2A) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  (canRecord =true\\(x\\)) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(max Record Time <=150) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(shortValue >= 100) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("  (  &  (  byteValue <= 100  )  (  byteValue >= 10  )  )  ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(bigIntValue = 4123456) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(bigDecValue = 4.123456) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(floatValue >= 1.0) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(doubleValue <= 2.011) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(charValue ~= a) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(booleanValue = false) ", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(& (| (room =d*m) (room =bed*) (room=abc)) (! (channel=999)))", props, ISTRUE); //$NON-NLS-1$
-		testFilter("(room=bedroom)", null, ISFALSE); //$NON-NLS-1$
-
-		testFilter("()", props, ISILLEGAL); //$NON-NLS-1$
-		testFilter("(=foo)", props, ISILLEGAL); //$NON-NLS-1$
-		testFilter("(", props, ISILLEGAL); //$NON-NLS-1$
-		testFilter("(abc = ))", props, ISILLEGAL); //$NON-NLS-1$
-		testFilter("(& (abc = xyz) (& (345))", props, ISILLEGAL); //$NON-NLS-1$
-		testFilter("  (room = b**oo!*m*) ) ", props, ISILLEGAL); //$NON-NLS-1$
-		testFilter("  (room = b**oo)*m*) ) ", props, ISILLEGAL); //$NON-NLS-1$
-		testFilter("  (room = *=b**oo*m*) ) ", props, ISILLEGAL); //$NON-NLS-1$
-		testFilter("  (room = =b**oo*m*) ) ", props, ISILLEGAL); //$NON-NLS-1$
-
-		try {
-			Filter f1 = ExpressionUtil.parseLDAP("( a = bedroom  )"); //$NON-NLS-1$
-			Filter f2 = ExpressionUtil.parseLDAP(" (a= bedroom  ) "); //$NON-NLS-1$
-			assertEquals("not equal", "(a= bedroom  )", f1.toString()); //$NON-NLS-1$ //$NON-NLS-2$
-			assertEquals("not equal", "(a= bedroom  )", f2.toString()); //$NON-NLS-1$ //$NON-NLS-2$
-			assertEquals("not equal", f1, f2); //$NON-NLS-1$
-			assertEquals("not equal", f2, f1); //$NON-NLS-1$
-			assertEquals("not equal", f1.hashCode(), f2.hashCode()); //$NON-NLS-1$
-
-			f1 = ExpressionUtil.parseLDAP("(status =\\28o*\\5c\\29\\2a)");
-			assertEquals("not equal", "(status=\\28o*\\5c\\29\\2a)", f1.toString()); //$NON-NLS-1$ //$NON-NLS-2$
-
-			f1 = ExpressionUtil.parseLDAP("(|(a=1)(&(a=1)(b=1)))"); //$NON-NLS-1$
-			f2 = ExpressionUtil.parseLDAP("(a=1)"); //$NON-NLS-1$
-			System.out.println(f2.toString());
-			System.out.println(f1.toString());
-
-			f1 = ExpressionUtil.parseLDAP("(|(&(os=macos)(ws=cocoa)(arch=x86))(&(ws=cocoa)(os=macos)(arch=ppc)))"); //$NON-NLS-1$
-			f2 = ExpressionUtil.parseLDAP("(&(os=macos)(ws=cocoa)(|(arch=x86)(arch=ppc)))"); //$NON-NLS-1$
-			System.out.println(f2.toString());
-			System.out.println(f1.toString());
-			assertEquals("not equal", f1, f2); //$NON-NLS-1$
-
-			f1 = ExpressionUtil.parseLDAP("(&(|(x=a)(y=b)(z=a))(|(x=a)(y=b)(z=b)))"); //$NON-NLS-1$
-			f2 = ExpressionUtil.parseLDAP("(|(x=a)(y=b)(&(z=a)(z=b)))"); //$NON-NLS-1$
-			System.out.println(f2.toString());
-			System.out.println(f1.toString());
-			assertEquals("not equal", f1, f2); //$NON-NLS-1$
-
-			f1 = ExpressionUtil.parseLDAP("(&(a=1)(|(a=1)(b=1)))"); //$NON-NLS-1$
-			f2 = ExpressionUtil.parseLDAP("(a=1)"); //$NON-NLS-1$
-			System.out.println(f2.toString());
-			System.out.println(f1.toString());
-
-			f1 = ExpressionUtil.parseLDAP("(|(a=1)(&(a=1)(b=1)))"); //$NON-NLS-1$
-			f2 = ExpressionUtil.parseLDAP("(a=1)"); //$NON-NLS-1$
-			System.out.println(f2.toString());
-			System.out.println(f1.toString());
-			assertEquals("not equal", f1, f2); //$NON-NLS-1$
-		} catch (IllegalArgumentException e) {
-			fail("unexpected invalid syntax: " + e); //$NON-NLS-1$
-		}
-		testFilter("(weirdValue = 100) ", props, ISFALSE); //$NON-NLS-1$
-
-	}
-
-	private void testFilter(String query, Dictionary<String, Object> props, int expect) {
-		final ServiceReference ref = new DictionaryServiceReference((Map<String, ? extends Object>) props);
-		Filter f1;
-		try {
-			f1 = ExpressionUtil.parseLDAP(query);
-
-			if (expect == ISILLEGAL) {
-				fail("expected exception"); //$NON-NLS-1$
-			}
-		} catch (ExpressionParseException e) {
-			System.out.println(e.toString());
-			if (expect != ISILLEGAL) {
-				fail("exception: " + e.toString()); //$NON-NLS-1$
-			}
-			return;
-		}
-
-		// TODO Doing raw conversion here for simplicity; could convert to Dictionary<String, ?>
-		// but the filter impl must still handle cases where non String keys are used.
-		boolean val = f1.match(props);
-		assertEquals("wrong result", expect == ISTRUE, val); //$NON-NLS-1$
-
-		val = f1.match(ref);
-		assertEquals("wrong result", expect == ISTRUE, val); //$NON-NLS-1$
-	}
 }
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/repository/SPIMetadataRepositoryTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/repository/SPIMetadataRepositoryTest.java
index 022b654..6051434 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/repository/SPIMetadataRepositoryTest.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/metadata/repository/SPIMetadataRepositoryTest.java
@@ -179,14 +179,14 @@
 
 	class SPIProvidedCapability implements IProvidedCapability {
 		private final String namespace;
-		private final Map<String, Object> attributes;
+		private final Map<String, Object> properties;
 
 		public SPIProvidedCapability(String namespace, String name, Version version) {
 			this.namespace = namespace;
 
-			this.attributes = new HashMap<>();
-			attributes.put(namespace, name);
-			attributes.put(ProvidedCapability.ATTRIBUTE_VERSION, version);
+			this.properties = new HashMap<>();
+			properties.put(namespace, name);
+			properties.put(IProvidedCapability.PROPERTY_VERSION, version);
 		}
 
 		@Override
@@ -199,7 +199,7 @@
 			if (!(namespace.equals(otherCapability.getNamespace()))) {
 				return false;
 			}
-			if (!(attributes.equals(otherCapability.getAttributes()))) {
+			if (!(properties.equals(otherCapability.getProperties()))) {
 				return false;
 			}
 			return true;
@@ -207,12 +207,12 @@
 
 		@Override
 		public String toString() {
-			return namespace + "; " + attributes;
+			return namespace + "; " + properties;
 		}
 
 		@Override
 		public String getName() {
-			return (String) attributes.get(namespace);
+			return (String) properties.get(namespace);
 		}
 
 		@Override
@@ -222,12 +222,12 @@
 
 		@Override
 		public Version getVersion() {
-			return (Version) attributes.get(ProvidedCapability.ATTRIBUTE_VERSION);
+			return (Version) properties.get(IProvidedCapability.PROPERTY_VERSION);
 		}
 
 		@Override
-		public Map<String, Object> getAttributes() {
-			return Collections.unmodifiableMap(attributes);
+		public Map<String, Object> getProperties() {
+			return Collections.unmodifiableMap(properties);
 		}
 	}
 
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/planner/PropertyMatchRequirement.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/planner/PropertyMatchRequirement.java
new file mode 100644
index 0000000..4d709c9
--- /dev/null
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/planner/PropertyMatchRequirement.java
@@ -0,0 +1,111 @@
+/*******************************************************************************
+ * Copyright (c) 2008, 2017 IBM Corporation and others. 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: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.equinox.p2.tests.planner;
+
+import static org.eclipse.core.runtime.IStatus.ERROR;
+import static org.eclipse.core.runtime.IStatus.OK;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.equinox.internal.p2.director.Explanation.MissingIU;
+import org.eclipse.equinox.internal.p2.director.ProfileChangeRequest;
+import org.eclipse.equinox.internal.provisional.p2.director.PlannerStatus;
+import org.eclipse.equinox.internal.provisional.p2.director.RequestStatus;
+import org.eclipse.equinox.p2.engine.IProfile;
+import org.eclipse.equinox.p2.engine.IProvisioningPlan;
+import org.eclipse.equinox.p2.metadata.IInstallableUnit;
+import org.eclipse.equinox.p2.metadata.IProvidedCapability;
+import org.eclipse.equinox.p2.metadata.IRequirement;
+import org.eclipse.equinox.p2.metadata.MetadataFactory;
+import org.eclipse.equinox.p2.planner.IPlanner;
+import org.eclipse.equinox.p2.tests.AbstractProvisioningTest;
+
+public class PropertyMatchRequirement extends AbstractProvisioningTest {
+	private IInstallableUnit providerIu;
+	private IInstallableUnit consumerIu;
+
+	private IProfile profile;
+	private IPlanner planner;
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		// A standard OSGi service representation
+		String osgiService = "osgi.service";
+		String objectClass = "objectClass";
+		List<String> objectClassList = Arrays.asList("org.example.A", "org.example.B", "org.example.C");
+
+		// Provider.
+		// TODO Check if p2 really needs a name. IProvidedCapability.equals() can differentiate by properties only.
+		Map<String, Object> capability = new HashMap<>();
+		capability.put(osgiService, "ignored-1");
+		capability.put(objectClass, objectClassList);
+
+		IProvidedCapability[] provides = new IProvidedCapability[] {
+				MetadataFactory.createProvidedCapability(osgiService, capability)
+		};
+		providerIu = createIU("provider", DEFAULT_VERSION, provides);
+
+		// Consumer
+		String requirement = String.format("(%s=%s)", objectClass, objectClassList.get(0));
+		IRequirement[] requires = new IRequirement[] {
+				MetadataFactory.createRequirement(osgiService, requirement, null, 1, 1, true)
+		};
+		consumerIu = createIU("consumer", DEFAULT_VERSION, requires);
+
+		// Planner
+		profile = createProfile("test." + getName());
+		planner = createPlanner();
+	}
+
+	public void testMandatoryPresent() {
+		createTestMetdataRepository(new IInstallableUnit[] {providerIu, consumerIu});
+
+		ProfileChangeRequest req = new ProfileChangeRequest(profile);
+		req.add(consumerIu);
+
+		// Must pass
+		IProvisioningPlan plan = planner.getProvisioningPlan(req, null, null);
+		assertEquals(OK, plan.getStatus().getSeverity());
+
+		// And both consumer and provider must be installed
+		assertInstallOperand(plan, consumerIu);
+		assertInstallOperand(plan, providerIu);
+	}
+
+	public void testMandatoryAbsent() {
+		createTestMetdataRepository(new IInstallableUnit[] {consumerIu});
+
+		ProfileChangeRequest req = new ProfileChangeRequest(profile);
+		req.add(consumerIu);
+
+		// Must fail
+		IProvisioningPlan plan = planner.getProvisioningPlan(req, null, null);
+		assertEquals(ERROR, plan.getStatus().getSeverity());
+
+		// With a good explanation
+		RequestStatus requestStatus = ((PlannerStatus) plan.getStatus()).getRequestStatus();
+		IRequirement consumerReq = consumerIu.getRequirements().iterator().next();
+		requestStatus.getExplanations()
+				.stream()
+				.filter(e -> e instanceof MissingIU)
+				.filter(e -> (((MissingIU) e).req.toString()).equals(consumerReq.toString()))
+				.findFirst()
+				.orElseGet(() -> {
+					fail("Did not find explanation for missing requirement: " + consumerReq);
+					return null;
+				});
+
+		// And the consumer must not be installed
+		assertNoOperand(plan, consumerIu);
+	}
+}
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/publisher/actions/ActionTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/publisher/actions/ActionTest.java
index 058cca7..faf5159 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/publisher/actions/ActionTest.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/publisher/actions/ActionTest.java
@@ -27,15 +27,13 @@
 import org.eclipse.core.runtime.NullProgressMonitor;
 import org.eclipse.core.runtime.Path;
 import org.eclipse.equinox.internal.p2.core.helpers.FileUtils;
+import org.eclipse.equinox.internal.p2.metadata.InstallableUnit;
 import org.eclipse.equinox.p2.metadata.IInstallableUnit;
 import org.eclipse.equinox.p2.metadata.IProvidedCapability;
 import org.eclipse.equinox.p2.metadata.IRequirement;
 import org.eclipse.equinox.p2.metadata.MetadataFactory;
 import org.eclipse.equinox.p2.metadata.Version;
 import org.eclipse.equinox.p2.metadata.VersionRange;
-import org.eclipse.equinox.p2.metadata.expression.ExpressionUtil;
-import org.eclipse.equinox.p2.metadata.expression.IExpression;
-import org.eclipse.equinox.p2.metadata.expression.IMatchExpression;
 import org.eclipse.equinox.p2.publisher.AbstractPublisherAction;
 import org.eclipse.equinox.p2.publisher.IPublisherInfo;
 import org.eclipse.equinox.p2.publisher.IPublisherResult;
@@ -92,18 +90,25 @@
 		verifyRequirement(actual, namespace, name, range, null, 1, 1, true);
 	}
 
-	protected void verifyRequirement(Collection<IRequirement> actual, String namespace, String name, VersionRange range, String filterStr, int minCard, int maxCard, boolean greedy) {
-		IRequirement expected = MetadataFactory.createRequirement(namespace, name, range, null, minCard, maxCard, greedy);
+	protected void verifyRequirement(Collection<IRequirement> actual, String namespace, String name, VersionRange range, String envFilter, int minCard, int maxCard, boolean greedy) {
+		IRequirement expected = MetadataFactory.createRequirement(namespace, name, range, InstallableUnit.parseFilter(envFilter), minCard, maxCard, greedy);
 		verifyRequirement(actual, expected);
 	}
 
-	protected void verifyRequirement(Collection<IRequirement> actual, String matchExpr, int minCard, int maxCard, boolean greedy) {
-		IExpression expr = ExpressionUtil.parse(matchExpr);
-		IMatchExpression<IInstallableUnit> matcher = ExpressionUtil.getFactory().matchExpression(expr);
-		IRequirement expected = MetadataFactory.createRequirement(matcher, null, minCard, maxCard, greedy);
+	protected void verifyRequirement(Collection<IRequirement> actual, String namespace, String propsFilter, String envFilter, int minCard, int maxCard, boolean greedy) {
+		IRequirement expected = MetadataFactory.createRequirement(namespace, propsFilter, InstallableUnit.parseFilter(envFilter), minCard, maxCard, greedy);
 		verifyRequirement(actual, expected);
 	}
 
+	/**
+	 * Safe to use only if actual and expected were created by the same method of {@link MetadataFactory}
+	 * because match expressions are not safe to compare for equality.
+	 *
+	 * This must be guaranteed by all sub-class test cases
+	 *
+	 * @param actual
+	 * @param expected
+	 */
 	protected void verifyRequirement(Collection<IRequirement> actual, IRequirement expected) {
 		for (IRequirement act : actual) {
 			if (expected.getMatches().equals(act.getMatches())) {
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/publisher/actions/BundlesActionTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/publisher/actions/BundlesActionTest.java
index af99bfe..7e3ab03 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/publisher/actions/BundlesActionTest.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/publisher/actions/BundlesActionTest.java
@@ -77,18 +77,19 @@
 public class BundlesActionTest extends ActionTest {
 	private static final String OSGI = PublisherHelper.OSGI_BUNDLE_CLASSIFIER;
 	private static final String OSGI_IDENTITY = "osgi.identity";
+	private static final String OSGI_EE = "osgi.ee";
 	private static final String JAVA_PACKAGE = "java.package";//$NON-NLS-1$
-	private static final String JAVA_EE_1_4_REQ = "providedCapabilities.exists(pc | pc.namespace == 'osgi.ee' && pc.attributes ~= filter('(|(&(osgi.ee=JavaSE)(version=1.4))(&(osgi.ee=CDC/Foundation)(version=1.1)))'))";
-	private static final String JAVA_EE_1_6_REQ = "providedCapabilities.exists(pc | pc.namespace == 'osgi.ee' && pc.attributes ~= filter('(&(osgi.ee=JavaSE)(version=1.6))'))";
+	private static final String JAVA_EE_1_4 = "(|(&(osgi.ee=JavaSE)(version=1.4))(&(osgi.ee=CDC/Foundation)(version=1.1)))";
+	private static final String JAVA_EE_1_6 = "(&(osgi.ee=JavaSE)(version=1.6))";
 
 	private static final String TEST1_IUD_NAME = "iud";//$NON-NLS-1$
 	private static final String TEST1_PROVZ_NAME = "iuz";//$NON-NLS-1$
 	private static final String TEST1_PROVBUNDLE_NAME = "test1";//$NON-NLS-1$
-	private static final String TEST1_REQ_EE_FILTER = JAVA_EE_1_4_REQ;
+	private static final String TEST1_REQ_EE = JAVA_EE_1_4;
 	private static final String TEST2_REQ_A_NAME = "iua";//$NON-NLS-1$
 	private static final String TEST2_REQ_B_NAME = "iub";//$NON-NLS-1$
 	private static final String TEST2_REQ_C_NAME = "iuc";//$NON-NLS-1$
-	private static final String TEST2_REQ_EE_FILTER = JAVA_EE_1_4_REQ;
+	private static final String TEST2_REQ_EE = JAVA_EE_1_4;
 	private static final String TEST2_PROV_Z_NAME = "iuz";//$NON-NLS-1$
 	private static final String TEST2_PROV_Y_NAME = "iuy";//$NON-NLS-1$
 	private static final String TEST2_PROV_X_NAME = "iux";//$NON-NLS-1$
@@ -99,9 +100,9 @@
 	private static final String TEST4_REQ_PACKAGE_OPTGREEDY_NAME = "iuf";//$NON-NLS-1$
 	private static final String TEST4_REQ_BUNDLE_OPTIONAL_NAME = "iug";//$NON-NLS-1$
 	private static final String TEST4_REQ_BUNDLE_OPTGREEDY_NAME = "iuh";//$NON-NLS-1$
-	private static final String TEST5_REQ_EE_FILTER = JAVA_EE_1_4_REQ;
+	private static final String TEST5_REQ_EE = JAVA_EE_1_4;
 	private static final String TEST5_PROV_BUNDLE_NAME = "test5";//$NON-NLS-1$
-	private static final String TESTDYN_REQ_EE_FILTER = JAVA_EE_1_6_REQ;
+	private static final String TESTDYN_REQ_EE = JAVA_EE_1_6;
 
 	private static final File TEST_BASE = new File(TestActivator.getTestDataFolder(), "BundlesActionTest");//$NON-NLS-1$
 	private static final File TEST_FILE1 = new File(TEST_BASE, TEST1_PROVBUNDLE_NAME);
@@ -173,32 +174,32 @@
 
 		bundlesAction.perform(info, results, new NullProgressMonitor());
 		Collection<IInstallableUnit> ius = results.getIUs(null, null);
-		assertEquals("1.0", 1, ius.size());
+		assertEquals(1, ius.size());
 
 		info = new PublisherInfo();
 		results = new PublisherResult();
 		bundlesAction = new BundlesAction(new File[] {foo});
 		bundlesAction.perform(info, results, new NullProgressMonitor());
 		ius = results.getIUs(null, null);
-		assertEquals("2.0", 1, ius.size());
+		assertEquals(1, ius.size());
 		QueryableArray queryableArray = new QueryableArray(ius.toArray(new IInstallableUnit[ius.size()]));
 		IQueryResult<IInstallableUnit> result = queryableArray.query(QueryUtil.createIUQuery("foo"), null);
-		assertEquals("3.1", 1, queryResultSize(result));
+		assertEquals(1, queryResultSize(result));
 		IInstallableUnit iu = result.iterator().next();
 		TranslationSupport utils = new TranslationSupport();
 		utils.setTranslationSource(queryableArray);
-		assertEquals("3.2", "English Foo", utils.getIUProperty(iu, IInstallableUnit.PROP_NAME));
+		assertEquals("English Foo", utils.getIUProperty(iu, IInstallableUnit.PROP_NAME));
 
 		bundlesAction = new BundlesAction(new File[] {foo_fragment});
 		bundlesAction.perform(info, results, new NullProgressMonitor());
 		ius = results.getIUs(null, null);
-		assertEquals("2.0", 3, ius.size());
+		assertEquals(3, ius.size());
 		queryableArray = new QueryableArray(ius.toArray(new IInstallableUnit[ius.size()]));
 		result = queryableArray.query(QueryUtil.createIUQuery("foo"), null);
-		assertEquals("2.1", 1, queryResultSize(result));
+		assertEquals(1, queryResultSize(result));
 		iu = result.iterator().next();
 		utils.setTranslationSource(queryableArray);
-		assertEquals("2.2", "German Foo", utils.getIUProperty(iu, IInstallableUnit.PROP_NAME, Locale.GERMAN.toString()));
+		assertEquals("German Foo", utils.getIUProperty(iu, IInstallableUnit.PROP_NAME, Locale.GERMAN.toString()));
 	}
 
 	private void verifyBundlesAction() throws Exception {
@@ -217,7 +218,7 @@
 		IArtifactDescriptor[] descriptors = artifactRepository.getArtifactDescriptors(key2);
 
 		// Should have one canonical and one packed
-		assertTrue("1.0", descriptors.length == 2);
+		assertEquals(2, descriptors.length);
 
 		int packedIdx;
 		int canonicalIdx;
@@ -244,18 +245,18 @@
 	}
 
 	private void verifyBundle1() {
-		ArrayList<IInstallableUnit> ius = new ArrayList<>(publisherResult.getIUs(TEST1_PROVBUNDLE_NAME, IPublisherResult.ROOT));
-		assertTrue(ius.size() == 1);
+		List<IInstallableUnit> ius = new ArrayList<>(publisherResult.getIUs(TEST1_PROVBUNDLE_NAME, IPublisherResult.ROOT));
+		assertEquals(1, ius.size());
 		IInstallableUnit bundle1IU = ius.get(0);
 
-		assertNotNull("1.0", bundle1IU);
-		assertEquals("1.1", bundle1IU.getVersion(), BUNDLE1_VERSION);
+		assertNotNull(bundle1IU);
+		assertEquals(bundle1IU.getVersion(), BUNDLE1_VERSION);
 
 		// check required capabilities
-		Collection<IRequirement> requiredCapability = bundle1IU.getRequirements();
-		verifyRequirement(requiredCapability, TEST1_IU_D_NAMESPACE, TEST1_IUD_NAME, TEST1_IU_D_VERSION_RANGE);
-		verifyRequirement(requiredCapability, TEST1_REQ_EE_FILTER, 0, 1, true);
-		assertEquals("2.0", 2, requiredCapability.size());
+		Collection<IRequirement> requirements = bundle1IU.getRequirements();
+		verifyRequirement(requirements, TEST1_IU_D_NAMESPACE, TEST1_IUD_NAME, TEST1_IU_D_VERSION_RANGE);
+		verifyRequirement(requirements, OSGI_EE, TEST1_REQ_EE, null, 1, 1, true);
+		assertEquals(2, requirements.size());
 
 		// check provided capabilities
 		Collection<IProvidedCapability> providedCapabilities = bundle1IU.getProvidedCapabilities();
@@ -264,7 +265,7 @@
 		verifyProvidedCapability(providedCapabilities, OSGI, TEST1_PROVBUNDLE_NAME, BUNDLE1_VERSION);
 		verifyProvidedCapability(providedCapabilities, TEST1_PROV_Z_NAMESPACE, TEST1_PROVZ_NAME, TEST2_PROVZ_VERSION);
 		verifyProvidedCapability(providedCapabilities, PublisherHelper.NAMESPACE_ECLIPSE_TYPE, "source", Version.create("1.0.0"));//$NON-NLS-1$//$NON-NLS-2$
-		assertEquals("2.1", 5, providedCapabilities.size());
+		assertEquals(5, providedCapabilities.size());
 
 		Collection<ITouchpointData> data = bundle1IU.getTouchpointData();
 		boolean found = false;
@@ -277,27 +278,27 @@
 				found = true;
 			}
 		}
-		assertTrue("3.0", found);
+		assertTrue(found);
 	}
 
 	private void verifyBundle2() {
-		ArrayList<IInstallableUnit> ius = new ArrayList<>(publisherResult.getIUs(TEST2_PROV_BUNDLE_NAME, IPublisherResult.ROOT));
-		assertTrue(ius.size() == 1);
-		IInstallableUnit bundle2IU = ius.get(0);
+		List<IInstallableUnit> ius = new ArrayList<>(publisherResult.getIUs(TEST2_PROV_BUNDLE_NAME, IPublisherResult.ROOT));
+		assertEquals(1, ius.size());
 
-		assertNotNull(bundle2IU);
-		assertEquals(bundle2IU.getVersion(), BUNDLE2_VERSION);
+		IInstallableUnit bundleIu = ius.get(0);
+		assertNotNull(bundleIu);
+		assertEquals(bundleIu.getVersion(), BUNDLE2_VERSION);
 
 		// check required capabilities
-		Collection<IRequirement> requirements = bundle2IU.getRequirements();
+		Collection<IRequirement> requirements = bundleIu.getRequirements();
 		verifyRequirement(requirements, TEST2_IU_A_NAMESPACE, TEST2_REQ_A_NAME, TEST2_IU_A_VERSION_RANGE);
 		verifyRequirement(requirements, TEST2_IU_B_NAMESPACE, TEST2_REQ_B_NAME, TEST2_IU_B_VERSION_RANGE);
 		verifyRequirement(requirements, TEST2_IU_C_NAMESPACE, TEST2_REQ_C_NAME, TEST2_IU_C_VERSION_RANGE);
-		verifyRequirement(requirements, TEST2_REQ_EE_FILTER, 0, 1, true);
-		assertTrue(requirements.size() == 4 /*number of tested elements*/);
+		verifyRequirement(requirements, OSGI_EE, TEST2_REQ_EE, null, 1, 1, true);
+		assertEquals(4, requirements.size());
 
 		// check provided capabilities
-		Collection<IProvidedCapability> providedCapabilities = bundle2IU.getProvidedCapabilities();
+		Collection<IProvidedCapability> providedCapabilities = bundleIu.getProvidedCapabilities();
 		verifyProvidedCapability(providedCapabilities, PROVBUNDLE_NAMESPACE, TEST2_PROV_BUNDLE_NAME, PROVBUNDLE2_VERSION);
 		verifyProvidedCapability(providedCapabilities, OSGI, TEST2_PROV_BUNDLE_NAME, BUNDLE2_VERSION);
 		verifyProvidedCapability(providedCapabilities, OSGI_IDENTITY, TEST2_PROV_BUNDLE_NAME, BUNDLE2_VERSION);
@@ -308,11 +309,11 @@
 		assertEquals(7, providedCapabilities.size()); /*number of tested elements*/
 
 		// check %bundle name is correct
-		Map<String, String> prop = bundle2IU.getProperties();
+		Map<String, String> prop = bundleIu.getProperties();
 		assertTrue(prop.get("org.eclipse.equinox.p2.name").toString().equalsIgnoreCase("%bundleName"));//$NON-NLS-1$//$NON-NLS-2$
 		assertTrue(prop.get("org.eclipse.equinox.p2.provider").toString().equalsIgnoreCase("%providerName"));//$NON-NLS-1$//$NON-NLS-2$
 
-		Collection<ITouchpointData> data = bundle2IU.getTouchpointData();
+		Collection<ITouchpointData> data = bundleIu.getTouchpointData();
 		boolean found = false;
 		for (ITouchpointData td : data) {
 			ITouchpointInstruction configure = td.getInstruction("configure");
@@ -323,18 +324,17 @@
 				found = true;
 			}
 		}
-		assertFalse("3.0", found);
-
+		assertFalse(found);
 	}
 
 	private void verifyBundle3() {
 		// also a regression test for bug 393051: manifest headers use uncommon (but valid) capitalization
 		ArrayList<IInstallableUnit> ius = new ArrayList<>(publisherResult.getIUs(TEST3_PROV_BUNDLE_NAME, IPublisherResult.ROOT));
+		assertEquals(1, ius.size());
 
-		assertTrue(ius.size() == 1);
-		IInstallableUnit bundle3IU = ius.get(0);
+		IInstallableUnit bundleIu = ius.get(0);
 
-		IUpdateDescriptor updateDescriptor = bundle3IU.getUpdateDescriptor();
+		IUpdateDescriptor updateDescriptor = bundleIu.getUpdateDescriptor();
 		String name = RequiredCapability.extractName(updateDescriptor.getIUsBeingUpdated().iterator().next());
 		VersionRange range = RequiredCapability.extractRange(updateDescriptor.getIUsBeingUpdated().iterator().next());
 		String description = updateDescriptor.getDescription();
@@ -349,36 +349,31 @@
 
 	private void verifyBundle4() {
 		ArrayList<IInstallableUnit> ius = new ArrayList<>(publisherResult.getIUs(TEST4_PROV_BUNDLE_NAME, IPublisherResult.ROOT));
-		assertTrue(ius.size() == 1);
-		IInstallableUnit bundle4IU = ius.get(0);
+		assertEquals(1, ius.size());
 
-		assertNotNull("1.0", bundle4IU);
-		assertEquals("1.1", bundle4IU.getVersion(), BUNDLE4_VERSION);
+		IInstallableUnit bundleIu = ius.get(0);
+		assertNotNull(bundleIu);
+		assertEquals(bundleIu.getVersion(), BUNDLE4_VERSION);
 
 		// check required capabilities
-		Collection<IRequirement> requirements = bundle4IU.getRequirements();
+		Collection<IRequirement> requirements = bundleIu.getRequirements();
 		verifyRequirement(requirements, JAVA_PACKAGE, TEST4_REQ_PACKAGE_OPTIONAL_NAME, DEFAULT_VERSION_RANGE, null, 0, 1, false);
 		verifyRequirement(requirements, JAVA_PACKAGE, TEST4_REQ_PACKAGE_OPTGREEDY_NAME, DEFAULT_VERSION_RANGE, null, 0, 1, true);
 		verifyRequirement(requirements, OSGI, TEST4_REQ_BUNDLE_OPTIONAL_NAME, DEFAULT_VERSION_RANGE, null, 0, 1, false);
 		verifyRequirement(requirements, OSGI, TEST4_REQ_BUNDLE_OPTGREEDY_NAME, DEFAULT_VERSION_RANGE, null, 0, 1, true);
-		assertEquals("2.0", 4, requirements.size());
+		assertEquals(4, requirements.size());
 	}
 
 	private void verifyBundle5() {
 		ArrayList<IInstallableUnit> ius = new ArrayList<>(publisherResult.getIUs(TEST5_PROV_BUNDLE_NAME, IPublisherResult.ROOT));
-		assertTrue(ius.size() == 1);
+		assertEquals(1, ius.size());
+
 		IInstallableUnit bundle5IU = ius.get(0);
 
 		Collection<IRequirement> requirements = bundle5IU.getRequirements();
-		verifyRequirement(requirements, TEST5_REQ_EE_FILTER, 0, 1, true);
-		assertTrue(requirements.size() == 2);
-		IRequirement requirement = requirements.iterator().next();
-
-		int min = requirement.getMin();
-		int max = requirement.getMax();
-
-		assertTrue(min == 6);
-		assertTrue(max == 7);
+		verifyRequirement(requirements, OSGI_EE, TEST5_REQ_EE, null, 1, 1, true);
+		verifyRequirement(requirements, "bar", "foo", VersionRange.emptyRange, null, 6, 7, true);
+		assertEquals(2, requirements.size());
 	}
 
 	@Override
@@ -491,7 +486,7 @@
 		IInstallableUnit iu = BundlesAction.createBundleIU(BundlesAction.createBundleDescription(testData), null, new PublisherInfo());
 
 		Collection<IRequirement> requirements = iu.getRequirements();
-		verifyRequirement(requirements, TESTDYN_REQ_EE_FILTER, 0, 1, true);
+		verifyRequirement(requirements, OSGI_EE, TESTDYN_REQ_EE, null, 1, 1, true);
 		assertEquals(1, requirements.size());
 	}