biz.aQute.resolve.RunResolution Maven / Gradle / Ivy
package biz.aQute.resolve;
import java.io.File;
import java.lang.annotation.Annotation;
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.TreeMap;
import java.util.stream.Collectors;
import org.osgi.resource.Resource;
import org.osgi.resource.Wire;
import org.osgi.service.resolver.ResolutionException;
import org.osgi.service.resolver.Resolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import aQute.bnd.build.Container;
import aQute.bnd.build.Project;
import aQute.bnd.build.Workspace;
import aQute.bnd.build.model.BndEditModel;
import aQute.bnd.build.model.clauses.HeaderClause;
import aQute.bnd.build.model.clauses.VersionedClause;
import aQute.bnd.exceptions.Exceptions;
import aQute.bnd.help.Syntax;
import aQute.bnd.help.instructions.ResolutionInstructions;
import aQute.bnd.help.instructions.ResolutionInstructions.RunStartLevel;
import aQute.bnd.help.instructions.ResolutionInstructions.Runorder;
import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Processor;
import aQute.bnd.osgi.resource.ResourceUtils;
import aQute.bnd.osgi.resource.ResourceUtils.IdentityCapability;
import aQute.bnd.result.Result;
import aQute.lib.dot.DOT;
import aQute.lib.io.IO;
import aQute.lib.json.JSONCodec;
import aQute.libg.tarjan.Tarjan;
/**
* Provides a simple way to resolve a project or bndrun file with support for
* the {@link BndEditModel}. Static methods are used to resolve and then this
* class holds the different aspects of the result. Either with an exception or
* the required and optional wiring.
*
* The resolution also then provides convenience methods to link it to the
* projects
*/
public class RunResolution {
final static Logger logger = LoggerFactory.getLogger(RunResolution.class);
private static final JSONCodec JSON_CODEC = new JSONCodec();
public final Project project;
public final Processor properties;
public final Map> required;
public final Map> optional;
public final Exception exception;
public final String log;
public final RunStartLevel runstartlevel;
/**
* The main workhorse to resolve
*
* @param project used for reporting errors
* @param actualProperties the actual properties used for resolving. This
* can be the project in builders that do not use the
* {@link BndEditModel}, otherwise it is generally the
* {@link BndEditModel}.
* @param callbacks any callbacks
* @return a Resolution
*/
public static RunResolution resolve(Project project, Processor actualProperties,
Collection callbacks) {
return resolve(project, actualProperties, callbacks, null);
}
/**
* The main workhorse to resolve
*
* @param project used for reporting errors
* @param actualProperties the actual properties used for resolving. This
* can be the project in builders that do not use the
* {@link BndEditModel}, otherwise it is generally the
* {@link BndEditModel}.
* @param callbacks any callbacks
* @param resolverLogger an optional Logger for the resolve Process. The
* Logger needs to be closed manually.
* @return a Resolution
*/
public static RunResolution resolve(Project project, Processor actualProperties,
Collection callbacks, ResolverLogger resolverLogger) {
if (callbacks == null)
callbacks = Collections.emptyList();
ResolverLogger logger = resolverLogger == null ? new ResolverLogger() : resolverLogger;
try {
try {
ResolveProcess resolve = new ResolveProcess();
Resolver resolver = new BndResolver(logger);
resolve.resolveRequired(actualProperties, project, project, resolver, callbacks, logger);
return new RunResolution(project, actualProperties, resolve.getRequiredWiring(),
resolve.getOptionalWiring(), logger.getLog());
} catch (ResolutionException e) {
return new RunResolution(project, actualProperties, e, logger.getLog());
} catch (Exception e) {
return new RunResolution(project, actualProperties, e, logger.getLog());
}
} finally {
if (resolverLogger == null) {
logger.close();
}
}
}
/**
* The secondary workhorse to resolve. This can be used if there is no
* interactive editing. In that case the properties to resolve are in the
* project and we can use the project both for the domain and the
* properties.
*
* @param project used for reporting errors and the properties for the
* resolve operations
* @param callbacks any callbacks
* @return a Resolution
*/
public static RunResolution resolve(Project project, Collection callbacks) {
return resolve(project, project, callbacks);
}
RunResolution(Project project, Processor properties, Map> required,
Map> optional, String log) {
this.project = project;
this.properties = properties;
this.required = required;
this.optional = optional;
this.log = log;
this.exception = null;
this.runstartlevel = getConfig(properties);
}
RunResolution(Project project, Processor properties, Exception e, String log) {
this.project = project;
this.properties = properties;
this.exception = e;
this.log = log;
this.required = null;
this.optional = null;
this.runstartlevel = getConfig(properties);
}
/**
* Check if the resolution is ok, that is, there was no exception.
*
* @return true if there was no exception
*/
public boolean isOK() {
return exception == null;
}
/**
* Update the {@link BndEditModel} with the calculated set of runbundles.
* Use the {@link ResolutionInstructions#runstartlevel(RunStartLevel)}.order
* to order the bundles. Do not update it if the list has not changed. In
* that case return false (no changes)
*
* If the {@link ResolutionInstructions#runstartlevel(RunStartLevel)}.order
* is {@value Runorder#MERGESORTBYNAMEVERSION} then merge the list with the
* previous one.
*
* return true if the list was changed
*
* @param model the edit model to update
* @return true if there were changes
*/
public boolean updateBundles(BndEditModel model) {
if (exception != null)
throw Exceptions.duck(exception);
List newer = new ArrayList<>(nonNull(getRunBundles()));
List older = new ArrayList<>(nonNull(model.getRunBundles()));
if (newer.equals(older))
return false;
//
// For backward compatibility reason
// we merge the old case
//
if (runstartlevel.order() == Runorder.MERGESORTBYNAMEVERSION) {
older.retainAll(newer);
newer.removeAll(older);
older.addAll(newer);
newer = older;
}
model.setRunBundles(newer);
return true;
}
public void updateBundles(Project project) {
assert isOK();
String s = HeaderClause.toParameters(getRunBundles())
.toString();
project.setProperty(Constants.RUNBUNDLES, s);
}
/**
* Sort the resources based on their dependencies. Least dependent bundles
* are first. This method uses a toplogical sort. If there are cycles then
* clearly the topological sort is not perfect.
*
* @param resolution the required wiring
* @return a topologically sorted list of resources
*/
public List sortByDependencies(Map> resolution) {
Map> dependencies = new TreeMap<>(getGraph(resolution));
// make a canonical representation as input to Tarjan
// so that the output is deterministic
dependencies.entrySet()
.forEach(e -> Collections.sort(e.getValue(), ResourceUtils::compareTo));
Collection extends Collection> topologicalSort = Tarjan.tarjan(dependencies);
return topologicalSort.stream()
.map(ResourceUtils::sort)
.flatMap(Collection::stream)
.filter(r -> !ResourceUtils.isInitialRequirement(r))
.collect(Collectors.toList());
}
/**
* Turn the wiring into a a->b map.
*
* @param resolution the wiring
* @return a map where the vertices are connected to other vertices.
*/
public Map> getGraph(Map> resolution) {
Map> dependencies = new HashMap<>();
for (Resource r : resolution.keySet()) {
dependencies.put(r, new ArrayList<>());
}
resolution.values()
.stream()
.flatMap(Collection::stream)
.forEach(wire -> {
List list = dependencies.computeIfAbsent(wire.getRequirer(), k -> new ArrayList<>());
list.add(wire.getProvider());
});
return dependencies;
}
/**
* Get a list of ordered run bundles.
*
* @return a list of ordered bundles
*/
public List getRunBundles() {
List orderedResources = getOrderedResources();
List versionedClauses = ResourceUtils.toVersionedClauses(orderedResources);
int begin = runstartlevel.begin();
if (begin > 0) {
// We allow 0 to set all to 1 level
int step = Math.max(0, runstartlevel.step());
int n = begin;
for (VersionedClause vc : versionedClauses) {
vc.getAttribs()
.put(Constants.RUNBUNDLES_STARTLEVEL_ATTRIBUTE, Integer.toString(n));
n += step;
}
}
return versionedClauses;
}
/**
* Return the current -runbundles.
*
* @return a non-null string in parameters format
*/
public String getRunBundlesAsString() {
return HeaderClause.toParameters(getRunBundles())
.toString();
}
/*
* Order the resources by Runorder
*/
public List getOrderedResources(Map> resolution, Runorder runorder) {
if (resolution == null)
return Collections.emptyList();
switch (runorder) {
case LEASTDEPENDENCIESFIRST :
return sortByDependencies(resolution);
case LEASTDEPENDENCIESLAST :
List sortByDependencies = sortByDependencies(resolution);
Collections.reverse(sortByDependencies);
return sortByDependencies;
case RANDOM :
List list = new ArrayList<>(ResourceUtils.sortByNameVersion(resolution.keySet()));
Collections.shuffle(list);
return list;
case MERGESORTBYNAMEVERSION :
case SORTBYNAMEVERSION :
default :
return ResourceUtils.sortByNameVersion(resolution.keySet());
}
}
public List getOrderedResources() {
return getOrderedResources(required, runstartlevel.order());
}
public String dot(String name) {
DOT dot = new DOT<>(name, getGraph(required));
dot.prune();
int n = 0;
Collection sortByDependencies = getOrderedResources();
for (Resource r : sortByDependencies) {
IdentityCapability ic = ResourceUtils.getIdentityCapability(r);
dot.name(r, ic.osgi_identity() + "[" + n + "]");
n++;
}
return dot.render();
}
/**
* Get a list of runbundles as containers
*/
public List getContainers() throws Exception {
List containers = new ArrayList<>();
for (Resource r : required.keySet()) {
Container bundle = project.getBundle(r);
containers.add(bundle);
}
return containers;
}
private Collection nonNull(Collection rl) {
if (rl != null)
return rl;
return Collections.emptyList();
}
private RunStartLevel getConfig(Processor properties) {
ResolutionInstructions instructions = Syntax.getInstructions(properties, ResolutionInstructions.class);
return instructions.runstartlevel(new RunStartLevel() {
@Override
public int step() {
return -1;
}
@Override
public Runorder order() {
return Runorder.MERGESORTBYNAMEVERSION;
}
@Override
public int begin() {
return -1;
}
@Override
public Class extends Annotation> annotationType() {
return null;
}
});
}
public RunResolution reportException() {
if (!isOK()) {
if (exception instanceof ResolutionException) {
project.error("Resolution failed %s", exception.getMessage());
} else {
project.exception(exception, "Resolution failed unexpectedly");
}
}
return this;
}
public String report(boolean optionals) {
if (exception instanceof ResolutionException) {
return ResolveProcess.format((ResolutionException) exception, optionals);
} else if (exception != null) {
return Exceptions.toString(Exceptions.unrollCause(exception));
} else if (log != null) {
return log;
} else {
return "";
}
}
public Map> getRequired() {
return required;
}
public static class CacheDTO {
public String checksum;
public List runbundles;
}
/**
* Cache this resolution for the given project. A cache can be retrieved
* with getCached(Project project). This requires a real resolution
*/
public void cache() {
assert isOK() : "can only be called for a real resolution";
try {
RunResolution.CacheDTO dto = new RunResolution.CacheDTO();
dto.checksum = project.getChecksum();
dto.runbundles = getRunBundles();
IO.store(ff -> {
JSON_CODEC.enc()
.to(ff)
.put(dto)
.close();
}, getCacheFile(project));
} catch (Exception e) {
throw Exceptions.duck(e);
}
}
/**
* Get the run bundles from the cache and resolve if necessary. The run
* bundles
*
* @param project the project to get the run bundles from
* @param resolveIfNecessary if true, call resolve
* @return a result, where the value can be null
*/
public static Result getRunBundles(Project project, boolean resolveIfNecessary) {
try {
File f = getCacheFile(project);
if (f.isFile()) {
try {
RunResolution.CacheDTO dto = JSON_CODEC.dec()
.from(f)
.get(RunResolution.CacheDTO.class);
if (dto.checksum.equals(project.getChecksum())) {
logger.info("read cache for {}", project);
return Result.ok(HeaderClause.toParameters(dto.runbundles)
.toString());
}
} catch (Exception e) {
logger.warn("{} getRunBundles exception in reading cache {}, ignoring", project, e);
IO.delete(f);
}
}
if (!resolveIfNecessary) {
return Result.ok("");
}
logger.info("resolve {}", project);
Result r = resolve(project, project, null).asResult();
logger.debug("resolve {} {}", project, r);
return r.flatMap(rr -> {
logger.info("saving cache {}", project);
rr.cache();
return Result.ok(rr);
})
.map(RunResolution::getRunBundlesAsString);
} catch (Exception e) {
throw Exceptions.duck(e);
}
}
/**
* Return a result based on the isOk status. If OK, the result is a
* resolution, otherwise it is an error string.
*
* @return a result based on isOk
*/
public Result asResult() {
if (isOK())
return Result.ok(this);
else
return Result.err(report(false));
}
private static File getCacheFile(Project project) {
String id = IO.toSafeFileName(project.getPropertiesFile()
.getAbsolutePath());
File cache = project.getWorkspace()
.getCache("resolutions/" + id);
cache.getParentFile()
.mkdirs();
return cache;
}
public static void clearCache(Workspace ws) {
File cache = ws.getCache("resolutions");
IO.delete(cache);
}
}