diff --git a/bundles/org.eclipse.osgi.tests/META-INF/MANIFEST.MF b/bundles/org.eclipse.osgi.tests/META-INF/MANIFEST.MF index 48e1b899f23..14ce523fdbf 100644 --- a/bundles/org.eclipse.osgi.tests/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.osgi.tests/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Core OSGi Tests Bundle-SymbolicName: org.eclipse.osgi.tests;singleton:=true -Bundle-Version: 3.19.100.qualifier +Bundle-Version: 3.19.200.qualifier Bundle-Vendor: Eclipse.org Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.29.0,4.0.0)", diff --git a/bundles/org.eclipse.osgi.tests/pom.xml b/bundles/org.eclipse.osgi.tests/pom.xml index d3e9d4bab10..beabd708d5f 100644 --- a/bundles/org.eclipse.osgi.tests/pom.xml +++ b/bundles/org.eclipse.osgi.tests/pom.xml @@ -19,7 +19,7 @@ org.eclipse.osgi org.eclipse.osgi.tests - 3.19.100-SNAPSHOT + 3.19.200-SNAPSHOT eclipse-test-plugin diff --git a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/container/TestModuleContainer.java b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/container/TestModuleContainer.java index 4bd75eee64c..22f0c87774d 100644 --- a/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/container/TestModuleContainer.java +++ b/bundles/org.eclipse.osgi.tests/src/org/eclipse/osgi/tests/container/TestModuleContainer.java @@ -3959,7 +3959,8 @@ protected void assertNotMoreThanPermutationCreated(ResolutionReport report, fail("Maximum of " + max + " permutations expected but was " + permutations); } else if (permutations < max) { System.out.println( - "## Permutations are below the threshold, consider adjusting the testcase to assert the lower count!"); + "## Permutations (" + permutations + ") are below the threshold (" + max + + "), consider adjusting the testcase to assert the lower count!"); } return; } @@ -4367,8 +4368,8 @@ private static void assertWires(List required, List... p public void testLocalUseConstraintViolations() throws Exception { ResolutionReport result = resolveTestSet("set1"); // TODO get down permutation count! - assertSucessfulWith(result, 52); - assertNotMoreThanPermutationCreated(result, ResolutionReport::getSubstitutionPermutations, 23); + assertSucessfulWith(result, 49); + assertNotMoreThanPermutationCreated(result, ResolutionReport::getSubstitutionPermutations, 20); } private ResolutionReport resolveTestSet(String name) throws Exception { diff --git a/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF b/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF index 5c4ecd9f507..d5482329f91 100644 --- a/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF @@ -107,7 +107,7 @@ Bundle-Activator: org.eclipse.osgi.internal.framework.SystemBundleActivator Bundle-Description: %systemBundle Bundle-Copyright: %copyright Bundle-Vendor: %eclipse.org -Bundle-Version: 3.20.0.qualifier +Bundle-Version: 3.20.100.qualifier Bundle-Localization: systembundle Bundle-DocUrl: http://www.eclipse.org Eclipse-ExtensibleAPI: true diff --git a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Candidates.java b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Candidates.java index 0b59f6dac0b..f4dc399f28f 100644 --- a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Candidates.java +++ b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Candidates.java @@ -18,6 +18,7 @@ */ package org.apache.felix.resolver; +import java.io.PrintStream; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicBoolean; @@ -34,6 +35,8 @@ class Candidates { + private static final boolean FILTER_USES = Boolean + .parseBoolean(System.getProperty("felix.resolver.candidates.filteruses", "true")); static class PopulateResult { boolean success; ResolutionError error; @@ -709,7 +712,7 @@ public Capability getFirstCandidate(Requirement req) return null; } - public void removeFirstCandidate(Requirement req) + public Capability removeFirstCandidate(Requirement req) { CandidateSelector candidates = m_candidateMap.get(req); // Remove the conflicting candidate. @@ -721,6 +724,7 @@ public void removeFirstCandidate(Requirement req) // Update the delta with the removed capability CopyOnWriteSet capPath = m_delta.getOrCompute(req); capPath.add(cap); + return cap; } public CandidateSelector clearMultipleCardinalityCandidates(Requirement req, Collection caps) @@ -1145,7 +1149,15 @@ public Candidates copy() m_delta.deepClone()); } - public void dump(ResolveContext rc) + /** + * Dump the current candidate set to system out + * + * @param rc the resolve context that should be used to look for existing + * wirings + * @param all if true all requirements are printed, if false only those that + * have more than one provider + */ + public void dump(ResolveContext rc, boolean all, PrintStream printStream) { // Create set of all revisions from requirements. Set resources = new CopyOnWriteSet(); @@ -1155,36 +1167,59 @@ public void dump(ResolveContext rc) resources.add(entry.getKey().getResource()); } // Now dump the revisions. - System.out.println("=== BEGIN CANDIDATE MAP ==="); + printStream.println("=== BEGIN CANDIDATE MAP ==="); for (Resource resource : resources) { - Wiring wiring = rc.getWirings().get(resource); - System.out.println(" " + resource - + " (" + ((wiring != null) ? "RESOLVED)" : "UNRESOLVED)")); - List reqs = (wiring != null) - ? wiring.getResourceRequirements(null) + dumpResource(resource, rc, all, printStream); + } + printStream.println("=== END CANDIDATE MAP ==="); + } + + protected void dumpResource(Resource resource, ResolveContext rc, boolean all, PrintStream printStream) { + Wiring wiring = rc == null ? null : rc.getWirings().get(resource); + List reqs = (wiring != null) ? wiring.getResourceRequirements(null) : resource.getRequirements(null); - for (Requirement req : reqs) - { - CandidateSelector candidates = m_candidateMap.get(req); - if ((candidates != null) && (!candidates.isEmpty())) - { - System.out.println(" " + req + ": " + candidates); + List dreqs = (wiring != null) ? Util.getDynamicRequirements(wiring.getResourceRequirements(null)) + : Util.getDynamicRequirements(resource.getRequirements(null)); + boolean hasMulti = hasMulti(reqs); + printStream.println(" " + (hasMulti ? "[?]" : "[!]") + Util.getResourceName(resource) + " (" + + ((wiring != null) ? "RESOLVED)" : "UNRESOLVED)")); + if (all || hasMulti) { + printRe(reqs, printStream, all); + printRe(dreqs, printStream, all); + } + } + + private boolean hasMulti(List reqs) { + for (Requirement req : reqs) { + CandidateSelector candidates = m_candidateMap.get(req); + if ((candidates != null) && (!candidates.isEmpty())) { + List remaining = candidates.getRemainingCandidates(); + if (remaining.size() > 1) { + return true; } } - reqs = (wiring != null) - ? Util.getDynamicRequirements(wiring.getResourceRequirements(null)) - : Util.getDynamicRequirements(resource.getRequirements(null)); - for (Requirement req : reqs) - { - CandidateSelector candidates = m_candidateMap.get(req); - if ((candidates != null) && (!candidates.isEmpty())) - { - System.out.println(" " + req + ": " + candidates); + } + return false; + } + + protected int printRe(List reqs, PrintStream printStream, boolean all) { + int dup = 0; + for (Requirement req : reqs) { + CandidateSelector candidates = m_candidateMap.get(req); + if ((candidates != null) && (!candidates.isEmpty())) { + List remaining = candidates.getRemainingCandidates(); + boolean hasMulti = remaining.size() > 1; + if (all || hasMulti) { + dup++; + printStream.println(" " + (hasMulti ? "[?]" : "[!]") + Util.toString(req) + ": "); + for (Capability cap : remaining) { + printStream.println(" " + Util.toString(cap)); + } } } } - System.out.println("=== END CANDIDATE MAP ==="); + return dup; } public Candidates permutate(Requirement req) @@ -1192,7 +1227,10 @@ public Candidates permutate(Requirement req) if (!Util.isMultiple(req) && canRemoveCandidate(req)) { Candidates perm = copy(); - perm.removeFirstCandidate(req); + Capability removed = perm.removeFirstCandidate(req); + if (FILTER_USES) { + ProblemReduction.removeUsesViolations(perm, req); + } return perm; } return null; @@ -1341,4 +1379,25 @@ public ResolutionException toException() { } + /** + * Returns the current provided {@link Capability} for the given resource if it + * is a candidate for the {@link Requirement} + * + * @param resource the resource to check + * @param requirement the requirement to check + * @return the {@link Capability} this Resource currently provides for the given + * {@link Requirement} or null if none is provided. + */ + public Capability getCapability(Resource resource, Requirement requirement) { + List providers = getCandidates(requirement); + if (providers != null) { + for (Capability capability : providers) { + if (capability.getResource().equals(resource)) { + return capability; + } + } + } + return null; + } + } diff --git a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/ProblemReduction.java b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/ProblemReduction.java new file mode 100644 index 00000000000..d1c954126ff --- /dev/null +++ b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/ProblemReduction.java @@ -0,0 +1,121 @@ +/* + * 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.resolver; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import org.eclipse.osgi.container.ModuleContainer; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +/** + * The idea of the {@link ProblemReduction} class is to strike out + * {@link Capability}s that might satisfy {@link Requirement}s but violates some + * contracts that would lead to a guaranteed unresolvable state. + */ +class ProblemReduction { + + private static final Capability[] EMPTY_CAPABILITIES = new Capability[0]; + + private static final boolean DEBUG_VIOLATES = false; + + /** + * Removes all violating providers for a given {@link Requirement} and + * {@link Candidates} in a local search, that is if the requirement has any uses + * it checks if there are other packages used by this one and removes any + * offending providers from the top of the list. + * + * @param candidates candidates to filter + * @param requirement the requirement where the search should start + * @return a list of Candidates that where dropped as part of the filtering + */ + static List removeUsesViolations(Candidates candidates, Requirement requirement) { + Resource targetResource = requirement.getResource(); + // fetch the current candidate for this requirement + Capability currentCandidate = candidates.getFirstCandidate(requirement); + Resource candidateResource = currentCandidate.getResource(); + // now check if it has any uses constraints + Set uses = new TreeSet<>(Util.getUses(currentCandidate)); + if (uses.isEmpty()) { + // there is nothing this one can conflict in this current set of candidates + return Collections.emptyList(); + } + if (DEBUG_VIOLATES) { + System.out.println("=== remove uses violations for " + ModuleContainer.toString(requirement)); + System.out.println("== current candidate is " + ModuleContainer.toString(currentCandidate)); + candidates.dumpResource(targetResource, null, true, System.out); + } + boolean repeat; + int round = 0; + List dropped = new ArrayList<>(); + do { + repeat = false; + round++; + if (DEBUG_VIOLATES) { + System.out.println("Round " + round + ":"); + for (String usedPackage : uses) { + System.out.println(" uses: " + usedPackage); + } + } + // now look at all other imports of the target resource if it is a package that + // is part of a used package + for (Requirement packageRequirement : targetResource.getRequirements(PackageNamespace.PACKAGE_NAMESPACE)) { + if (packageRequirement == requirement) { + continue; + } + Capability providedPackage = candidates.getCapability(candidateResource, packageRequirement); + if (providedPackage == null) { + // we do not provide anything for this package + continue; + } + if (uses.contains(Util.getPackageName(providedPackage))) { + // this is a package where we are a candidate and that has a uses constraint, so + // this package must be provided by us as well or we run into a uses-violation + // later on! + Capability capability = removeViolators(candidates, candidateResource, packageRequirement, dropped); + // if we have added any additional uses we need to reiterate... + repeat |= uses.addAll(Util.getUses(capability)); + } + } + } while (repeat); + if (DEBUG_VIOLATES && !dropped.isEmpty()) { + System.out.println(); + System.out.println("After removal (" + dropped.size() + " dropped)"); + candidates.dumpResource(targetResource, null, true, System.out); + System.out.println(); + } + return dropped; + } + + private static Capability removeViolators(Candidates candidates, Resource candidateResource, + Requirement packageRequirement, List dropped) { + Capability capability; + while ((capability = candidates.getFirstCandidate(packageRequirement)).getResource() != candidateResource) { + dropped.add(candidates.copy()); + candidates.removeFirstCandidate(packageRequirement); + } + return capability; + } + +} diff --git a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Util.java b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Util.java index 973cd5b72c6..31487d8f97b 100644 --- a/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Util.java +++ b/bundles/org.eclipse.osgi/felix/src/org/apache/felix/resolver/Util.java @@ -19,9 +19,19 @@ package org.apache.felix.resolver; 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 java.util.Set; +import java.util.stream.Collectors; +import org.eclipse.osgi.internal.framework.FilterImpl; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.Version; import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.namespace.IdentityNamespace; import org.osgi.framework.namespace.PackageNamespace; import org.osgi.resource.Capability; @@ -58,6 +68,14 @@ public static Version getVersion(Resource resource) return null; } + public static String getResourceName(Resource resource) { + String symbolicName = getSymbolicName(resource); + if (symbolicName != null) { + return symbolicName + " " + getVersion(resource); + } + return resource.toString(); + } + public static boolean isFragment(Resource resource) { List caps = resource.getCapabilities(null); @@ -81,13 +99,13 @@ public static boolean isOptional(Requirement req) public static boolean isMultiple(Requirement req) { - return Namespace.CARDINALITY_MULTIPLE.equals(req.getDirectives() + return Namespace.CARDINALITY_MULTIPLE.equals(req.getDirectives() .get(Namespace.REQUIREMENT_CARDINALITY_DIRECTIVE)) && !isDynamic(req); } public static boolean isDynamic(Requirement req) { - return PackageNamespace.RESOLUTION_DYNAMIC.equals(req.getDirectives() + return PackageNamespace.RESOLUTION_DYNAMIC.equals(req.getDirectives() .get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE)); } @@ -115,4 +133,114 @@ public static List getDynamicRequirements(List reqs) } return result; } + + public static String getPackageName(Capability capability) { + if (capability != null && PackageNamespace.PACKAGE_NAMESPACE.equals(capability.getNamespace())) { + Object object = capability.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE); + if (object instanceof String) { + return (String) object; + } + } + return ""; + } + + public static Set getUses(Capability capability) { + if (capability != null && PackageNamespace.PACKAGE_NAMESPACE.equals(capability.getNamespace())) { + String uses = capability.getDirectives().get(PackageNamespace.CAPABILITY_USES_DIRECTIVE); + if (uses != null && !uses.isEmpty()) { + return Arrays.stream(uses.split(",")).map(String::trim).collect(Collectors.toSet()); + } + } + return Collections.emptySet(); + } + + static String toString(Requirement requirement) { + if (PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace())) { + return Constants.IMPORT_PACKAGE + ": " //$NON-NLS-1$ + + createOSGiRequirement(requirement, PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, + PackageNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); + } else if (BundleNamespace.BUNDLE_NAMESPACE.equals(requirement.getNamespace())) { + return Constants.REQUIRE_BUNDLE + ": " //$NON-NLS-1$ + + createOSGiRequirement(requirement, BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); + } else if (HostNamespace.HOST_NAMESPACE.equals(requirement.getNamespace())) { + return Constants.FRAGMENT_HOST + ": " //$NON-NLS-1$ + + createOSGiRequirement(requirement, HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); + } + return Constants.REQUIRE_CAPABILITY + ": " + requirement.toString(); //$NON-NLS-1$ + } + + private static String createOSGiRequirement(Requirement requirement, String... versions) { + Map directives = new HashMap<>(requirement.getDirectives()); + String filter = directives.remove(Namespace.REQUIREMENT_FILTER_DIRECTIVE); + if (filter == null) + throw new IllegalArgumentException("No filter directive found:" + requirement); //$NON-NLS-1$ + FilterImpl filterImpl; + try { + filterImpl = FilterImpl.newInstance(filter); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Invalid filter directive", e); //$NON-NLS-1$ + } + Map matchingAttributes = filterImpl.getStandardOSGiAttributes(versions); + String name = matchingAttributes.remove(requirement.getNamespace()); + if (name == null) + throw new IllegalArgumentException("Invalid requirement: " + requirement); //$NON-NLS-1$ + return name + toString(matchingAttributes, false, true) + toString(directives, true, true); + } + + static String toString(Map map, boolean directives) { + return toString(map, directives, false); + } + + static String toString(Map map, boolean directives, boolean stringsOnly) { + if (map.size() == 0) + return ""; //$NON-NLS-1$ + String assignment = directives ? ":=" : "="; //$NON-NLS-1$ //$NON-NLS-2$ + Set> set = map.entrySet(); + StringBuilder sb = new StringBuilder(); + for (java.util.Map.Entry entry : set) { + sb.append("; "); //$NON-NLS-1$ + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) value; + if (list.isEmpty()) + continue; + Object component = list.get(0); + String className = component.getClass().getName(); + String type = className.substring(className.lastIndexOf('.') + 1); + sb.append(key).append(':').append("List<").append(type).append(">").append(assignment).append('"'); //$NON-NLS-1$ //$NON-NLS-2$ + for (Object object : list) + sb.append(object).append(','); + sb.setLength(sb.length() - 1); + sb.append('"'); + } else { + String type = ""; //$NON-NLS-1$ + if (!(value instanceof String) && !stringsOnly) { + String className = value.getClass().getName(); + type = ":" + className.substring(className.lastIndexOf('.') + 1); //$NON-NLS-1$ + } + sb.append(key).append(type).append(assignment).append('"').append(value).append('"'); + } + } + return sb.toString(); + } + + static String toString(Capability capability) { + if (PackageNamespace.PACKAGE_NAMESPACE.equals(capability.getNamespace())) { + return Constants.EXPORT_PACKAGE + ": " + createOSGiCapability(capability); //$NON-NLS-1$ + } else if (BundleNamespace.BUNDLE_NAMESPACE.equals(capability.getNamespace())) { + return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(capability); //$NON-NLS-1$ + } else if (HostNamespace.HOST_NAMESPACE.equals(capability.getNamespace())) { + return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(capability); //$NON-NLS-1$ + } + return Constants.PROVIDE_CAPABILITY + ": " + capability.toString(); //$NON-NLS-1$ + } + + private static String createOSGiCapability(Capability cap) { + Map attributes = new HashMap<>(cap.getAttributes()); + Map directives = cap.getDirectives(); + String name = String.valueOf(attributes.remove(cap.getNamespace())); + return name + toString(attributes, false, true) + toString(directives, true, true); + } } \ No newline at end of file diff --git a/bundles/org.eclipse.osgi/pom.xml b/bundles/org.eclipse.osgi/pom.xml index 80c0d324026..6375514ebd3 100644 --- a/bundles/org.eclipse.osgi/pom.xml +++ b/bundles/org.eclipse.osgi/pom.xml @@ -19,7 +19,7 @@ org.eclipse.osgi org.eclipse.osgi - 3.20.0-SNAPSHOT + 3.20.100-SNAPSHOT eclipse-plugin diff --git a/features/org.eclipse.equinox.core.feature/feature.xml b/features/org.eclipse.equinox.core.feature/feature.xml index c426c66b33e..9b30abc82ff 100644 --- a/features/org.eclipse.equinox.core.feature/feature.xml +++ b/features/org.eclipse.equinox.core.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/features/org.eclipse.equinox.core.sdk/feature.xml b/features/org.eclipse.equinox.core.sdk/feature.xml index f07279b4f74..8c08f312424 100644 --- a/features/org.eclipse.equinox.core.sdk/feature.xml +++ b/features/org.eclipse.equinox.core.sdk/feature.xml @@ -2,7 +2,7 @@ diff --git a/features/org.eclipse.equinox.server.core/feature.xml b/features/org.eclipse.equinox.server.core/feature.xml index 1a10f83fdf6..0d4b17fe006 100644 --- a/features/org.eclipse.equinox.server.core/feature.xml +++ b/features/org.eclipse.equinox.server.core/feature.xml @@ -2,7 +2,7 @@