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.