/*******************************************************************************
* Copyright (c) 2008, 2010 VMware Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware Inc. - initial contribution
*******************************************************************************/
package org.eclipse.virgo.kernel.userregion.internal.quasi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.service.resolver.ExportPackageDescription;
import org.eclipse.osgi.service.resolver.ImportPackageSpecification;
import org.eclipse.osgi.service.resolver.PlatformAdmin;
import org.eclipse.osgi.service.resolver.ResolverError;
import org.eclipse.osgi.service.resolver.State;
import org.eclipse.osgi.service.resolver.VersionConstraint;
import org.eclipse.osgi.service.resolver.VersionRange;
import org.eclipse.virgo.kernel.userregion.internal.equinox.UsesAnalyser;
import org.eclipse.virgo.kernel.userregion.internal.equinox.UsesAnalyser.AnalysedUsesConflict;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
/**
* Helper class that analyses resolution failures and generates a human-readable failure description.
* <p/>
*
* <strong>Concurrent Semantics</strong><br/>
*
* Threadsafe.
*
*/
public final class StandardResolutionFailureDetective implements ResolutionFailureDetective {
private final UsesAnalyser usesAnalyser = new UsesAnalyser();
private final PlatformAdmin platformAdmin;
/**
* Constructor for a new {@link ResolutionFailureDetective}.
*
* @param platformAdmin the {@link org.eclipse.osgi.launch.Equinox Equinox} {@link PlatformAdmin} service.
*/
public StandardResolutionFailureDetective(PlatformAdmin platformAdmin) {
this.platformAdmin = platformAdmin;
}
/**
* Generates a description of all the resolver errors for the supplied {@link Bundle} in the supplied {@link State}.
*/
public String generateFailureDescription(State state, BundleDescription bundleDescription, ResolverErrorsHolder resolverErrorsHolder) {
StringBuilder sb = new StringBuilder();
sb.append("Cannot resolve: ").append(bundleDescription.getSymbolicName()).append("\n");
// these resolver errors are for all unresolved bundles in the state:
ResolverError[] resolverErrors = gatherResolverErrors(bundleDescription, state);
resolverErrorsHolder.setResolverErrors(resolverErrors);
if (resolverErrors.length > 0) {
indent(sb, 1);
sb.append("Resolver report:\n");
for (ResolverError resolverError : resolverErrors) {
indent(sb, 2);
formatResolverError(resolverError, sb, state);
sb.append("\n");
}
} else {
VersionConstraint[] unsatisfiedLeaves = this.platformAdmin.getStateHelper().getUnsatisfiedLeaves(
new BundleDescription[] { bundleDescription });
if (unsatisfiedLeaves.length > 0) {
indent(sb, 1);
sb.append("Unsatisfied leaf constraints:\n");
for (VersionConstraint versionConstraint : unsatisfiedLeaves) {
if (!isOptional(versionConstraint)) {
indent(sb, 2);
formatConstraint(versionConstraint, sb);
sb.append("\n");
}
}
}
}
return sb.toString();
}
/**
* List all the resolver errors in the given state starting with those of the given bundle.
*/
private ResolverError[] gatherResolverErrors(BundleDescription bundleDescription, State state) {
Set<ResolverError> resolverErrors = new LinkedHashSet<ResolverError>();
Collections.addAll(resolverErrors, state.getResolverErrors(bundleDescription));
BundleDescription[] bundles = state.getBundles();
for (BundleDescription bd : bundles) {
if (bd != bundleDescription && !bd.isResolved()) {
Collections.addAll(resolverErrors, state.getResolverErrors(bd));
}
}
return resolverErrors.toArray(new ResolverError[resolverErrors.size()]);
}
/**
* Finds the member of <code>candidates</code> that is the nearest match to <code>match</code>.
*
* @param match the string to match against.
* @param candidates the candidates to search.
* @return the nearest match.
*/
private String nearestMatch(String match) {
Set<String> candidates = gatherExports();
int nearestDistance = Integer.MAX_VALUE;
String nearestMatch = null;
for (String candidate : candidates) {
int distance = calculateStringDistance(match, candidate);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestMatch = candidate;
}
}
return nearestMatch;
}
/**
* Calculate the distance between the given two Strings according to the Levenshtein algorithm.
*
* @param s1 the first String
* @param s2 the second String
* @return the distance value
*/
private final static int calculateStringDistance(String s1, String s2) {
if (s1.isEmpty()) {
return s2.length();
}
if (s2.isEmpty()) {
return s1.length();
}
final int s2len = s2.length();
final int s1len = s1.length();
int d[][] = new int[s1len + 1][s2len + 1];
for (int i = 0; i <= s1len; i++) {
d[i][0] = i;
}
for (int j = 0; j <= s2len; j++) {
d[0][j] = j;
}
for (int i = 1; i <= s1len; i++) {
char s_i = s1.charAt(i - 1);
for (int j = 1; j <= s2len; j++) {
int cost;
char t_j = s2.charAt(j - 1);
if (Character.toLowerCase(s_i) == Character.toLowerCase(t_j)) {
cost = 0;
} else {
cost = 1;
}
d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + cost);
}
}
return d[s1len][s2len];
}
/**
* Gathers all the package exports.
*
* @return the exported packages.
*/
private Set<String> gatherExports() {
State state = this.platformAdmin.getState(false);
ExportPackageDescription[] exportedPackages = state.getExportedPackages();
Set<String> exports = new HashSet<String>(exportedPackages.length);
for (ExportPackageDescription epd : exportedPackages) {
exports.add(epd.getName());
}
return exports;
}
private void formatResolverError(ResolverError resolverError, StringBuilder sb, State state) {
if (resolverError.getType() == ResolverError.IMPORT_PACKAGE_USES_CONFLICT) {
formatUsesConflict(resolverError, sb, state);
} else if (resolverError.getType() == ResolverError.MISSING_FRAGMENT_HOST) {
formatMissingFragment(resolverError, sb);
} else if (resolverError.getType() == ResolverError.FRAGMENT_CONFLICT) {
formatFragmentConflict(resolverError, sb, state);
} else {
formatBasicResolverError(resolverError, sb);
}
}
private void formatBasicResolverError(ResolverError resolverError, StringBuilder sb) {
sb.append(this.getTypeDescription(resolverError.getType()));
formatResolverErrorData(resolverError, sb);
formatResolverErrorUnsatisfiedConstraint(resolverError, sb);
}
private void formatResolverErrorUnsatisfiedConstraint(ResolverError resolverError, StringBuilder sb) {
VersionConstraint unsatisfiedConstraint = resolverError.getUnsatisfiedConstraint();
if (unsatisfiedConstraint != null) {
formatMissingConstraintWithAttributes(resolverError, sb, unsatisfiedConstraint);
} else {
sb.append(" In bundle <").append(resolverError.getBundle()).append(">");
}
}
private void formatMissingFragment(ResolverError resolverError, StringBuilder sb) {
sb.append(this.getTypeDescription(resolverError.getType()));
sb.append(" The affected fragment is ").append(resolverError.getBundle()).append(".");
formatResolverErrorData(resolverError, sb);
formatResolverErrorUnsatisfiedConstraint(resolverError, sb);
}
private void formatResolverErrorData(ResolverError resolverError, StringBuilder sb) {
String data = resolverError.getData();
if (data != null) {
sb.append(" Resolver error data <").append(data).append(">.");
}
}
private void formatUsesConflict(ResolverError resolverError, StringBuilder sb, State state) {
VersionConstraint unsatisfiedConstraint = resolverError.getUnsatisfiedConstraint();
BundleDescription bundle = resolverError.getBundle();
sb.append("Uses violation: <").append(unsatisfiedConstraint).append("> in bundle <").append(bundle).append("[").append(bundle.getBundleId()).append(
"]").append(">\n");
AnalysedUsesConflict[] usesConflicts = this.usesAnalyser.getUsesConflicts(state, resolverError);
if (usesConflicts == null || usesConflicts.length == 0) {
indent(sb, 3);
sb.append(" Resolver reported uses conflict for import");
formatConstraintAttributes(sb, unsatisfiedConstraint);
} else {
formatConflictsFound(sb, usesConflicts);
}
}
private void formatMissingConstraintWithAttributes(ResolverError resolverError, StringBuilder sb, VersionConstraint unsatisfiedConstraint) {
sb.append(" Caused by missing constraint in bundle <").append(resolverError.getBundle()).append(">\n");
indent(sb, 3);
sb.append(" constraint: <").append(unsatisfiedConstraint).append(">");
formatConstraintAttributes(sb, unsatisfiedConstraint);
}
private void formatConstraintAttributes(StringBuilder sb, VersionConstraint unsatisfiedConstraint) {
if (unsatisfiedConstraint instanceof ImportPackageSpecification) {
ImportPackageSpecification importPackageSpecification = (ImportPackageSpecification) unsatisfiedConstraint;
formatConstrainedBundleAttributes(sb, importPackageSpecification);
Map<?, ?> attributes = importPackageSpecification.getAttributes();
if (attributes != null && !attributes.isEmpty()) {
sb.append("\n");
indent(sb, 3);
sb.append("with attributes ").append(attributes).append("\n");
}
}
}
private void formatConstrainedBundleAttributes(StringBuilder sb, ImportPackageSpecification importPackageSpecification) {
String bundleSymbolicName = importPackageSpecification.getBundleSymbolicName();
if (null != bundleSymbolicName) {
sb.append(" constrained to bundle <").append(bundleSymbolicName).append(">");
VersionRange versionRange = importPackageSpecification.getBundleVersionRange();
if (null != versionRange) {
sb.append(" constrained bundle version range \"").append(versionRange).append("\"");
}
}
}
private void formatConflictsFound(StringBuilder sb, AnalysedUsesConflict[] usesConflicts) {
indent(sb, 3);
sb.append("Found conflicts:\n");
for (AnalysedUsesConflict conflict : usesConflicts) {
for (String line : conflict.getConflictStatement()) {
indent(sb, 4);
sb.append(line).append("\n");
}
}
}
private void formatFragmentConflict(ResolverError resolverError, StringBuilder sb, State state) {
formatMissingFragment(resolverError, sb);
List<BundleDescription> possibleHosts = findPossibleHosts(resolverError.getUnsatisfiedConstraint().getBundle().getHost(), state);
if (!possibleHosts.isEmpty()) {
sb.append("\n");
indent(sb, 3);
sb.append("Possible hosts:\n");
for (BundleDescription possibleHost : possibleHosts) {
indent(sb, 4);
sb.append(possibleHost).append(" ").append(possibleHost.isResolved() ? "(resolved)" : "(not resolved)").append("\n");
}
indent(sb, 3);
sb.append("Constraint conflict:\n");
indent(sb, 4);
sb.append(resolverError.getUnsatisfiedConstraint());
}
}
private List<BundleDescription> findPossibleHosts(VersionConstraint hostSpecification, State state) {
List<BundleDescription> possibleHosts = new ArrayList<BundleDescription>();
BundleDescription[] bundles = state.getBundles(hostSpecification.getName());
if (bundles != null) {
for (BundleDescription bundle : bundles) {
if (hostSpecification.getVersionRange().isIncluded(bundle.getVersion())) {
possibleHosts.add(bundle);
}
}
}
return possibleHosts;
}
private void formatConstraint(VersionConstraint versionConstraint, StringBuilder sb) {
String constraintInformation = versionConstraint.toString();
String bundleInQuestion = versionConstraint.getBundle().toString();
sb.append("Bundle: ").append(bundleInQuestion).append(" - ").append(constraintInformation);
if (versionConstraint instanceof ImportPackageSpecification) {
sb.append("\n");
indent(sb, 3);
sb.append("Did you mean: '").append(nearestMatch(versionConstraint.getName())).append("'?");
}
}
private boolean isOptional(VersionConstraint versionConstraint) {
if (versionConstraint instanceof ImportPackageSpecification) {
ImportPackageSpecification ips = (ImportPackageSpecification) versionConstraint;
return !ImportPackageSpecification.RESOLUTION_STATIC.equals(ips.getDirective(Constants.RESOLUTION_DIRECTIVE));
}
return false;
}
/**
* Indent the supplied {@link StringBuilder} to the supplied <code>level</code>.
*
* @param out the <code>StringBuilder</code> to indent.
* @param level the indentation level.
*/
private void indent(StringBuilder out, int level) {
for (int n = 0; n < level; n++) {
out.append(" ");
}
}
/**
* Provides a human readable string for any type of equinox resolver error. Errors are defined in
* {@link org.eclipse.osgi.service.resolver.ResolverError}
*
* @param type
* @return
*/
private String getTypeDescription(int type) {
switch (type) {
case ResolverError.MISSING_IMPORT_PACKAGE:
return "An Import-Package could not be resolved.";
case ResolverError.MISSING_REQUIRE_BUNDLE:
return "A Require-Bundle could not be resolved.";
case ResolverError.MISSING_FRAGMENT_HOST:
return "A Fragment-Host could not be resolved.";
case ResolverError.SINGLETON_SELECTION:
return "The bundle could not be resolved because another singleton bundle was selected.";
case ResolverError.FRAGMENT_CONFLICT:
return "The fragment could not be resolved because of a constraint conflict with a host, possibly because the host is already resolved.";
case ResolverError.IMPORT_PACKAGE_USES_CONFLICT:
return "An Import-Package could not be resolved because of a uses directive conflict.";
case ResolverError.REQUIRE_BUNDLE_USES_CONFLICT:
return "A Require-Bundle could not be resolved because of a uses directive conflict.";
case ResolverError.IMPORT_PACKAGE_PERMISSION:
return "An Import-Package could not be resolved because the importing bundle does not have the correct permissions to import the package.";
case ResolverError.EXPORT_PACKAGE_PERMISSION:
return "An Import-Package could not be resolved because no exporting bundle has the correct permissions to export the package.";
case ResolverError.REQUIRE_BUNDLE_PERMISSION:
return "A Require-Bundle could not be resolved because the requiring bundle does not have the correct permissions to require the bundle.";
case ResolverError.PROVIDE_BUNDLE_PERMISSION:
return "A Require-Bundle could not be resolved because no bundle with the required symbolic name has the correct permissions to provied the required symbolic name.";
case ResolverError.HOST_BUNDLE_PERMISSION:
return "A Fragment-Host could not be resolved because no bundle with the required symbolic name has the correct permissions to host a fragment.";
case ResolverError.FRAGMENT_BUNDLE_PERMISSION:
return "A Fragment-Host could not be resolved because the fragment bundle does not have the correct permissions to be a fragment.";
case ResolverError.PLATFORM_FILTER:
return "A bundle could not be resolved because a platform filter did not match the runtime environment.";
case ResolverError.MISSING_EXECUTION_ENVIRONMENT:
return "A bundle could not be resolved because the required execution enviroment did not match the runtime environment.";
case ResolverError.MISSING_GENERIC_CAPABILITY:
return "A bundle could not be resolved because the required generic capability could not be resolved.";
case ResolverError.NO_NATIVECODE_MATCH:
return "A bundle could not be resolved because no match was found for the native code specification.";
case ResolverError.INVALID_NATIVECODE_PATHS:
return "A bundle could not be resolved because the matching native code paths are invalid.";
case ResolverError.DISABLED_BUNDLE:
return "A bundle could not be resolved because the bundle was disabled.";
default:
return "Unknown Error.";
}
}
}