You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gn...@apache.org on 2018/04/20 09:23:35 UTC

svn commit: r1829639 [2/3] - in /felix/trunk: bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/ utils/ utils/src/main/java/org/apache/felix/utils/collections/ u...

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/ResourceBuilder.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/ResourceBuilder.java?rev=1829639&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/ResourceBuilder.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/ResourceBuilder.java Fri Apr 20 09:23:35 2018
@@ -0,0 +1,1210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.utils.resource;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.felix.utils.collections.StringArrayMap;
+import org.apache.felix.utils.version.VersionRange;
+import org.apache.felix.utils.version.VersionTable;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+import org.osgi.framework.namespace.ExecutionEnvironmentNamespace;
+import org.osgi.framework.namespace.IdentityNamespace;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.namespace.service.ServiceNamespace;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import org.osgi.service.repository.ContentNamespace;
+
+public final class ResourceBuilder {
+
+    public static final String RESOLUTION_DYNAMIC = "dynamic";
+
+    private static final char EOF = (char) -1;
+
+    private static final int CLAUSE_START = 0;
+    private static final int PARAMETER_START = 1;
+    private static final int KEY = 2;
+    private static final int DIRECTIVE_OR_TYPEDATTRIBUTE = 4;
+    private static final int ARGUMENT = 8;
+    private static final int VALUE = 16;
+
+    private static final int CHAR = 1;
+    private static final int DELIMITER = 2;
+    private static final int STARTQUOTE = 4;
+    private static final int ENDQUOTE = 8;
+
+    private static final Map<String, String> DEFAULT_DIRECTIVES = Collections.singletonMap(ServiceNamespace.CAPABILITY_EFFECTIVE_DIRECTIVE, ServiceNamespace.EFFECTIVE_ACTIVE);
+
+    private ResourceBuilder() {
+    }
+
+    public static ResourceImpl build(String uri, Map<String, String> headerMap) throws BundleException {
+        return build(new ResourceImpl(), uri, headerMap, false);
+    }
+
+    public static ResourceImpl build(String uri, Map<String, String> headerMap, boolean removeServiceRequirements) throws BundleException {
+        return build(new ResourceImpl(), uri, headerMap, removeServiceRequirements);
+    }
+
+    public static ResourceImpl build(ResourceImpl resource, String uri, Map<String, String> headerMap) throws BundleException {
+        return build(resource, uri, headerMap, false);
+    }
+
+    public static ResourceImpl build(ResourceImpl resource, String uri, Map<String, String> headerMap, boolean removeServiceRequirements) throws BundleException {
+        try {
+            return doBuild(resource, uri, headerMap, removeServiceRequirements);
+        } catch (Exception e) {
+            throw new BundleException("Unable to build resource for " + uri + ": " + e.getMessage(), e);
+        }
+    }
+
+    private static ResourceImpl doBuild(ResourceImpl resource, String uri, Map<String, String> headerMap, boolean removeServiceRequirements) throws BundleException {
+        // Verify that only manifest version 2 is specified.
+        String manifestVersion = getManifestVersion(headerMap);
+        if (manifestVersion == null || !manifestVersion.equals("2")) {
+            throw new BundleException("Unsupported 'Bundle-ManifestVersion' value: " + manifestVersion);
+        }
+
+        //
+        // Parse bundle version.
+        //
+
+        Version bundleVersion = Version.emptyVersion;
+        if (headerMap.get(Constants.BUNDLE_VERSION) != null) {
+            bundleVersion = VersionTable.getVersion(headerMap.get(Constants.BUNDLE_VERSION));
+        }
+
+        //
+        // Parse bundle symbolic name.
+        //
+
+        String bundleSymbolicName;
+        ParsedHeaderClause bundleCap = parseBundleSymbolicName(headerMap);
+        if (bundleCap == null) {
+            throw new BundleException("Bundle manifest must include bundle symbolic name");
+        }
+        bundleSymbolicName = (String) bundleCap.attrs.get(BundleRevision.BUNDLE_NAMESPACE);
+
+        // Now that we have symbolic name and version, create the resource
+        String type = headerMap.get(Constants.FRAGMENT_HOST) == null ? IdentityNamespace.TYPE_BUNDLE : IdentityNamespace.TYPE_FRAGMENT;
+        {
+            Map<String, Object> attrs = new StringArrayMap<>(3);
+            attrs.put(IdentityNamespace.IDENTITY_NAMESPACE, bundleSymbolicName);
+            attrs.put(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, type);
+            attrs.put(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, bundleVersion);
+            CapabilityImpl identity = new CapabilityImpl(resource, IdentityNamespace.IDENTITY_NAMESPACE, Collections.<String, String>emptyMap(), attrs);
+            resource.addCapability(identity);
+        }
+        if (uri != null) {
+            resource.addCapability(new CapabilityImpl(resource, ContentNamespace.CONTENT_NAMESPACE,
+                    Collections.<String, String>emptyMap(),
+                    Collections.<String, Object>singletonMap(ContentNamespace.CAPABILITY_URL_ATTRIBUTE, uri)));
+        }
+
+        // Add a bundle and host capability to all
+        // non-fragment bundles. A host capability is the same
+        // as a require capability, but with a different capability
+        // namespace. Bundle capabilities resolve required-bundle
+        // dependencies, while host capabilities resolve fragment-host
+        // dependencies.
+        if (headerMap.get(Constants.FRAGMENT_HOST) == null) {
+            // All non-fragment bundles have bundle capability.
+            resource.addCapability(new CapabilityImpl(resource, BundleRevision.BUNDLE_NAMESPACE, bundleCap.dirs, bundleCap.attrs));
+            // A non-fragment bundle can choose to not have a host capability.
+            String attachment = bundleCap.dirs.get(Constants.FRAGMENT_ATTACHMENT_DIRECTIVE);
+            attachment = (attachment == null) ? Constants.FRAGMENT_ATTACHMENT_RESOLVETIME : attachment;
+            if (!attachment.equalsIgnoreCase(Constants.FRAGMENT_ATTACHMENT_NEVER)) {
+                Map<String, Object> hostAttrs = new StringArrayMap<>(bundleCap.attrs.size());
+                for (Map.Entry<String, Object> e : bundleCap.attrs.entrySet()) {
+                    String k = e.getKey();
+                    if (BundleRevision.BUNDLE_NAMESPACE.equals(k)) {
+                        k = BundleRevision.HOST_NAMESPACE;
+                    }
+                    hostAttrs.put(k, e.getValue());
+                }
+                resource.addCapability(new CapabilityImpl(
+                        resource, BundleRevision.HOST_NAMESPACE,
+                        bundleCap.dirs,
+                        hostAttrs));
+            }
+        }
+
+        //
+        // Parse Fragment-Host.
+        //
+
+        List<RequirementImpl> hostReqs = parseFragmentHost(resource, headerMap);
+
+        //
+        // Parse Require-Bundle
+        //
+
+        List<ParsedHeaderClause> rbClauses = parseStandardHeader(headerMap.get(Constants.REQUIRE_BUNDLE));
+        rbClauses = normalizeRequireClauses(rbClauses);
+        List<Requirement> rbReqs = convertRequires(rbClauses, resource);
+
+        //
+        // Parse Import-Package.
+        //
+
+        List<ParsedHeaderClause> importClauses = parseStandardHeader(headerMap.get(Constants.IMPORT_PACKAGE));
+        importClauses = normalizeImportClauses(importClauses);
+        List<Requirement> importReqs = convertImports(importClauses, resource);
+
+        //
+        // Parse DynamicImport-Package.
+        //
+
+        List<ParsedHeaderClause> dynamicClauses = parseStandardHeader(headerMap.get(Constants.DYNAMICIMPORT_PACKAGE));
+        dynamicClauses = normalizeDynamicImportClauses(dynamicClauses);
+        List<Requirement> dynamicReqs = convertImports(dynamicClauses, resource);
+
+        //
+        // Parse Require-Capability.
+        //
+
+        List<ParsedHeaderClause> requireClauses = parseStandardHeader(headerMap.get(Constants.REQUIRE_CAPABILITY));
+        requireClauses = normalizeRequireCapabilityClauses(requireClauses);
+        List<Requirement> requireReqs = convertRequireCapabilities(requireClauses, resource);
+
+        //
+        // Parse Bundle-RequiredExecutionEnvironment.
+        //
+        List<Requirement> breeReqs =
+                parseBreeHeader(headerMap.get(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT), resource);
+
+        //
+        // Parse Export-Package.
+        //
+
+        List<ParsedHeaderClause> exportClauses = parseStandardHeader(headerMap.get(Constants.EXPORT_PACKAGE));
+        exportClauses = normalizeExportClauses(exportClauses, bundleSymbolicName, bundleVersion);
+        List<Capability> exportCaps = convertExports(exportClauses, resource);
+
+        //
+        // Parse Provide-Capability.
+        //
+
+        List<ParsedHeaderClause> provideClauses = parseStandardHeader(headerMap.get(Constants.PROVIDE_CAPABILITY));
+        provideClauses = normalizeProvideCapabilityClauses(provideClauses);
+        List<Capability> provideCaps = convertProvideCapabilities(provideClauses, resource);
+
+        //
+        // Parse Import-Service and Export-Service
+        // if Require-Capability and Provide-Capability are not set for services
+        //
+
+        boolean hasServiceReferenceCapability = false;
+        for (Capability cap : exportCaps) {
+            hasServiceReferenceCapability |= ServiceNamespace.SERVICE_NAMESPACE.equals(cap.getNamespace());
+        }
+        if (!hasServiceReferenceCapability) {
+            List<ParsedHeaderClause> exportServices = parseStandardHeader(headerMap.get(Constants.EXPORT_SERVICE));
+            List<Capability> caps = convertExportService(exportServices, resource);
+            provideCaps.addAll(caps);
+        }
+
+        boolean hasServiceReferenceRequirement = false;
+        for (Requirement req : requireReqs) {
+            hasServiceReferenceRequirement |= ServiceNamespace.SERVICE_NAMESPACE.equals(req.getNamespace());
+        }
+        if (!hasServiceReferenceRequirement) {
+            List<ParsedHeaderClause> importServices = parseStandardHeader(headerMap.get(Constants.IMPORT_SERVICE));
+            List<Requirement> reqs = convertImportService(importServices, resource);
+            if (!reqs.isEmpty()) {
+                requireReqs.addAll(reqs);
+                hasServiceReferenceRequirement = true;
+            }
+        }
+
+        if (hasServiceReferenceRequirement && removeServiceRequirements) {
+            for (Iterator<Requirement> iterator = requireReqs.iterator(); iterator.hasNext();) {
+                Requirement req = iterator.next();
+                if (ServiceNamespace.SERVICE_NAMESPACE.equals(req.getNamespace())) {
+                    iterator.remove();
+                }
+            }
+        }
+
+        // Combine all capabilities.
+        resource.addCapabilities(exportCaps);
+        resource.addCapabilities(provideCaps);
+
+        // Combine all requirements.
+        resource.addRequirements(hostReqs);
+        resource.addRequirements(importReqs);
+        resource.addRequirements(rbReqs);
+        resource.addRequirements(requireReqs);
+        resource.addRequirements(dynamicReqs);
+
+        return resource;
+    }
+
+    public static List<Requirement> parseRequirement(Resource resource, String requirement) throws BundleException {
+        List<ParsedHeaderClause> requireClauses = parseStandardHeader(requirement);
+        requireClauses = normalizeRequireCapabilityClauses(requireClauses);
+        return convertRequireCapabilities(requireClauses, resource);
+    }
+
+    public static List<Capability> parseCapability(Resource resource, String capability) throws BundleException {
+        List<ParsedHeaderClause> provideClauses = parseStandardHeader(capability);
+        provideClauses = normalizeProvideCapabilityClauses(provideClauses);
+        return convertProvideCapabilities(provideClauses, resource);
+    }
+
+    @SuppressWarnings("deprecation")
+    private static List<ParsedHeaderClause> normalizeImportClauses(List<ParsedHeaderClause> clauses) throws BundleException {
+        // Verify that the values are equals if the package specifies
+        // both version and specification-version attributes.
+        Set<String> dupeSet = new HashSet<>();
+        for (ParsedHeaderClause clause : clauses) {
+            // Check for "version" and "specification-version" attributes
+            // and verify they are the same if both are specified.
+            Object v = clause.attrs.get(Constants.VERSION_ATTRIBUTE);
+            Object sv = clause.attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION);
+            if ((v != null) && (sv != null)) {
+                // Verify they are equal.
+                if (!((String) v).trim().equals(((String) sv).trim())) {
+                    throw new IllegalArgumentException(
+                            "Both version and specification-version are specified, but they are not equal.");
+                }
+            }
+
+            // Ensure that only the "version" attribute is used and convert
+            // it to the VersionRange type.
+            if ((v != null) || (sv != null)) {
+                clause.attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION);
+                v = (v == null) ? sv : v;
+                clause.attrs.put(Constants.VERSION_ATTRIBUTE, VersionRange.parseVersionRange(v.toString()));
+            }
+
+            // If bundle version is specified, then convert its type to VersionRange.
+            v = clause.attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE);
+            if (v != null) {
+                clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, VersionRange.parseVersionRange(v.toString()));
+            }
+
+            // Verify java.* is not imported, nor any duplicate imports.
+            for (String pkgName : clause.paths) {
+                if (!dupeSet.contains(pkgName)) {
+                    // Verify that java.* packages are not imported.
+                    if (pkgName.startsWith("java.")) {
+                        throw new BundleException("Importing java.* packages not allowed: " + pkgName);
+                    // The character "." has no meaning in the OSGi spec except
+                    // when placed on the bundle class path. Some people, however,
+                    // mistakenly think it means the default package when imported
+                    // or exported. This is not correct. It is invalid.
+                    } else if (pkgName.equals(".")) {
+                        throw new BundleException("Importing '.' is invalid.");
+                    // Make sure a package name was specified.
+                    } else if (pkgName.length() == 0) {
+                        throw new BundleException(
+                                "Imported package names cannot be zero length.");
+                    }
+                    dupeSet.add(pkgName);
+                } else {
+                    throw new BundleException("Duplicate import: " + pkgName);
+                }
+            }
+        }
+
+        return clauses;
+    }
+
+    private static List<Capability> convertExportService(List<ParsedHeaderClause> clauses, Resource resource) {
+        List<Capability> capList = new ArrayList<>();
+        for (ParsedHeaderClause clause : clauses) {
+            for (String path : clause.paths) {
+                Map<String, Object> attrs = new LinkedHashMap<>();
+                attrs.put(Constants.OBJECTCLASS, path);
+                attrs.putAll(clause.attrs);
+                capList.add(new CapabilityImpl(
+                        resource,
+                        ServiceNamespace.SERVICE_NAMESPACE,
+                        DEFAULT_DIRECTIVES,
+                        attrs));
+            }
+        }
+        return capList;
+    }
+
+    private static List<Requirement> convertImportService(List<ParsedHeaderClause> clauses, Resource resource) throws BundleException {
+        try {
+            List<Requirement> reqList = new ArrayList<>();
+            for (ParsedHeaderClause clause : clauses) {
+                for (String path : clause.paths) {
+                    String multiple = clause.dirs.get("multiple");
+                    String avail = clause.dirs.get("availability");
+                    String filter = (String) clause.attrs.get("filter");
+                    Map<String, String> dirs = new LinkedHashMap<>(2);
+                    dirs.put(ServiceNamespace.REQUIREMENT_EFFECTIVE_DIRECTIVE, ServiceNamespace.EFFECTIVE_ACTIVE);
+                    if ("optional".equals(avail)) {
+                        dirs.put(ServiceNamespace.REQUIREMENT_RESOLUTION_DIRECTIVE, ServiceNamespace.RESOLUTION_OPTIONAL);
+                    }
+                    if ("true".equals(multiple)) {
+                        dirs.put(ServiceNamespace.REQUIREMENT_CARDINALITY_DIRECTIVE, ServiceNamespace.CARDINALITY_MULTIPLE);
+                    }
+                    if (filter == null) {
+                        filter = "(" + Constants.OBJECTCLASS + "=" + path + ")";
+                    } else if (!filter.startsWith("(") && !filter.endsWith(")")) {
+                        filter = "(&(" + Constants.OBJECTCLASS + "=" + path + ")(" + filter + "))";
+                    } else {
+                        filter = "(&(" + Constants.OBJECTCLASS + "=" + path + ")" + filter + ")";
+                    }
+                    dirs.put(ServiceNamespace.REQUIREMENT_FILTER_DIRECTIVE, filter);
+                    reqList.add(new RequirementImpl(
+                            resource,
+                            ServiceNamespace.SERVICE_NAMESPACE,
+                            dirs,
+                            null,
+                            SimpleFilter.parse(filter)));
+                }
+            }
+            return reqList;
+        } catch (Exception ex) {
+            throw new BundleException("Error creating requirement: " + ex, ex);
+        }
+    }
+
+    private static List<Requirement> convertImports(List<ParsedHeaderClause> clauses, Resource resource) {
+        // Now convert generic header clauses into requirements.
+        List<Requirement> reqList = new ArrayList<>();
+        for (ParsedHeaderClause clause : clauses) {
+            for (String path : clause.paths) {
+                // Prepend the package name to the array of attributes.
+                Map<String, Object> attrs = clause.attrs;
+                // Note that we use a linked hash map here to ensure the
+                // package attribute is first, which will make indexing
+                // more efficient.
+                // TODO: OSGi R4.3 - This is ordering is kind of hacky.
+                // Prepend the package name to the array of attributes.
+                Map<String, Object> newAttrs = new LinkedHashMap<>(attrs.size() + 1);
+                // We want this first from an indexing perspective.
+                newAttrs.put(BundleRevision.PACKAGE_NAMESPACE, path);
+                newAttrs.putAll(attrs);
+                // But we need to put it again to make sure it wasn't overwritten.
+                newAttrs.put(BundleRevision.PACKAGE_NAMESPACE, path);
+
+                // Create filter now so we can inject filter directive.
+                SimpleFilter sf = SimpleFilter.convert(newAttrs);
+
+                // Inject filter directive.
+                // TODO: OSGi R4.3 - Can we insert this on demand somehow?
+                Map<String, String> dirs = clause.dirs;
+                Map<String, String> newDirs = new StringArrayMap<>(dirs.size() + 1);
+                newDirs.putAll(dirs);
+                newDirs.put(Constants.FILTER_DIRECTIVE, sf.toString());
+
+                // Create package requirement and add to requirement list.
+                reqList.add(
+                        new RequirementImpl(
+                                resource,
+                                BundleRevision.PACKAGE_NAMESPACE,
+                                newDirs,
+                                null,
+                                sf)
+                );
+            }
+        }
+
+        return reqList;
+    }
+
+    @SuppressWarnings("deprecation")
+    private static List<ParsedHeaderClause> normalizeDynamicImportClauses(List<ParsedHeaderClause> clauses) throws BundleException {
+        // Verify that the values are equals if the package specifies
+        // both version and specification-version attributes.
+        for (ParsedHeaderClause clause : clauses) {
+            // Add the resolution directive to indicate that these are
+            // dynamic imports.
+            clause.dirs.put(Constants.RESOLUTION_DIRECTIVE, RESOLUTION_DYNAMIC);
+
+            // Check for "version" and "specification-version" attributes
+            // and verify they are the same if both are specified.
+            Object v = clause.attrs.get(Constants.VERSION_ATTRIBUTE);
+            Object sv = clause.attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION);
+            if ((v != null) && (sv != null)) {
+                // Verify they are equal.
+                if (!((String) v).trim().equals(((String) sv).trim())) {
+                    throw new IllegalArgumentException(
+                            "Both version and specification-version are specified, but they are not equal.");
+                }
+            }
+
+            // Ensure that only the "version" attribute is used and convert
+            // it to the VersionRange type.
+            if ((v != null) || (sv != null)) {
+                clause.attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION);
+                v = (v == null) ? sv : v;
+                clause.attrs.put(Constants.VERSION_ATTRIBUTE, VersionRange.parseVersionRange(v.toString()));
+            }
+
+            // If bundle version is specified, then convert its type to VersionRange.
+            v = clause.attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE);
+            if (v != null) {
+                clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, VersionRange.parseVersionRange(v.toString()));
+            }
+
+            // Dynamic imports can have duplicates, so verify that java.*
+            // packages are not imported.
+            for (String pkgName : clause.paths) {
+                if (pkgName.startsWith("java.")) {
+                    throw new BundleException("Dynamically importing java.* packages not allowed: " + pkgName);
+                } else if (!pkgName.equals("*") && pkgName.endsWith("*") && !pkgName.endsWith(".*")) {
+                    throw new BundleException("Partial package name wild carding is not allowed: " + pkgName);
+                }
+            }
+        }
+
+        return clauses;
+    }
+
+    private static List<ParsedHeaderClause> normalizeRequireCapabilityClauses(
+            List<ParsedHeaderClause> clauses) throws BundleException {
+
+        // Convert attributes into specified types.
+        for (ParsedHeaderClause clause : clauses) {
+            for (Map.Entry<String, Object> entry : clause.attrs.entrySet()) {
+                if (entry.getKey().equals("version")) {
+                    clause.attrs.put(entry.getKey(), new VersionRange(entry.getValue().toString()));
+                }
+            }
+            for (Map.Entry<String, String> entry : clause.types.entrySet()) {
+                String type = entry.getValue();
+                if (!type.equals("String")) {
+                    if (type.equals("Double")) {
+                        clause.attrs.put(
+                                entry.getKey(),
+                                new Double(clause.attrs.get(entry.getKey()).toString().trim()));
+                    } else if (type.equals("Version")) {
+                        clause.attrs.put(
+                                entry.getKey(),
+                                new Version(clause.attrs.get(entry.getKey()).toString().trim()));
+                    } else if (type.equals("Long")) {
+                        clause.attrs.put(
+                                entry.getKey(),
+                                new Long(clause.attrs.get(entry.getKey()).toString().trim()));
+                    } else if (type.startsWith("List")) {
+                        int startIdx = type.indexOf('<');
+                        int endIdx = type.indexOf('>');
+                        if (((startIdx > 0) && (endIdx <= startIdx))
+                                || ((startIdx < 0) && (endIdx > 0))) {
+                            throw new BundleException(
+                                    "Invalid Provide-Capability attribute list type for '"
+                                            + entry.getKey()
+                                            + "' : "
+                                            + type
+                            );
+                        }
+
+                        String listType = "String";
+                        if (endIdx > startIdx) {
+                            listType = type.substring(startIdx + 1, endIdx).trim();
+                        }
+
+                        List<String> tokens = parseDelimitedString(
+                                clause.attrs.get(entry.getKey()).toString(), ",", false);
+                        List<Object> values = new ArrayList<>(tokens.size());
+                        for (String token : tokens) {
+                            switch (listType) {
+                            case "String":
+                                values.add(token);
+                                break;
+                            case "Double":
+                                values.add(new Double(token.trim()));
+                                break;
+                            case "Version":
+                                values.add(new Version(token.trim()));
+                                break;
+                            case "Long":
+                                values.add(new Long(token.trim()));
+                                break;
+                            default:
+                                throw new BundleException(
+                                        "Unknown Provide-Capability attribute list type for '"
+                                                + entry.getKey()
+                                                + "' : "
+                                                + type
+                                );
+                            }
+                        }
+                        clause.attrs.put(
+                                entry.getKey(),
+                                values);
+                    } else {
+                        throw new BundleException(
+                                "Unknown Provide-Capability attribute type for '"
+                                        + entry.getKey()
+                                        + "' : "
+                                        + type
+                        );
+                    }
+                }
+            }
+        }
+
+        return clauses;
+    }
+
+    private static List<ParsedHeaderClause> normalizeProvideCapabilityClauses(
+            List<ParsedHeaderClause> clauses) throws BundleException {
+
+        // Convert attributes into specified types.
+        for (ParsedHeaderClause clause : clauses) {
+            for (Map.Entry<String, String> entry : clause.types.entrySet()) {
+                String type = entry.getValue();
+                if (!type.equals("String")) {
+                    if (type.equals("Double")) {
+                        clause.attrs.put(
+                                entry.getKey(),
+                                new Double(clause.attrs.get(entry.getKey()).toString().trim()));
+                    } else if (type.equals("Version")) {
+                        clause.attrs.put(
+                                entry.getKey(),
+                                new Version(clause.attrs.get(entry.getKey()).toString().trim()));
+                    } else if (type.equals("Long")) {
+                        clause.attrs.put(
+                                entry.getKey(),
+                                new Long(clause.attrs.get(entry.getKey()).toString().trim()));
+                    } else if (type.startsWith("List")) {
+                        int startIdx = type.indexOf('<');
+                        int endIdx = type.indexOf('>');
+                        if (((startIdx > 0) && (endIdx <= startIdx))
+                                || ((startIdx < 0) && (endIdx > 0))) {
+                            throw new BundleException(
+                                    "Invalid Provide-Capability attribute list type for '"
+                                            + entry.getKey()
+                                            + "' : "
+                                            + type
+                            );
+                        }
+
+                        String listType = "String";
+                        if (endIdx > startIdx) {
+                            listType = type.substring(startIdx + 1, endIdx).trim();
+                        }
+
+                        List<String> tokens = parseDelimitedString(
+                                clause.attrs.get(entry.getKey()).toString(), ",", false);
+                        List<Object> values = new ArrayList<>(tokens.size());
+                        for (String token : tokens) {
+                            switch (listType) {
+                            case "String":
+                                values.add(token);
+                                break;
+                            case "Double":
+                                values.add(new Double(token.trim()));
+                                break;
+                            case "Version":
+                                values.add(new Version(token.trim()));
+                                break;
+                            case "Long":
+                                values.add(new Long(token.trim()));
+                                break;
+                            default:
+                                throw new BundleException(
+                                        "Unknown Provide-Capability attribute list type for '"
+                                                + entry.getKey()
+                                                + "' : "
+                                                + type
+                                );
+                            }
+                        }
+                        clause.attrs.put(
+                                entry.getKey(),
+                                values);
+                    } else {
+                        throw new BundleException(
+                                "Unknown Provide-Capability attribute type for '"
+                                        + entry.getKey()
+                                        + "' : "
+                                        + type
+                        );
+                    }
+                }
+            }
+        }
+
+        return clauses;
+    }
+
+    private static List<Requirement> convertRequireCapabilities(
+            List<ParsedHeaderClause> clauses, Resource resource) throws BundleException {
+
+        // Now convert generic header clauses into requirements.
+        List<Requirement> reqList = new ArrayList<>();
+        for (ParsedHeaderClause clause : clauses) {
+            try {
+                String filterStr = clause.dirs.get(Constants.FILTER_DIRECTIVE);
+                SimpleFilter sf = (filterStr != null)
+                        ? SimpleFilter.parse(filterStr)
+                        : SimpleFilter.convert(clause.attrs);
+                for (String path : clause.paths) {
+                    // Create requirement and add to requirement list.
+                    reqList.add(new RequirementImpl(
+                            resource, path, clause.dirs, clause.attrs, sf));
+                }
+            } catch (Exception ex) {
+                throw new BundleException("Error creating requirement: " + ex, ex);
+            }
+        }
+
+        return reqList;
+    }
+
+    private static List<Capability> convertProvideCapabilities(
+            List<ParsedHeaderClause> clauses, Resource resource) throws BundleException {
+
+        List<Capability> capList = new ArrayList<>();
+        for (ParsedHeaderClause clause : clauses) {
+            for (String path : clause.paths) {
+                if (path.startsWith("osgi.wiring.")) {
+//                    throw new BundleException("Manifest cannot use Provide-Capability for '" + path + "' namespace.");
+                }
+
+                // Create package capability and add to capability list.
+                capList.add(new CapabilityImpl(resource, path, clause.dirs, clause.attrs));
+            }
+        }
+
+        return capList;
+    }
+
+    @SuppressWarnings("deprecation")
+    private static List<ParsedHeaderClause> normalizeExportClauses(
+            List<ParsedHeaderClause> clauses,
+            String bsn, Version bv) throws BundleException {
+
+        // Verify that "java.*" packages are not exported.
+        for (ParsedHeaderClause clause : clauses) {
+            // Verify that the named package has not already been declared.
+            for (String pkgName : clause.paths) {
+                // Verify that java.* packages are not exported.
+                if (pkgName.startsWith("java.")) {
+                    throw new BundleException("Exporting java.* packages not allowed: " + pkgName);
+                // The character "." has no meaning in the OSGi spec except
+                // when placed on the bundle class path. Some people, however,
+                // mistakenly think it means the default package when imported
+                // or exported. This is not correct. It is invalid.
+                } else if (pkgName.equals(".")) {
+                    throw new BundleException("Exporing '.' is invalid.");
+                // Make sure a package name was specified.
+                } else if (pkgName.length() == 0) {
+                    throw new BundleException("Exported package names cannot be zero length.");
+                }
+            }
+
+            // Check for "version" and "specification-version" attributes
+            // and verify they are the same if both are specified.
+            Object v = clause.attrs.get(Constants.VERSION_ATTRIBUTE);
+            Object sv = clause.attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION);
+            if ((v != null) && (sv != null)) {
+                // Verify they are equal.
+                if (!((String) v).trim().equals(((String) sv).trim())) {
+                    throw new IllegalArgumentException("Both version and specification-version are specified, but they are not equal.");
+                }
+            }
+
+            // Always add the default version if not specified.
+            if ((v == null) && (sv == null)) {
+                v = Version.emptyVersion;
+            }
+
+            // Ensure that only the "version" attribute is used and convert
+            // it to the appropriate type.
+            if ((v != null) || (sv != null)) {
+                // Convert version attribute to type Version.
+                clause.attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION);
+                v = (v == null) ? sv : v;
+                clause.attrs.put(Constants.VERSION_ATTRIBUTE, VersionTable.getVersion(v.toString()));
+            }
+
+            // Find symbolic name and version attribute, if present.
+            if (clause.attrs.containsKey(Constants.BUNDLE_VERSION_ATTRIBUTE)
+                    || clause.attrs.containsKey(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE)) {
+                throw new BundleException("Exports must not specify bundle symbolic name or bundle version.");
+            }
+
+            // Now that we know that there are no bundle symbolic name and version
+            // attributes, add them since the spec says they are there implicitly.
+            clause.attrs.put(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE, bsn);
+            clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, bv);
+        }
+
+        return clauses;
+    }
+
+    private static List<Capability> convertExports(List<ParsedHeaderClause> clauses, Resource resource) {
+        List<Capability> capList = new ArrayList<>();
+        for (ParsedHeaderClause clause : clauses) {
+            for (String pkgName : clause.paths) {
+                // Prepend the package name to the array of attributes.
+                Map<String, Object> attrs = clause.attrs;
+                Map<String, Object> newAttrs = new StringArrayMap<>(attrs.size() + 1);
+                newAttrs.putAll(attrs);
+                newAttrs.put(BundleRevision.PACKAGE_NAMESPACE, pkgName);
+
+                // Create package capability and add to capability list.
+                capList.add(new CapabilityImpl(resource, BundleRevision.PACKAGE_NAMESPACE, clause.dirs, newAttrs));
+            }
+        }
+
+        return capList;
+    }
+
+    private static String getManifestVersion(Map<String, String> headerMap) {
+        String manifestVersion = headerMap.get(Constants.BUNDLE_MANIFESTVERSION);
+        return (manifestVersion == null) ? "1" : manifestVersion.trim();
+    }
+
+    private static ParsedHeaderClause parseBundleSymbolicName(Map<String, String> headerMap) throws BundleException {
+        List<ParsedHeaderClause> clauses = parseStandardHeader(headerMap.get(Constants.BUNDLE_SYMBOLICNAME));
+        if (clauses.size() > 0) {
+            if (clauses.size() > 1 || clauses.get(0).paths.size() > 1) {
+                throw new BundleException("Cannot have multiple symbolic names: " + headerMap.get(Constants.BUNDLE_SYMBOLICNAME));
+            }
+
+            // Get bundle version.
+            Version bundleVersion = Version.emptyVersion;
+            if (headerMap.get(Constants.BUNDLE_VERSION) != null) {
+                bundleVersion = VersionTable.getVersion(headerMap.get(Constants.BUNDLE_VERSION));
+            }
+
+            // Create a require capability and return it.
+            ParsedHeaderClause clause = clauses.get(0);
+            String symName = clause.paths.get(0);
+            clause.attrs.put(BundleRevision.BUNDLE_NAMESPACE, symName);
+            clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, bundleVersion);
+            return clause;
+        }
+
+        return null;
+    }
+
+    private static List<RequirementImpl> parseFragmentHost(Resource resource, Map<String, String> headerMap) throws BundleException {
+        List<RequirementImpl> reqs = new ArrayList<>();
+        List<ParsedHeaderClause> clauses = parseStandardHeader(headerMap.get(Constants.FRAGMENT_HOST));
+        if (clauses.size() > 0) {
+            // Make sure that only one fragment host symbolic name is specified.
+            if (clauses.size() > 1 || clauses.get(0).paths.size() > 1) {
+                throw new BundleException("Fragments cannot have multiple hosts: " + headerMap.get(Constants.FRAGMENT_HOST));
+            }
+
+            // If the bundle-version attribute is specified, then convert
+            // it to the proper type.
+            Object value = clauses.get(0).attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE);
+            value = (value == null) ? "0.0.0" : value;
+            clauses.get(0).attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, VersionRange.parseVersionRange(value.toString()));
+
+            // Note that we use a linked hash map here to ensure the
+            // host symbolic name is first, which will make indexing
+            // more efficient.
+            // TODO: OSGi R4.3 - This is ordering is kind of hacky.
+            // Prepend the host symbolic name to the map of attributes.
+            Map<String, Object> attrs = clauses.get(0).attrs;
+            Map<String, Object> newAttrs = new LinkedHashMap<>(attrs.size() + 1);
+            // We want this first from an indexing perspective.
+            newAttrs.put(BundleRevision.HOST_NAMESPACE, clauses.get(0).paths.get(0));
+            newAttrs.putAll(attrs);
+            // But we need to put it again to make sure it wasn't overwritten.
+            newAttrs.put(BundleRevision.HOST_NAMESPACE, clauses.get(0).paths.get(0));
+
+            // Create filter now so we can inject filter directive.
+            SimpleFilter sf = SimpleFilter.convert(newAttrs);
+
+            // Inject filter directive.
+            // TODO: OSGi R4.3 - Can we insert this on demand somehow?
+            Map<String, String> dirs = clauses.get(0).dirs;
+            Map<String, String> newDirs = new StringArrayMap<>(dirs.size() + 1);
+            newDirs.putAll(dirs);
+            newDirs.put(Constants.FILTER_DIRECTIVE, sf.toString());
+
+            reqs.add(new RequirementImpl(
+                    resource, BundleRevision.HOST_NAMESPACE,
+                    newDirs,
+                    newAttrs));
+        }
+
+        return reqs;
+    }
+
+    private static List<Requirement> parseBreeHeader(String header, Resource resource) {
+        List<String> filters = new ArrayList<>();
+        for (String entry : parseDelimitedString(header, ",")) {
+            List<String> names = parseDelimitedString(entry, "/");
+            List<String> left = parseDelimitedString(names.get(0), "-");
+
+            String lName = left.get(0);
+            Version lVer;
+            try {
+                lVer = Version.parseVersion(left.get(1));
+            } catch (Exception ex) {
+                // Version doesn't parse. Make it part of the name.
+                lName = names.get(0);
+                lVer = null;
+            }
+
+            String rName = null;
+            Version rVer = null;
+            if (names.size() > 1) {
+                List<String> right = parseDelimitedString(names.get(1), "-");
+                rName = right.get(0);
+                try {
+                    rVer = Version.parseVersion(right.get(1));
+                } catch (Exception ex) {
+                    rName = names.get(1);
+                    rVer = null;
+                }
+            }
+
+            String versionClause;
+            if (lVer != null) {
+                if ((rVer != null) && (!rVer.equals(lVer))) {
+                    // Both versions are defined, but different. Make each of them part of the name
+                    lName = names.get(0);
+                    rName = names.get(1);
+                    versionClause = null;
+                } else {
+                    versionClause = getBreeVersionClause(lVer);
+                }
+            } else {
+                versionClause = getBreeVersionClause(rVer);
+            }
+
+            if ("J2SE".equals(lName)) {
+                // J2SE is not used in the Capability variant of BREE, use JavaSE here
+                // This can only happen with the lName part...
+                lName = "JavaSE";
+            }
+
+            String nameClause;
+            if (rName != null) {
+                nameClause = "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE + "=" + lName + "/" + rName + ")";
+            } else {
+                nameClause = "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE + "=" + lName + ")";
+            }
+
+            String filter;
+            if (versionClause != null) {
+                filter = "(&" + nameClause + versionClause + ")";
+            } else {
+                filter = nameClause;
+            }
+
+            filters.add(filter);
+        }
+
+        if (filters.size() == 0) {
+            return Collections.emptyList();
+        } else {
+            String reqFilter;
+            if (filters.size() == 1) {
+                reqFilter = filters.get(0);
+            } else {
+                // If there are more BREE filters, we need to or them together
+                StringBuilder sb = new StringBuilder("(|");
+                for (String f : filters) {
+                    sb.append(f);
+                }
+                sb.append(")");
+                reqFilter = sb.toString();
+            }
+
+            SimpleFilter sf = SimpleFilter.parse(reqFilter);
+            return Collections.<Requirement>singletonList(new RequirementImpl(
+                    resource,
+                    ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE,
+                    Collections.singletonMap(ExecutionEnvironmentNamespace.REQUIREMENT_FILTER_DIRECTIVE, reqFilter),
+                    null,
+                    sf));
+        }
+    }
+
+    private static String getBreeVersionClause(Version ver) {
+        if (ver == null) {
+            return null;
+        }
+
+        return "(" + ExecutionEnvironmentNamespace.CAPABILITY_VERSION_ATTRIBUTE + "=" + ver + ")";
+    }
+
+    private static List<ParsedHeaderClause> normalizeRequireClauses(List<ParsedHeaderClause> clauses) {
+        // Convert bundle version attribute to VersionRange type.
+        for (ParsedHeaderClause clause : clauses) {
+            Object value = clause.attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE);
+            if (value != null) {
+                clause.attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, VersionRange.parseVersionRange(value.toString()));
+            }
+        }
+
+        return clauses;
+    }
+
+    private static List<Requirement> convertRequires(List<ParsedHeaderClause> clauses, Resource resource) {
+        List<Requirement> reqList = new ArrayList<>();
+        for (ParsedHeaderClause clause : clauses) {
+            for (String path : clause.paths) {
+                // Prepend the bundle symbolic name to the array of attributes.
+                Map<String, Object> attrs = clause.attrs;
+                // Note that we use a linked hash map here to ensure the
+                // symbolic name attribute is first, which will make indexing
+                // more efficient.
+                // TODO: OSGi R4.3 - This is ordering is kind of hacky.
+                // Prepend the symbolic name to the array of attributes.
+                Map<String, Object> newAttrs = new LinkedHashMap<>(attrs.size() + 1);
+                // We want this first from an indexing perspective.
+                newAttrs.put(BundleRevision.BUNDLE_NAMESPACE, path);
+                newAttrs.putAll(attrs);
+                // But we need to put it again to make sure it wasn't overwritten.
+                newAttrs.put(BundleRevision.BUNDLE_NAMESPACE, path);
+
+                // Create filter now so we can inject filter directive.
+                SimpleFilter sf = SimpleFilter.convert(newAttrs);
+
+                // Inject filter directive.
+                // TODO: OSGi R4.3 - Can we insert this on demand somehow?
+                Map<String, String> dirs = clause.dirs;
+                Map<String, String> newDirs = new StringArrayMap<>(dirs.size() + 1);
+                newDirs.putAll(dirs);
+                newDirs.put(Constants.FILTER_DIRECTIVE, sf.toString());
+
+                // Create package requirement and add to requirement list.
+                reqList.add(new RequirementImpl(resource, BundleRevision.BUNDLE_NAMESPACE, newDirs, newAttrs));
+            }
+        }
+
+        return reqList;
+    }
+
+    private static char charAt(int pos, String headers, int length) {
+        if (pos >= length) {
+            return EOF;
+        }
+        return headers.charAt(pos);
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static List<ParsedHeaderClause> parseStandardHeader(String header) {
+        List<ParsedHeaderClause> clauses = new ArrayList<>();
+        if (header == null) {
+            return clauses;
+        }
+        ParsedHeaderClause clause = null;
+        String key = null;
+        Map targetMap = null;
+        int state = CLAUSE_START;
+        int currentPosition = 0;
+        int startPosition = 0;
+        int length = header.length();
+        boolean quoted = false;
+        boolean escaped = false;
+
+        char currentChar;
+        do {
+            currentChar = charAt(currentPosition, header, length);
+            switch (state) {
+            case CLAUSE_START:
+                clause = new ParsedHeaderClause();
+                clauses.add(clause);
+                // Fall through
+            case PARAMETER_START:
+                startPosition = currentPosition;
+                state = KEY;
+                // Fall through
+            case KEY:
+                switch (currentChar) {
+                case ':':
+                case '=':
+                    key = header.substring(startPosition, currentPosition).trim();
+                    startPosition = currentPosition + 1;
+                    targetMap = clause.attrs;
+                    state = currentChar == ':' ? DIRECTIVE_OR_TYPEDATTRIBUTE : ARGUMENT;
+                    break;
+                case EOF:
+                case ',':
+                case ';':
+                    clause.paths.add(header.substring(startPosition, currentPosition).trim());
+                    state = currentChar == ',' ? CLAUSE_START : PARAMETER_START;
+                    break;
+                default:
+                    break;
+                }
+                currentPosition++;
+                break;
+            case DIRECTIVE_OR_TYPEDATTRIBUTE:
+                switch (currentChar) {
+                case '=':
+                    if (startPosition != currentPosition) {
+                        clause.types.put(key, header.substring(startPosition, currentPosition).trim());
+                    } else {
+                        targetMap = clause.dirs;
+                    }
+                    state = ARGUMENT;
+                    startPosition = currentPosition + 1;
+                    break;
+                default:
+                    break;
+                }
+                currentPosition++;
+                break;
+            case ARGUMENT:
+                if (currentChar == '\"') {
+                    quoted = true;
+                    currentPosition++;
+                } else {
+                    quoted = false;
+                }
+                if (!Character.isWhitespace(currentChar)) {
+                    state = VALUE;
+                } else {
+                    currentPosition++;
+                }
+                break;
+            case VALUE:
+                if (escaped) {
+                    escaped = false;
+                } else {
+                    if (currentChar == '\\') {
+                        escaped = true;
+                    } else if (quoted && currentChar == '\"') {
+                        quoted = false;
+                    } else if (!quoted) {
+                        String value;
+                        switch (currentChar) {
+                        case EOF:
+                        case ';':
+                        case ',':
+                            value = header.substring(startPosition, currentPosition).trim();
+                            if (value.startsWith("\"") && value.endsWith("\"")) {
+                                value = value.substring(1, value.length() - 1);
+                            }
+                            if (targetMap.put(key, value) != null) {
+                                throw new IllegalArgumentException(
+                                        "Duplicate '" + key + "' in: " + header);
+                            }
+                            state = currentChar == ';' ? PARAMETER_START : CLAUSE_START;
+                            break;
+                        default:
+                            break;
+                        }
+                    }
+                }
+                currentPosition++;
+                break;
+            default:
+                break;
+            }
+        } while (currentChar != EOF);
+
+        if (state > PARAMETER_START) {
+            throw new IllegalArgumentException("Unable to parse header: " + header);
+        }
+        return clauses;
+    }
+
+    public static List<String> parseDelimitedString(String value, String delim) {
+        return parseDelimitedString(value, delim, true);
+    }
+
+    /**
+     * Parses delimited string and returns an array containing the tokens. This
+     * parser obeys quotes, so the delimiter character will be ignored if it is
+     * inside of a quote. This method assumes that the quote character is not
+     * included in the set of delimiter characters.
+     *
+     * @param value the delimited string to parse.
+     * @param delim the characters delimiting the tokens.
+     * @param trim true to trim the string, false else.
+     * @return a list of string or an empty list if there are none.
+     */
+    public static List<String> parseDelimitedString(String value, String delim, boolean trim) {
+        if (value == null) {
+            value = "";
+        }
+
+        List<String> list = new ArrayList<>();
+
+        StringBuilder sb = new StringBuilder();
+
+        int expecting = CHAR | DELIMITER | STARTQUOTE;
+
+        boolean isEscaped = false;
+        for (int i = 0; i < value.length(); i++) {
+            char c = value.charAt(i);
+
+            boolean isDelimiter = delim.indexOf(c) >= 0;
+
+            if (!isEscaped && c == '\\') {
+                isEscaped = true;
+                continue;
+            }
+
+            if (isEscaped) {
+                sb.append(c);
+            } else if (isDelimiter && ((expecting & DELIMITER) > 0)) {
+                if (trim) {
+                    list.add(sb.toString().trim());
+                } else {
+                    list.add(sb.toString());
+                }
+                sb.delete(0, sb.length());
+                expecting = CHAR | DELIMITER | STARTQUOTE;
+            } else if ((c == '"') && (expecting & STARTQUOTE) > 0) {
+                sb.append(c);
+                expecting = CHAR | ENDQUOTE;
+            } else if ((c == '"') && (expecting & ENDQUOTE) > 0) {
+                sb.append(c);
+                expecting = CHAR | STARTQUOTE | DELIMITER;
+            } else if ((expecting & CHAR) > 0) {
+                sb.append(c);
+            } else {
+                throw new IllegalArgumentException("Invalid delimited string: " + value);
+            }
+
+            isEscaped = false;
+        }
+
+        if (sb.length() > 0) {
+            if (trim) {
+                list.add(sb.toString().trim());
+            } else {
+                list.add(sb.toString());
+            }
+        }
+
+        return list;
+    }
+
+
+    static class ParsedHeaderClause {
+        public final List<String> paths = new ArrayList<>();
+        public final Map<String, String> dirs = new StringArrayMap<>(0);
+        public final Map<String, Object> attrs = new StringArrayMap<>(0);
+        public final Map<String, String> types = new StringArrayMap<>(0);
+    }
+}

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java?rev=1829639&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/ResourceImpl.java Fri Apr 20 09:23:35 2018
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.utils.resource;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.utils.collections.StringArrayMap;
+import org.osgi.framework.Version;
+import org.osgi.framework.namespace.IdentityNamespace;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+
+/**
+ */
+public class ResourceImpl implements Resource {
+
+    protected final List<Capability> caps;
+    protected final List<Requirement> reqs;
+
+    /**
+     * CAUTION: This constructor does not ensure that the resource
+     * has the required identity capability
+     */
+    public ResourceImpl() {
+        caps = new ArrayList<>(0);
+        reqs = new ArrayList<>(0);
+    }
+
+    public ResourceImpl(String name, String type, Version version) {
+        caps = new ArrayList<>(1);
+        Map<String, String> dirs = new StringArrayMap<>(0);
+        Map<String, Object> attrs = new StringArrayMap<>(3);
+        attrs.put(IdentityNamespace.IDENTITY_NAMESPACE, name);
+        attrs.put(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, type);
+        attrs.put(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, version);
+        CapabilityImpl identity = new CapabilityImpl(this, IdentityNamespace.IDENTITY_NAMESPACE, dirs, attrs);
+        caps.add(identity);
+        reqs = new ArrayList<>(0);
+    }
+
+    public void addCapability(Capability capability) {
+        assert capability.getResource() == this;
+        caps.add(capability);
+    }
+
+    public void addCapabilities(Collection<? extends Capability> capabilities) {
+        for (Capability cap : capabilities) {
+            assert cap.getResource() == this;
+        }
+        caps.addAll(capabilities);
+    }
+
+    public void addRequirement(Requirement requirement) {
+        assert requirement.getResource() == this;
+        reqs.add(requirement);
+    }
+
+    public void addRequirements(Collection<? extends Requirement> requirements) {
+        for (Requirement req : requirements) {
+            assert req.getResource() == this;
+        }
+        reqs.addAll(requirements);
+    }
+
+    public List<Capability> getCapabilities(String namespace) {
+        List<Capability> result = caps;
+        if (namespace != null) {
+            result = new ArrayList<>();
+            for (Capability cap : caps) {
+                if (cap.getNamespace().equals(namespace)) {
+                    result.add(cap);
+                }
+            }
+        }
+        return result;
+    }
+
+    public List<Requirement> getRequirements(String namespace) {
+        List<Requirement> result = reqs;
+        if (namespace != null) {
+            result = new ArrayList<>();
+            for (Requirement req : reqs) {
+                if (req.getNamespace().equals(namespace)) {
+                    result.add(req);
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        for (Capability cap : caps) {
+            if (IdentityNamespace.IDENTITY_NAMESPACE.equals(cap.getNamespace())) {
+                return cap.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE) + "/"
+                        + cap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE);
+            }
+        }
+        return "Unidentified resource";
+    }
+
+}

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/SimpleFilter.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/SimpleFilter.java?rev=1829639&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/SimpleFilter.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/resource/SimpleFilter.java Fri Apr 20 09:23:35 2018
@@ -0,0 +1,592 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.utils.resource;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.felix.utils.version.VersionRange;
+
+public class SimpleFilter {
+    public static final int MATCH_ALL = 0;
+    public static final int AND = 1;
+    public static final int OR = 2;
+    public static final int NOT = 3;
+    public static final int EQ = 4;
+    public static final int LTE = 5;
+    public static final int GTE = 6;
+    public static final int SUBSTRING = 7;
+    public static final int PRESENT = 8;
+    public static final int APPROX = 9;
+
+    /**
+     * Strings which are commonly found in filter specification. We use this map as an interner.
+     */
+    private static final ConcurrentHashMap<String, String> COMMON_STRINGS;
+
+    static {
+        ConcurrentHashMap<String, String> s = new ConcurrentHashMap<>(8);
+        s.put("optional", "optional");
+        s.put("osgi.ee", "osgi.ee");
+        s.put("resolution", "resolution");
+        s.put("uses", "uses");
+        s.put("version", "version");
+        COMMON_STRINGS = s;
+    }
+
+    public static final SimpleFilter MATCH_ALL_FILTER = new SimpleFilter(null, null, MATCH_ALL);
+
+    private final String name;
+    private final Object value;
+    private final int op;
+
+    SimpleFilter(String name, Object value, int op) {
+        this.name = reuseCommonString(name);
+        this.value = value;
+        this.op = op;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Object getValue() {
+        return value;
+    }
+
+    public int getOperation() {
+        return op;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        toString(sb);
+        return sb.toString();
+    }
+
+    private void toString(StringBuilder sb) {
+        switch (op)
+        {
+        case AND:
+            sb.append("(&");
+            toString(sb, (List<?>) value);
+            sb.append(")");
+            break;
+        case OR:
+            sb.append("(|");
+            toString(sb, (List<?>) value);
+            sb.append(")");
+            break;
+        case NOT:
+            sb.append("(!");
+            toString(sb, (List<?>) value);
+            sb.append(")");
+            break;
+        case EQ:
+            sb.append("(")
+                    .append(name)
+                    .append("=");
+            toEncodedString(sb, value);
+            sb.append(")");
+            break;
+        case LTE:
+            sb.append("(")
+                    .append(name)
+                    .append("<=");
+            toEncodedString(sb, value);
+            sb.append(")");
+            break;
+        case GTE:
+            sb.append("(")
+                    .append(name)
+                    .append(">=");
+            toEncodedString(sb, value);
+            sb.append(")");
+            break;
+        case SUBSTRING:
+            sb.append("(").append(name).append("=");
+            unparseSubstring(sb, (List<?>) value);
+            sb.append(")");
+            break;
+        case PRESENT:
+            sb.append("(").append(name).append("=*)");
+            break;
+        case APPROX:
+            sb.append("(").append(name).append("~=");
+            toEncodedString(sb, value);
+            sb.append(")");
+            break;
+        case MATCH_ALL:
+            sb.append("(*)");
+            break;
+        }
+    }
+
+    private static String reuseCommonString(String str) {
+        // TODO: when JDK8, should be:
+        // TODO: return str != null ? COMMON_STRINGS.getOrDefault(str, str) : null;
+        if (str != null) {
+            String r = COMMON_STRINGS.get(str);
+            return r != null ? r : str;
+        } else {
+            return null;
+        }
+    }
+
+    private static void toString(StringBuilder sb, List<?> list) {
+        for (Object o : list) {
+            SimpleFilter sf = (SimpleFilter) o;
+            sf.toString(sb);
+        }
+    }
+
+
+    private static String toDecodedString(String s, int startIdx, int endIdx) {
+        StringBuilder sb = null;
+        boolean escaped = false;
+        for (int i = startIdx; i < endIdx; i++) {
+            char c = s.charAt(i);
+            if (!escaped && (c == '\\')) {
+                sb = new StringBuilder(endIdx - startIdx);
+                sb.append(s, startIdx, i);
+                escaped = true;
+            } else {
+                escaped = false;
+                if (sb != null) {
+                    sb.append(c);
+                }
+            }
+        }
+        return sb != null ? sb.toString() : s.substring(startIdx, endIdx);
+    }
+
+    private static void toEncodedString(StringBuilder sb, Object o) {
+        if (o instanceof String) {
+            String s = (String) o;
+            for (int i = 0; i < s.length(); i++) {
+                char c = s.charAt(i);
+                if ((c == '\\') || (c == '(') || (c == ')') || (c == '*')) {
+                    sb.append('\\');
+                }
+                sb.append(c);
+            }
+        } else {
+            sb.append(o);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public static SimpleFilter parse(String filter) {
+        int idx = skipWhitespace(filter, 0);
+
+        if ((filter == null) || (filter.length() == 0) || (idx >= filter.length())) {
+            throw new IllegalArgumentException("Null or empty filter.");
+        } else if (filter.charAt(idx) != '(') {
+            throw new IllegalArgumentException("Missing opening parenthesis: " + filter);
+        }
+
+        SimpleFilter sf = null;
+        Deque<Object> stack = new LinkedList<>();
+        boolean isEscaped = false;
+        while (idx < filter.length()) {
+            if (sf != null) {
+                throw new IllegalArgumentException(
+                        "Only one top-level operation allowed: " + filter);
+            }
+
+            if (!isEscaped && (filter.charAt(idx) == '(')) {
+                // Skip paren and following whitespace.
+                idx = skipWhitespace(filter, idx + 1);
+
+                if (filter.charAt(idx) == '&') {
+                    int peek = skipWhitespace(filter, idx + 1);
+                    if (filter.charAt(peek) == '(') {
+                        idx = peek - 1;
+                        stack.addFirst(new SimpleFilter(null, new ArrayList<>(2), SimpleFilter.AND));
+                    } else {
+                        stack.addFirst(idx);
+                    }
+                } else if (filter.charAt(idx) == '|') {
+                    int peek = skipWhitespace(filter, idx + 1);
+                    if (filter.charAt(peek) == '(') {
+                        idx = peek - 1;
+                        stack.addFirst(new SimpleFilter(null, new ArrayList<>(2), SimpleFilter.OR));
+                    } else {
+                        stack.addFirst(idx);
+                    }
+                } else if (filter.charAt(idx) == '!') {
+                    int peek = skipWhitespace(filter, idx + 1);
+                    if (filter.charAt(peek) == '(') {
+                        idx = peek - 1;
+                        stack.addFirst(new SimpleFilter(null, new ArrayList<>(1), SimpleFilter.NOT));
+                    } else {
+                        stack.addFirst(idx);
+                    }
+                } else {
+                    stack.addFirst(idx);
+                }
+            } else if (!isEscaped && (filter.charAt(idx) == ')')) {
+                Object top = stack.removeFirst();
+                Object next = stack.peekFirst();
+                if (top instanceof SimpleFilter) {
+                    if (next instanceof SimpleFilter) {
+                        ((List<Object>) ((SimpleFilter) next).value).add(top);
+                    } else {
+                        sf = (SimpleFilter) top;
+                    }
+                } else if (next instanceof SimpleFilter) {
+                    ((List<Object>) ((SimpleFilter) next).value).add(
+                            SimpleFilter.subfilter(filter, (Integer) top, idx));
+                } else {
+                    sf = SimpleFilter.subfilter(filter, (Integer) top, idx);
+                }
+            } else {
+                isEscaped = !isEscaped && (filter.charAt(idx) == '\\');
+            }
+
+            idx = skipWhitespace(filter, idx + 1);
+        }
+
+        if (sf == null) {
+            throw new IllegalArgumentException("Missing closing parenthesis: " + filter);
+        }
+
+        return sf;
+    }
+
+    private static SimpleFilter subfilter(String filter, int startIdx, int endIdx) {
+        // Determine the ending index of the attribute name.
+        int attrEndIdx = startIdx;
+        for (int i = 0; i < (endIdx - startIdx); i++) {
+            char c = filter.charAt(startIdx + i);
+            if ("=<>~".indexOf(c) >= 0) {
+                break;
+            } else if (!Character.isWhitespace(c)) {
+                attrEndIdx = startIdx + i + 1;
+            }
+        }
+        if (attrEndIdx == startIdx) {
+            throw new IllegalArgumentException(
+                    "Missing attribute name: " + filter.substring(startIdx, endIdx));
+        }
+        String attr = filter.substring(startIdx, attrEndIdx);
+
+        // Skip the attribute name and any following whitespace.
+        startIdx = skipWhitespace(filter, attrEndIdx);
+
+        // Determine the operator type.
+        int op;
+        switch (filter.charAt(startIdx)) {
+        case '=':
+            op = EQ;
+            startIdx++;
+            break;
+        case '<':
+            if (filter.charAt(startIdx + 1) != '=') {
+                throw new IllegalArgumentException(
+                        "Unknown operator: " + filter.substring(startIdx, endIdx));
+            }
+            op = LTE;
+            startIdx += 2;
+            break;
+        case '>':
+            if (filter.charAt(startIdx + 1) != '=') {
+                throw new IllegalArgumentException(
+                        "Unknown operator: " + filter.substring(startIdx, endIdx));
+            }
+            op = GTE;
+            startIdx += 2;
+            break;
+        case '~':
+            if (filter.charAt(startIdx + 1) != '=') {
+                throw new IllegalArgumentException(
+                        "Unknown operator: " + filter.substring(startIdx, endIdx));
+            }
+            op = APPROX;
+            startIdx += 2;
+            break;
+        default:
+            throw new IllegalArgumentException(
+                    "Unknown operator: " + filter.substring(startIdx, endIdx));
+        }
+
+        // Parse value.
+        Object value = toDecodedString(filter, startIdx, endIdx);
+
+        // Check if the equality comparison is actually a substring
+        // or present operation.
+        if (op == EQ) {
+            String valueStr = filter.substring(startIdx, endIdx);
+            List<String> values = parseSubstring(valueStr);
+            if ((values.size() == 2)
+                    && (values.get(0).length() == 0)
+                    && (values.get(1).length() == 0)) {
+                op = PRESENT;
+            } else if (values.size() > 1) {
+                op = SUBSTRING;
+                value = values;
+            }
+        }
+
+        return new SimpleFilter(attr, value, op);
+    }
+
+    public static List<String> parseSubstring(String value) {
+        int length = value.length();
+
+        boolean isSimple = true;
+        for (int idx = 0; idx < length; idx++) {
+            char c = value.charAt(idx);
+            if (c == '*' || c == '\\') {
+                isSimple = false;
+                break;
+            }
+        }
+        if (isSimple) {
+            return Collections.singletonList(value);
+        }
+
+        StringBuilder ss = new StringBuilder();
+        // int kind = SIMPLE; // assume until proven otherwise
+        boolean wasStar = false; // indicates last piece was a star
+        boolean leftstar = false; // track if the initial piece is a star
+        boolean rightstar = false; // track if the final piece is a star
+
+        int idx = 0;
+
+        // We assume (sub)strings can contain leading and trailing blanks
+        List<String> pieces = new ArrayList<>(2);
+        boolean escaped = false;
+        for (;;) {
+            if (idx >= length) {
+                if (wasStar) {
+                    // insert last piece as "" to handle trailing star
+                    rightstar = true;
+                } else {
+                    pieces.add(ss.toString());
+                    // accumulate the last piece
+                    // note that in the case of
+                    // (cn=); this might be
+                    // the string "" (!=null)
+                }
+                ss.setLength(0);
+                break;
+            }
+
+            // Read the next character and account for escapes.
+            char c = value.charAt(idx++);
+            if (!escaped && (c == '*')) {
+                // If we have successive '*' characters, then we can
+                // effectively collapse them by ignoring succeeding ones.
+                if (!wasStar) {
+                    if (ss.length() > 0) {
+                        pieces.add(ss.toString()); // accumulate the pieces
+                        // between '*' occurrences
+                    }
+                    ss.setLength(0);
+                    // if this is a leading star, then track it
+                    if (pieces.isEmpty()) {
+                        leftstar = true;
+                    }
+                    wasStar = true;
+                }
+            } else if (!escaped && (c == '\\')) {
+                escaped = true;
+            } else {
+                escaped = false;
+                wasStar = false;
+                ss.append(c);
+            }
+        }
+        if (leftstar || rightstar || pieces.size() > 1) {
+            // insert leading and/or trailing "" to anchor ends
+            if (rightstar) {
+                pieces.add("");
+            }
+            if (leftstar) {
+                pieces.add(0, "");
+            }
+        }
+        return pieces;
+    }
+
+    public static void unparseSubstring(StringBuilder sb, List<?> pieces) {
+        for (int i = 0; i < pieces.size(); i++) {
+            if (i > 0) {
+                sb.append("*");
+            }
+            toEncodedString(sb, pieces.get(i));
+        }
+    }
+
+    public static boolean compareSubstring(List<String> pieces, String s) {
+        // Walk the pieces to match the string
+        // There are implicit stars between each piece,
+        // and the first and last pieces might be "" to anchor the match.
+        // assert (pieces.length > 1)
+        // minimal case is <string>*<string>
+
+        boolean result = true;
+        int len = pieces.size();
+
+        // Special case, if there is only one piece, then
+        // we must perform an equality test.
+        if (len == 1) {
+            return s.equals(pieces.get(0));
+        }
+
+        // Otherwise, check whether the pieces match
+        // the specified string.
+
+        int index = 0;
+
+        for (int i = 0; i < len; i++) {
+            String piece = pieces.get(i);
+
+            // If this is the first piece, then make sure the
+            // string starts with it.
+            if (i == 0) {
+                if (!s.startsWith(piece)) {
+                    result = false;
+                    break;
+                }
+            }
+
+            // If this is the last piece, then make sure the
+            // string ends with it.
+            if (i == (len - 1)) {
+                result = s.endsWith(piece) && (s.length() >= (index + piece.length()));
+                break;
+            }
+
+            // If this is neither the first or last piece, then
+            // make sure the string contains it.
+            if ((i > 0) && (i < (len - 1))) {
+                index = s.indexOf(piece, index);
+                if (index < 0) {
+                    result = false;
+                    break;
+                }
+            }
+
+            // Move string index beyond the matching piece.
+            index += piece.length();
+        }
+
+        return result;
+    }
+
+    private static int skipWhitespace(String s, int startIdx) {
+        int len = s.length();
+        while ((startIdx < len) && Character.isWhitespace(s.charAt(startIdx))) {
+            startIdx++;
+        }
+        return startIdx;
+    }
+
+    /**
+     * Converts a attribute map to a filter. The filter is created by iterating
+     * over the map's entry set. If ordering of attributes is important (e.g.,
+     * for hitting attribute indices), then the map's entry set should iterate
+     * in the desired order. Equality testing is assumed for all attribute types
+     * other than version ranges, which are handled appropriated. If the attribute
+     * map is empty, then a filter that matches anything is returned.
+     *
+     * @param attrs Map of attributes to convert to a filter.
+     * @return A filter corresponding to the attributes.
+     */
+    public static SimpleFilter convert(Map<String, Object> attrs) {
+        // Rather than building a filter string to be parsed into a SimpleFilter,
+        // we will just create the parsed SimpleFilter directly.
+
+        List<SimpleFilter> filters = new ArrayList<>(attrs.size());
+
+        for (Entry<String, Object> entry : attrs.entrySet()) {
+            if (entry.getValue() instanceof VersionRange) {
+                VersionRange vr = (VersionRange) entry.getValue();
+                if (!vr.isOpenFloor()) {
+                    filters.add(
+                            new SimpleFilter(
+                                    entry.getKey(),
+                                    vr.getFloor().toString(),
+                                    SimpleFilter.GTE)
+                    );
+                } else {
+                    SimpleFilter val = new SimpleFilter(
+                        entry.getKey(),
+                        vr.getFloor().toString(),
+                        SimpleFilter.LTE);
+                    SimpleFilter not =
+                            new SimpleFilter(null, Collections.singletonList(val), SimpleFilter.NOT);
+                    filters.add(not);
+                }
+
+                if (vr.getCeiling() != null) {
+                    if (!vr.isOpenCeiling()) {
+                        filters.add(
+                                new SimpleFilter(
+                                        entry.getKey(),
+                                        vr.getCeiling().toString(),
+                                        SimpleFilter.LTE)
+                        );
+                    } else if (!vr.getCeiling().equals(VersionRange.INFINITE_VERSION)) {
+                        SimpleFilter val = new SimpleFilter(
+                            entry.getKey(),
+                            vr.getCeiling().toString(),
+                            SimpleFilter.GTE);
+                        SimpleFilter not =
+                                new SimpleFilter(null, Collections.singletonList(val), SimpleFilter.NOT);
+                        filters.add(not);
+                    }
+                }
+            } else {
+                List<String> values = SimpleFilter.parseSubstring(entry.getValue().toString());
+                if (values.size() > 1) {
+                    filters.add(
+                            new SimpleFilter(
+                                    entry.getKey(),
+                                    values,
+                                    SimpleFilter.SUBSTRING)
+                    );
+                } else {
+                    filters.add(
+                            new SimpleFilter(
+                                    entry.getKey(),
+                                    values.get(0),
+                                    SimpleFilter.EQ)
+                    );
+                }
+            }
+        }
+
+        switch (filters.size()) {
+            case 0:
+                return MATCH_ALL_FILTER;
+            case 1:
+                return filters.get(0);
+            default:
+                return new SimpleFilter(null, filters, SimpleFilter.AND);
+        }
+    }
+}

Added: felix/trunk/utils/src/test/java/org/apache/felix/utils/collections/StringArrayMapTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/java/org/apache/felix/utils/collections/StringArrayMapTest.java?rev=1829639&view=auto
==============================================================================
--- felix/trunk/utils/src/test/java/org/apache/felix/utils/collections/StringArrayMapTest.java (added)
+++ felix/trunk/utils/src/test/java/org/apache/felix/utils/collections/StringArrayMapTest.java Fri Apr 20 09:23:35 2018
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.utils.collections;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class StringArrayMapTest {
+
+    @Test
+    public void testHashCode() {
+        StringArrayMap<String> m1 = new StringArrayMap<>();
+        StringArrayMap<String> m2 = new StringArrayMap<>();
+        m1.put("ding", "dong");
+        m1.put("la", "la");
+        m2.put("ding", "dong");
+        m2.put("la", "la");
+        assertEquals(m1, m2);
+        assertEquals(m1.hashCode(), m2.hashCode());
+    }
+}

Modified: felix/trunk/utils/src/test/java/org/apache/felix/utils/resource/CapabilityImplTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/java/org/apache/felix/utils/resource/CapabilityImplTest.java?rev=1829639&r1=1829638&r2=1829639&view=diff
==============================================================================
--- felix/trunk/utils/src/test/java/org/apache/felix/utils/resource/CapabilityImplTest.java (original)
+++ felix/trunk/utils/src/test/java/org/apache/felix/utils/resource/CapabilityImplTest.java Fri Apr 20 09:23:35 2018
@@ -20,7 +20,6 @@ package org.apache.felix.utils.resource;
 
 import junit.framework.TestCase;
 
-import org.apache.felix.utils.resource.CapabilityImpl;
 import org.mockito.Mockito;
 import org.osgi.resource.Resource;
 
@@ -28,46 +27,49 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
+import static org.junit.Assert.assertNotEquals;
+
 public class CapabilityImplTest extends TestCase {
+
     public void testCapability() {
+        Resource res = new ResourceImpl();
         Map<String, Object> attrs = Collections.<String,Object>singletonMap("foo", "bar");
         Map<String, String> dirs = Collections.emptyMap();
-        CapabilityImpl c = new CapabilityImpl("org.foo.bar", attrs, dirs);
+        CapabilityImpl c = new CapabilityImpl(Mockito.mock(Resource.class), "org.foo.bar", dirs, attrs);
 
         assertEquals("org.foo.bar", c.getNamespace());
         assertEquals(attrs, c.getAttributes());
         assertEquals(dirs, c.getDirectives());
-        assertNull(c.getResource());
+        assertNotNull(c.getResource());
     }
 
     public void testCapabilityEqualsHashcode() {
-        Map<String, Object> attrs = new HashMap<String, Object>();
+        Map<String, Object> attrs = new HashMap<>();
         attrs.put("ding", "dong");
         attrs.put("la", "la");
         Map<String, String> dirs = Collections.singletonMap("a", "b");
         Resource res = Mockito.mock(Resource.class);
-        CapabilityImpl c1 = new CapabilityImpl("org.foo.bar", attrs, dirs, res);
+        CapabilityImpl c1 = new CapabilityImpl(res, "org.foo.bar", dirs, attrs);
         assertEquals(res, c1.getResource());
 
-        CapabilityImpl c2 = new CapabilityImpl("org.foo.bar", attrs, dirs, res);
+        CapabilityImpl c2 = new CapabilityImpl(res, "org.foo.bar", dirs, attrs);
         assertEquals(c1, c2);
         assertEquals(c1.hashCode(), c2.hashCode());
 
-        CapabilityImpl c3 = new CapabilityImpl("org.foo.bar2", attrs, dirs, res);
-        assertFalse(c1.equals(c3));
+        CapabilityImpl c3 = new CapabilityImpl(res, "org.foo.bar2", dirs, attrs);
+        assertNotEquals(c1, c3);
         assertFalse(c1.hashCode() == c3.hashCode());
     }
 
     public void testCopyCapability() {
         Resource res = Mockito.mock(Resource.class);
-        CapabilityImpl c = new CapabilityImpl("x.y.z",
-                Collections.<String, Object>singletonMap("a", 123),
+        CapabilityImpl c = new CapabilityImpl(res, "x.y.z",
                 Collections.<String, String>singletonMap("x", "y"),
-                res);
+                Collections.<String, Object>singletonMap("a", 123));
 
         Resource res2 = Mockito.mock(Resource.class);
         CapabilityImpl c2 = new CapabilityImpl(res2, c);
-        assertFalse("Should not be equal, the resources are different", c.equals(c2));
+        assertNotEquals("Should not be equal, the resources are different", c, c2);
 
         CapabilityImpl c3 = new CapabilityImpl(res, c);
         assertEquals(c, c3);