You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by da...@apache.org on 2018/04/27 10:02:45 UTC

[sling-org-apache-sling-feature-resolver] 01/20: SLING-7512 Order features based on their dependencies.

This is an automated email from the ASF dual-hosted git repository.

davidb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-resolver.git

commit 29e4150eb7a154678d77ee86073d7134bc6b0006
Author: David Bosschaert <bo...@adobe.com>
AuthorDate: Fri Feb 23 13:42:28 2018 +0000

    SLING-7512 Order features based on their dependencies.
    
    Very initial implementation which contains the refactoring of
    Requirements and Capabilities to use the OSGi ones.
---
 pom.xml                                            |  98 +++++++++
 .../sling/feature/resolver/FrameworkResolver.java  | 224 +++++++++++++++++++++
 .../feature/resolver/impl/BundleResourceImpl.java  | 191 ++++++++++++++++++
 .../feature/resolver/impl/ResolveContextImpl.java  |  96 +++++++++
 .../feature/resolver/FrameworkResolverTest.java    | 100 +++++++++
 .../resolver/impl/BundleResourceImplTest.java      | 182 +++++++++++++++++
 .../resolver/impl/ResolveContextImplTest.java      | 129 ++++++++++++
 src/test/resources/feature1.json                   |   5 +
 src/test/resources/feature2.json                   |   5 +
 src/test/resources/feature3.json                   |   5 +
 10 files changed, 1035 insertions(+)

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..c753af4
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+    <!--
+        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.
+    -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>32</version>
+        <relativePath />
+    </parent>
+
+    <artifactId>org.apache.sling.feature.resolver</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+    
+    <name>Apache Sling Feature Resolver</name>
+    <description>
+        Resolver integration of the Feature Model
+    </description>
+
+    <properties>
+        <sling.java.version>8</sling.java.version>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.analyser</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.support</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.resolver</artifactId>
+            <version>1.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Testing -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>2.8.9</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>5.6.10</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/feature/resolver/FrameworkResolver.java b/src/main/java/org/apache/sling/feature/resolver/FrameworkResolver.java
new file mode 100644
index 0000000..f62c3db
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/resolver/FrameworkResolver.java
@@ -0,0 +1,224 @@
+/*
+ * 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.sling.feature.resolver;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.analyser.impl.BundleDescriptorImpl;
+import org.apache.sling.feature.process.FeatureResolver;
+import org.apache.sling.feature.resolver.impl.BundleResourceImpl;
+import org.apache.sling.feature.resolver.impl.ResolveContextImpl;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+import org.osgi.framework.namespace.BundleNamespace;
+import org.osgi.framework.namespace.HostNamespace;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import org.osgi.resource.Wire;
+import org.osgi.service.resolver.ResolutionException;
+import org.osgi.service.resolver.Resolver;
+
+public class FrameworkResolver implements FeatureResolver {
+    private final ArtifactManager artifactManager;
+    private final Resolver resolver;
+    private final Resource frameworkResource;
+    private final Framework framework;
+
+    public FrameworkResolver(ArtifactManager am, Map<String, String> frameworkProperties) {
+        artifactManager = am;
+
+        Resolver r = null;
+        // Launch an OSGi framework and obtain its resolver
+        try {
+            FrameworkFactory fwf = ServiceLoader.load(FrameworkFactory.class).iterator().next();
+            framework = fwf.newFramework(frameworkProperties);
+            framework.init();
+            framework.start();
+            BundleContext ctx = framework.getBundleContext();
+
+            // Create a resource representing the framework
+            BundleRevision br = framework.adapt(BundleRevision.class);
+            List<Capability> caps = br.getCapabilities(PackageNamespace.PACKAGE_NAMESPACE);
+            frameworkResource = new BundleResourceImpl(
+                    Collections.singletonMap(PackageNamespace.PACKAGE_NAMESPACE, caps), Collections.emptyMap());
+
+            int i=0;
+            while (i < 20) {
+                ServiceReference<Resolver> ref = ctx.getServiceReference(Resolver.class);
+                if (ref != null) {
+                    r = ctx.getService(ref);
+                    break;
+                }
+
+                // The service isn't there yet, let's wait a little and try again
+                Thread.sleep(500);
+                i++;
+            }
+        } catch (BundleException | InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        resolver = r;
+    }
+
+    @Override
+    public void close() throws Exception {
+        framework.stop();
+    }
+
+    @Override
+    public List<Feature> orderFeatures(List<Feature> features) {
+        try {
+            return internalOrderFeatures(features);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public List<Feature> internalOrderFeatures(List<Feature> features) throws IOException {
+        Map<Resource, Feature> bundleMap = new HashMap<>();
+        for (Feature f : features) {
+            for (Artifact b : f.getBundles()) {
+                BundleDescriptor bd = getBundleDescriptor(artifactManager, b);
+                Resource r = new BundleResourceImpl(bd);
+                bundleMap.put(r, f);
+            }
+        }
+
+        Set<Resource> availableBundles = new HashSet<>(bundleMap.keySet());
+        // Add these to the available features
+        Artifact lpa = new Artifact(ArtifactId.parse("org.apache.sling/org.apache.sling.launchpad.api/1.2.0"));
+        availableBundles.add(new BundleResourceImpl(getBundleDescriptor(artifactManager, lpa)));
+        availableBundles.add(frameworkResource);
+
+        List<Resource> orderedBundles = new LinkedList<>();
+        try {
+            for (Resource bundle : bundleMap.keySet()) {
+                if (orderedBundles.contains(bundle)) {
+                    // Already handled
+                    continue;
+                }
+                Map<Resource, List<Wire>> deps = resolver.resolve(new ResolveContextImpl(bundle, availableBundles));
+
+                for (Map.Entry<Resource, List<Wire>> entry : deps.entrySet()) {
+                    Resource curBundle = entry.getKey();
+
+                    if (!bundleMap.containsKey(curBundle)) {
+                        // This is some synthesized bundle. Ignoring.
+                        continue;
+                    }
+
+                    if (!orderedBundles.contains(curBundle)) {
+                        orderedBundles.add(curBundle);
+                    }
+
+                    for (Wire w : entry.getValue()) {
+                        Resource provBundle = w.getProvider();
+                        int curBundleIdx = orderedBundles.indexOf(curBundle);
+                        int newBundleIdx = orderedBundles.indexOf(provBundle);
+                        if (newBundleIdx >= 0) {
+                            if (curBundleIdx < newBundleIdx) {
+                                // If the list already contains the providing but after the current bundle, remove it there to move it before the current bundle
+                                orderedBundles.remove(provBundle);
+                            } else {
+                                // If the providing bundle is already before the current bundle, then no need to change anything
+                                continue;
+                            }
+                        }
+                        orderedBundles.add(curBundleIdx, provBundle);
+                    }
+                }
+            }
+        } catch (ResolutionException e) {
+            throw new RuntimeException(e);
+        }
+
+        // Sort the fragments so that fragments are started before the host bundle
+        for (int i=0; i<orderedBundles.size(); i++) {
+            Resource r = orderedBundles.get(i);
+            List<Requirement> reqs = r.getRequirements(HostNamespace.HOST_NAMESPACE);
+            if (reqs.size() > 0) {
+                // This is a fragment
+                Requirement req = reqs.iterator().next(); // TODO handle more host requirements
+                String bsn = req.getAttributes().get(HostNamespace.HOST_NAMESPACE).toString(); // TODO this is not valid, should obtain from filter
+                int idx = getBundleIndex(orderedBundles, bsn); // TODO check for filter too
+                if (idx < i) {
+                    // the fragment is after the host, and should be moved to be before the host
+                    Resource frag = orderedBundles.remove(i);
+                    orderedBundles.add(idx, frag);
+                }
+            }
+        }
+
+        List<Feature> orderedFeatures = new ArrayList<>();
+        for (Resource r : orderedBundles) {
+            Feature f = bundleMap.get(r);
+            if (f != null) {
+                if (!orderedFeatures.contains(f)) {
+                    orderedFeatures.add(f);
+                }
+            }
+        }
+        return orderedFeatures;
+    }
+
+    private static int getBundleIndex(List<Resource> bundles, String bundleSymbolicName) {
+        for (int i=0; i<bundles.size(); i++) {
+            Resource b = bundles.get(i);
+            if (bundleSymbolicName.equals(getBundleSymbolicName(b))) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private static String getBundleSymbolicName(Resource b) {
+        for (Capability cap : b.getCapabilities(BundleNamespace.BUNDLE_NAMESPACE)) {
+            return cap.getAttributes().get(BundleNamespace.BUNDLE_NAMESPACE).toString();
+        }
+        return null;
+    }
+
+    private static BundleDescriptor getBundleDescriptor(ArtifactManager artifactManager, Artifact b) throws IOException {
+        final File file = artifactManager.getArtifactHandler(b.getId().toMvnUrl()).getFile();
+        if ( file == null ) {
+            throw new IOException("Unable to find file for " + b.getId());
+        }
+
+        return new BundleDescriptorImpl(b, file, -1);
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/resolver/impl/BundleResourceImpl.java b/src/main/java/org/apache/sling/feature/resolver/impl/BundleResourceImpl.java
new file mode 100644
index 0000000..16cc812
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/resolver/impl/BundleResourceImpl.java
@@ -0,0 +1,191 @@
+/*
+ * 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.sling.feature.resolver.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.sling.feature.OSGiCapability;
+import org.apache.sling.feature.OSGiRequirement;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.support.util.PackageInfo;
+import org.osgi.framework.Version;
+import org.osgi.framework.VersionRange;
+import org.osgi.framework.namespace.BundleNamespace;
+import org.osgi.framework.namespace.ExecutionEnvironmentNamespace;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+
+/**
+ * Implementation of the OSGi Resource interface.
+ */
+public class BundleResourceImpl implements Resource {
+    final String hint;
+    final Map<String, List<Capability>> capabilities;
+    final Map<String, List<Requirement>> requirements;
+
+    /**
+     * Create a resource based on a BundleDescriptor.
+     * @param bd The BundleDescriptor to represent.
+     */
+    public BundleResourceImpl(BundleDescriptor bd) {
+        hint = bd.getBundleSymbolicName() + " " + bd.getBundleVersion();
+        Map<String, List<Capability>> caps = new HashMap<>();
+        for (Capability c : bd.getCapabilities()) {
+            List<Capability> l = caps.get(c.getNamespace());
+            if (l == null) {
+                l = new ArrayList<>();
+                caps.put(c.getNamespace(), l);
+            }
+            l.add(new OSGiCapability(this, c));
+        }
+
+        // Add the package capabilities (export package)
+        List<Capability> pkgCaps = new ArrayList<>();
+        for(PackageInfo exported : bd.getExportedPackages()) {
+            Map<String, Object> attrs = new HashMap<>();
+            attrs.put(PackageNamespace.PACKAGE_NAMESPACE, exported.getName());
+            attrs.put(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, exported.getPackageVersion());
+            attrs.put(PackageNamespace.CAPABILITY_BUNDLE_SYMBOLICNAME_ATTRIBUTE, bd.getBundleSymbolicName());
+            attrs.put(PackageNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, new Version(bd.getBundleVersion()));
+            pkgCaps.add(new OSGiCapability(this, PackageNamespace.PACKAGE_NAMESPACE, attrs, Collections.emptyMap()));
+        }
+        caps.put(PackageNamespace.PACKAGE_NAMESPACE, Collections.unmodifiableList(pkgCaps));
+
+        // Add the bundle capability
+        Map<String, Object> battrs = new HashMap<>();
+        battrs.put(BundleNamespace.BUNDLE_NAMESPACE, bd.getBundleSymbolicName());
+        battrs.put(BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, new Version(bd.getBundleVersion()));
+        OSGiCapability bundleCap = new OSGiCapability(this, BundleNamespace.BUNDLE_NAMESPACE, battrs, Collections.emptyMap());
+        caps.put(BundleNamespace.BUNDLE_NAMESPACE, Collections.singletonList(bundleCap));
+        capabilities = Collections.unmodifiableMap(caps);
+
+        Map<String, List<Requirement>> reqs = new HashMap<>();
+        for (Requirement r : bd.getRequirements()) {
+            List<Requirement> l = reqs.get(r.getNamespace());
+            if (l == null) {
+                l = new ArrayList<>();
+                reqs.put(r.getNamespace(), l);
+            }
+            // Add the requirement and associate with this resource
+            l.add(new OSGiRequirement(this, r));
+        }
+
+        // TODO What do we do with the execution environment?
+        reqs.remove(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE);
+
+        // Add the package requirements (import package)
+        List<Requirement> pkgReqs = new ArrayList<>();
+        for(PackageInfo imported : bd.getImportedPackages()) {
+            Map<String, String> dirs = new HashMap<>();
+            VersionRange range = imported.getPackageVersionRange();
+            String rangeFilter;
+            if (range != null) {
+                rangeFilter = range.toFilterString(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE);
+            } else {
+                rangeFilter = "";
+            }
+            dirs.put(PackageNamespace.REQUIREMENT_FILTER_DIRECTIVE,
+                "(&(" + PackageNamespace.PACKAGE_NAMESPACE + "=" + imported.getName() + ")" + rangeFilter + ")");
+            if (imported.isOptional())
+                dirs.put(PackageNamespace.REQUIREMENT_RESOLUTION_DIRECTIVE,
+                    PackageNamespace.RESOLUTION_OPTIONAL);
+            pkgReqs.add(new OSGiRequirement(this, PackageNamespace.PACKAGE_NAMESPACE, Collections.emptyMap(), dirs));
+        }
+        reqs.put(PackageNamespace.PACKAGE_NAMESPACE, Collections.unmodifiableList(pkgReqs));
+        requirements = Collections.unmodifiableMap(reqs);
+    }
+
+    /**
+     * Constructor. Create a resource based on capabilties and requirements.
+     * @param hnt
+     * @param caps The capabilities of the resource.
+     * @param reqs The requirements of the resource.
+     */
+    public BundleResourceImpl(Map<String, List<Capability>> caps, Map<String, List<Requirement>> reqs) {
+        hint = "" + System.identityHashCode(this);
+        capabilities = caps;
+        requirements = reqs;
+    }
+
+    @Override
+    public List<Capability> getCapabilities(String namespace) {
+        if (namespace == null) {
+            return capabilities.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
+        }
+
+        List<Capability> caps = capabilities.get(namespace);
+        if (caps == null)
+            return Collections.emptyList();
+        return caps;
+    }
+
+    @Override
+    public List<Requirement> getRequirements(String namespace) {
+        if (namespace == null) {
+            return requirements.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
+        }
+
+        List<Requirement> reqs = requirements.get(namespace);
+        if (reqs == null)
+            return Collections.emptyList();
+        return reqs;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((capabilities == null) ? 0 : capabilities.hashCode());
+        result = prime * result + ((requirements == null) ? 0 : requirements.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        BundleResourceImpl other = (BundleResourceImpl) obj;
+        if (capabilities == null) {
+            if (other.capabilities != null)
+                return false;
+        } else if (!capabilities.equals(other.capabilities))
+            return false;
+        if (requirements == null) {
+            if (other.requirements != null)
+                return false;
+        } else if (!requirements.equals(other.requirements))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "BundleResourceImpl [" + hint + "]";
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/resolver/impl/ResolveContextImpl.java b/src/main/java/org/apache/sling/feature/resolver/impl/ResolveContextImpl.java
new file mode 100644
index 0000000..91f4183
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/resolver/impl/ResolveContextImpl.java
@@ -0,0 +1,96 @@
+/*
+ * 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.sling.feature.resolver.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import org.osgi.resource.Wiring;
+import org.osgi.service.resolver.HostedCapability;
+import org.osgi.service.resolver.ResolveContext;
+
+/**
+ * Implementation of the OSGi ResolveContext for use with the OSGi Resolver.
+ */
+public class ResolveContextImpl extends ResolveContext {
+    private final Resource bundle;
+    private final Collection<Resource> availableResources;
+
+    /**
+     * Constructor.
+     * @param mainResource The main resource to resolve.
+     * @param available The available resources to provide dependencies.
+     */
+    public ResolveContextImpl(Resource mainResource, Collection<Resource> available) {
+        bundle = mainResource;
+        availableResources = available;
+    }
+
+    @Override
+    public Collection<Resource> getMandatoryResources() {
+        return Collections.singleton(bundle);
+    }
+
+    @Override
+    public List<Capability> findProviders(Requirement requirement) {
+        List<Capability> providers = new ArrayList<>();
+
+        String f = requirement.getDirectives().get("filter");
+        try {
+            Filter filter = FrameworkUtil.createFilter(f);
+            for (Resource r : availableResources) {
+                for (Capability c : r.getCapabilities(requirement.getNamespace())) {
+                    if (filter.matches(c.getAttributes())) {
+                        providers.add(c);
+                    }
+                }
+            }
+        } catch (InvalidSyntaxException e) {
+            throw new RuntimeException("Invalid filter " + f + " in requirement " + requirement);
+        }
+
+        return providers;
+    }
+
+    @Override
+    public int insertHostedCapability(List<Capability> capabilities, HostedCapability hostedCapability) {
+        capabilities.add(0, hostedCapability);
+        return 0;
+    }
+
+    @Override
+    public boolean isEffective(Requirement requirement) {
+        String eff = requirement.getDirectives().get("effective");
+        if (eff == null)
+            return true; // resolve is the default
+        return "resolve".equals(eff.trim());
+    }
+
+    @Override
+    public Map<Resource, Wiring> getWirings() {
+        return Collections.emptyMap();
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/resolver/FrameworkResolverTest.java b/src/test/java/org/apache/sling/feature/resolver/FrameworkResolverTest.java
new file mode 100644
index 0000000..3f28644
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/resolver/FrameworkResolverTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.sling.feature.resolver;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.io.FileReader;
+import java.net.URL;
+import java.nio.file.FileVisitOption;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.process.FeatureResolver;
+import org.apache.sling.feature.support.ArtifactHandler;
+import org.apache.sling.feature.support.ArtifactManager;
+import org.apache.sling.feature.support.ArtifactManagerConfig;
+import org.apache.sling.feature.support.json.FeatureJSONReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.Constants;
+
+public class FrameworkResolverTest {
+    private Path tempDir;
+
+    @Before
+    public void setup() throws Exception {
+        tempDir = Files.createTempDirectory(getClass().getSimpleName());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Delete the temp dir again
+        Files.walk(tempDir, FileVisitOption.FOLLOW_LINKS)
+            .sorted(Comparator.reverseOrder())
+            .map(Path::toFile)
+            .forEach(File::delete);
+    }
+
+    private Map<String, String> getFrameworkProps() {
+        return Collections.singletonMap(Constants.FRAMEWORK_STORAGE, tempDir.toFile().getAbsolutePath());
+    }
+
+    @Test
+    public void testResolveEmptyFeatureList() throws Exception {
+        ArtifactManager am = ArtifactManager.getArtifactManager(new ArtifactManagerConfig());
+        try (FeatureResolver fr = new FrameworkResolver(am, getFrameworkProps())) {
+            assertEquals(Collections.emptyList(),
+                    fr.orderFeatures(Collections.emptyList()));
+        }
+    }
+
+    @Test
+    public void testOrderFeatures() throws Exception {
+        ArtifactManager am = ArtifactManager.getArtifactManager(new ArtifactManagerConfig());
+
+        Feature f1 = readFeature("/feature1.json", am);
+        Feature f2 = readFeature("/feature2.json", am);
+        Feature f3 = readFeature("/feature3.json", am);
+
+        try (FeatureResolver fr = new FrameworkResolver(am, getFrameworkProps())) {
+            List<Feature> ordered = fr.orderFeatures(Arrays.asList(f1, f2, f3));
+            List<Feature> expected = Arrays.asList(f3, f2, f1);
+            assertEquals(expected, ordered);
+        }
+    }
+
+    private Feature readFeature(final String res,
+            final ArtifactManager artifactManager) throws Exception {
+        URL url = getClass().getResource(res);
+        String file = new File(url.toURI()).getAbsolutePath();
+        final ArtifactHandler featureArtifact = artifactManager.getArtifactHandler(file);
+
+        try (final FileReader r = new FileReader(featureArtifact.getFile())) {
+            final Feature f = FeatureJSONReader.read(r, featureArtifact.getUrl());
+            return f;
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/resolver/impl/BundleResourceImplTest.java b/src/test/java/org/apache/sling/feature/resolver/impl/BundleResourceImplTest.java
new file mode 100644
index 0000000..08deb2c
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/resolver/impl/BundleResourceImplTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.sling.feature.resolver.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.OSGiCapability;
+import org.apache.sling.feature.OSGiRequirement;
+import org.apache.sling.feature.analyser.BundleDescriptor;
+import org.apache.sling.feature.analyser.Descriptor;
+import org.apache.sling.feature.analyser.impl.BundleDescriptorImpl;
+import org.apache.sling.feature.support.util.PackageInfo;
+import org.junit.Test;
+import org.osgi.framework.Version;
+import org.osgi.framework.namespace.BundleNamespace;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+
+public class BundleResourceImplTest {
+    @Test
+    public void testResource() {
+        Map<String, List<Capability>> caps = new HashMap<>();
+
+        Capability c1 = new OSGiCapability("ns.1",
+                Collections.singletonMap("ns.1", "c1"), Collections.emptyMap());
+        Capability c2 = new OSGiCapability("ns.1",
+                Collections.singletonMap("ns.1", "c2"), Collections.emptyMap());
+        List<Capability> capLst1 = Arrays.asList(c1, c2);
+        caps.put("ns.1", capLst1);
+        Capability c3 = new OSGiCapability("ns.2",
+                Collections.singletonMap("ns.2", "c3"), Collections.emptyMap());
+        List<Capability> capLst2 = Collections.singletonList(c3);
+        caps.put("ns.2", capLst2);
+
+        Requirement r1 = new OSGiRequirement("ns.1",
+                Collections.emptyMap(), Collections.singletonMap("mydir", "myvalue"));
+        List<Requirement> reqList = Collections.singletonList(r1);
+        Resource res = new BundleResourceImpl(caps,
+                Collections.singletonMap("ns.1", reqList));
+
+        assertEquals(0, res.getCapabilities("nonexistent").size());
+        assertEquals(0, res.getRequirements("ns.2").size());
+        assertEquals(capLst1, res.getCapabilities("ns.1"));
+        assertEquals(reqList, res.getRequirements("ns.1"));
+
+        List<Capability> mergedCaps = res.getCapabilities(null);
+        assertEquals(3, mergedCaps.size());
+        assertTrue(mergedCaps.containsAll(capLst1));
+        assertTrue(mergedCaps.containsAll(capLst2));
+        assertEquals(reqList, res.getRequirements(null));
+    }
+
+    @Test
+    public void testBundleResource() throws Exception {
+        ArtifactId id = new ArtifactId("grp", "art", "1.2.3", null, null);
+        Artifact artifact = new Artifact(id);
+
+        PackageInfo ex1 = new PackageInfo("org.foo.a", "0.0.1.SNAPSHOT", false);
+        Set<PackageInfo> pkgs = Collections.singleton(ex1);
+        Set<Requirement> reqs = Collections.emptySet();
+        Set<Capability> caps = Collections.emptySet();
+        BundleDescriptor bd = new BundleDescriptorImpl(artifact, pkgs, reqs, caps);
+
+        setField(Descriptor.class, "locked", bd, false); // Unlock the Bundle Descriptor for the test
+        PackageInfo im1 = new PackageInfo("org.bar", "[1,2)", false);
+        PackageInfo im2 = new PackageInfo("org.tar", null, true);
+        bd.getImportedPackages().add(im1);
+        bd.getImportedPackages().add(im2);
+
+        Resource res = new BundleResourceImpl(bd);
+        assertNotNull(
+                getCapAttribute(res, BundleNamespace.BUNDLE_NAMESPACE, BundleNamespace.BUNDLE_NAMESPACE));
+        assertEquals(new Version("1.2.3"),
+                getCapAttribute(res, BundleNamespace.BUNDLE_NAMESPACE, BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE));
+
+        List<Capability> exports = res.getCapabilities(PackageNamespace.PACKAGE_NAMESPACE);
+        assertEquals(1, exports.size());
+        assertEquals("org.foo.a",
+                getCapAttribute(res, PackageNamespace.PACKAGE_NAMESPACE, PackageNamespace.PACKAGE_NAMESPACE));
+        assertEquals(new Version("0.0.1.SNAPSHOT"),
+                getCapAttribute(res, PackageNamespace.PACKAGE_NAMESPACE, PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE));
+        assertEquals(getCapAttribute(res, BundleNamespace.BUNDLE_NAMESPACE, BundleNamespace.BUNDLE_NAMESPACE),
+                getCapAttribute(res, PackageNamespace.PACKAGE_NAMESPACE, PackageNamespace.CAPABILITY_BUNDLE_SYMBOLICNAME_ATTRIBUTE));
+        assertEquals(new Version("1.2.3"),
+                getCapAttribute(res, PackageNamespace.PACKAGE_NAMESPACE, PackageNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE));
+
+        List<Requirement> requirements = res.getRequirements(PackageNamespace.PACKAGE_NAMESPACE);
+        assertEquals(2, requirements.size());
+
+        Requirement reqBar = null;
+        Requirement reqTar = null;
+        for (Requirement req : requirements) {
+            if (req.getDirectives().get("filter").contains("org.bar"))
+                reqBar = req;
+            else
+                reqTar = req;
+        }
+
+        assertEquals(1, reqBar.getDirectives().size());
+        assertEquals("(&(osgi.wiring.package=org.bar)(&(version>=1.0.0)(!(version>=2.0.0))))",
+                reqBar.getDirectives().get(PackageNamespace.REQUIREMENT_FILTER_DIRECTIVE));
+
+        assertEquals(2, reqTar.getDirectives().size());
+        assertEquals("(&(osgi.wiring.package=org.tar))",
+                reqTar.getDirectives().get(PackageNamespace.REQUIREMENT_FILTER_DIRECTIVE));
+        assertEquals(PackageNamespace.RESOLUTION_OPTIONAL,
+                reqTar.getDirectives().get(PackageNamespace.REQUIREMENT_RESOLUTION_DIRECTIVE));
+    }
+
+    @Test
+    public void testBundleResourceGenericCapReq() throws Exception {
+        ArtifactId id = new ArtifactId("org.apache", "org.apache.someartifact", "0.0.0", null, null);
+        Artifact artifact = new Artifact(id);
+
+        Capability cap = new OSGiCapability("org.example.cap1",
+                Collections.singletonMap("intAttr", 999),
+                Collections.singletonMap("somedir", "mydir"));
+        Set<Capability> caps = Collections.singleton(cap);
+
+        Requirement req1 = new OSGiRequirement("org.example.req1",
+                Collections.singletonMap("boolAttr", true),
+                Collections.singletonMap("adir", "aval"));
+        Requirement req2 = new OSGiRequirement("org.example.req2",
+                Collections.singletonMap("boolAttr", false),
+                Collections.singletonMap("adir", "aval2"));
+        Set<Requirement> reqs = new HashSet<>(Arrays.asList(req1, req2));
+        BundleDescriptorImpl bd = new BundleDescriptorImpl(artifact, Collections.emptySet(), reqs, caps);
+
+        Resource res = new BundleResourceImpl(bd);
+
+        assertEquals(caps, new HashSet<>(res.getCapabilities("org.example.cap1")));
+        assertEquals(Collections.singleton(req1),
+                new HashSet<>(res.getRequirements("org.example.req1")));
+        assertEquals(Collections.singleton(req2),
+                new HashSet<>(res.getRequirements("org.example.req2")));
+        assertEquals(reqs, new HashSet<>(res.getRequirements(null)));
+    }
+
+    private Object getCapAttribute(Resource res, String ns, String attr) {
+        List<Capability> caps = res.getCapabilities(ns);
+        if (caps.size() == 0)
+            return null;
+
+        Capability cap = caps.iterator().next();
+        return cap.getAttributes().get(attr);
+    }
+
+    private void setField(Class<?> cls, String field, Object obj, Object val) throws Exception {
+        Field f = cls.getDeclaredField(field);
+        f.setAccessible(true);
+        f.set(obj, val);
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/resolver/impl/ResolveContextImplTest.java b/src/test/java/org/apache/sling/feature/resolver/impl/ResolveContextImplTest.java
new file mode 100644
index 0000000..b63d14b
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/resolver/impl/ResolveContextImplTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.sling.feature.resolver.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.feature.OSGiCapability;
+import org.apache.sling.feature.OSGiRequirement;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.Version;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+import org.osgi.service.resolver.HostedCapability;
+import org.osgi.service.resolver.ResolveContext;
+
+public class ResolveContextImplTest {
+    @Test
+    public void testMandatory() {
+        Resource mainRes = new BundleResourceImpl(Collections.emptyMap(), Collections.emptyMap());
+        List<Resource> available = Arrays.asList();
+        ResolveContext ctx = new ResolveContextImpl(mainRes, available);
+
+        assertEquals(Collections.singleton(mainRes), ctx.getMandatoryResources());
+    }
+
+    @Test
+    public void testFindProviders() {
+        Resource res1 = exportBundle("org.foo", "2");
+        Resource res2 = exportBundle("org.bar", "1.2");
+        Resource res3 = exportBundle("org.foo", "1.0.0.TESTING");
+        Resource res4 = exportBundle("org.foo", "1.9");
+
+        Resource mainRes = new BundleResourceImpl(Collections.emptyMap(), Collections.emptyMap());
+        List<Resource> available = Arrays.asList(res1, res2, res3, res4);
+        ResolveContext ctx = new ResolveContextImpl(mainRes, available);
+
+        Requirement req = new OSGiRequirement(PackageNamespace.PACKAGE_NAMESPACE,
+                Collections.emptyMap(),
+                Collections.singletonMap("filter",
+                        "(&(osgi.wiring.package=org.foo)(&(version>=1.0.0)(!(version>=2.0.0))))"));
+
+        List<Capability> expected = new ArrayList<>();
+        expected.addAll(res3.getCapabilities(null));
+        expected.addAll(res4.getCapabilities(null));
+        List<Capability> providers = ctx.findProviders(req);
+        assertEquals(expected, providers);
+    }
+
+    private Resource exportBundle(String pkg, String version) {
+        Map<String, Object> attrs = new HashMap<>();
+        attrs.put(PackageNamespace.PACKAGE_NAMESPACE, pkg);
+        attrs.put(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, new Version(version));
+        Capability cap = new OSGiCapability(PackageNamespace.PACKAGE_NAMESPACE,
+                attrs, Collections.emptyMap());
+        return new BundleResourceImpl(
+                Collections.singletonMap(PackageNamespace.PACKAGE_NAMESPACE,
+                        Collections.singletonList(cap)),
+                Collections.emptyMap());
+    }
+
+    @Test
+    public void testInsertHostedCapability() {
+        ResolveContext ctx = new ResolveContextImpl(Mockito.mock(Resource.class),
+                Collections.emptyList());
+
+        Capability cap1 =
+                new OSGiCapability("abc1", Collections.emptyMap(), Collections.emptyMap());
+        Capability cap2 =
+                new OSGiCapability("abc2", Collections.emptyMap(), Collections.emptyMap());
+        List<Capability> caps = new ArrayList<>();
+        caps.add(cap1);
+        caps.add(cap2);
+
+        HostedCapability hc = Mockito.mock(HostedCapability.class);
+        int idx = ctx.insertHostedCapability(caps, hc);
+        assertSame(hc, caps.get(idx));
+        assertEquals(3, caps.size());
+    }
+
+    @Test
+    public void testEffectiveRequirement() {
+        ResolveContext ctx = new ResolveContextImpl(Mockito.mock(Resource.class),
+                Collections.emptyList());
+
+        Map<String, String> dirs = new HashMap<>();
+        dirs.put("filter", "(somekey=someval)");
+        dirs.put("effective", "resolve ");
+        Requirement ereq1 = new OSGiRequirement(PackageNamespace.PACKAGE_NAMESPACE,
+                Collections.emptyMap(), dirs);
+        assertTrue(ctx.isEffective(ereq1));
+
+        Requirement ereq2 = new OSGiRequirement(PackageNamespace.PACKAGE_NAMESPACE,
+                Collections.emptyMap(),
+                Collections.singletonMap("filter", "(a=b)"));
+        assertTrue(ctx.isEffective(ereq2));
+
+        Requirement req3 = new OSGiRequirement(PackageNamespace.PACKAGE_NAMESPACE,
+                Collections.emptyMap(),
+                Collections.singletonMap("effective", "active"));
+        assertFalse(ctx.isEffective(req3));
+    }
+}
\ No newline at end of file
diff --git a/src/test/resources/feature1.json b/src/test/resources/feature1.json
new file mode 100644
index 0000000..80288e5
--- /dev/null
+++ b/src/test/resources/feature1.json
@@ -0,0 +1,5 @@
+{
+    "id": "org.apache.sling.test.features/feature1/1.0.0",
+    "bundles": 
+        ["org.apache.sling/org.apache.sling.commons.logservice/1.0.6"]
+}
\ No newline at end of file
diff --git a/src/test/resources/feature2.json b/src/test/resources/feature2.json
new file mode 100644
index 0000000..53a8f98
--- /dev/null
+++ b/src/test/resources/feature2.json
@@ -0,0 +1,5 @@
+{
+    "id": "org.apache.sling.test.features/feature2/1.0.0",
+    "bundles": 
+        ["org.slf4j/slf4j-api/1.7.25"]
+}
\ No newline at end of file
diff --git a/src/test/resources/feature3.json b/src/test/resources/feature3.json
new file mode 100644
index 0000000..33a09a8
--- /dev/null
+++ b/src/test/resources/feature3.json
@@ -0,0 +1,5 @@
+{
+    "id": "org.apache.sling.test.features/feature3/1.0.0",
+    "bundles": 
+        ["org.slf4j/slf4j-simple/1.7.25"]
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
davidb@apache.org.