package aQute.bnd.differ; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.Formatter; import java.util.Map; import java.util.Set; import java.util.jar.Manifest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import aQute.bnd.header.OSGiHeader; import aQute.bnd.header.Parameters; import aQute.bnd.osgi.Constants; import aQute.bnd.osgi.Descriptors; import aQute.bnd.osgi.Instructions; import aQute.bnd.osgi.Jar; import aQute.bnd.osgi.Processor; import aQute.bnd.service.diff.Delta; import aQute.bnd.service.diff.Diff; import aQute.bnd.service.diff.Diff.Ignore; import aQute.bnd.service.diff.Differ; import aQute.bnd.service.diff.Tree; import aQute.bnd.service.diff.Type; import aQute.bnd.version.Version; import aQute.libg.generics.Create; import aQute.service.reporter.Reporter; /** * This class maintains */ public class Baseline { private final static Logger logger = LoggerFactory.getLogger(Baseline.class); public static class Info { public String packageName; public Diff packageDiff; public Collection<String> providers; public Map<String,String> attributes; public Version newerVersion; public Version olderVersion; public Version suggestedVersion; public Version suggestedIfProviders; public boolean mismatch; public String warning = ""; public String reason; } public static class BundleInfo { public String bsn; public Version olderVersion; public Version newerVersion; public Version suggestedVersion; public boolean mismatch; public String reason; @Deprecated public Version version; } final Differ differ; final Reporter bnd; final BundleInfo binfo = new BundleInfo(); Diff diff; Set<Info> infos; String bsn; Version newerVersion; Version olderVersion; Version suggestedVersion; String releaseRepository; public Baseline(Reporter bnd, Differ differ) throws IOException { this.differ = differ; this.bnd = bnd; } /** * This method compares a jar to a baseline jar and returns version * suggestions if the baseline does not agree with the newer jar. The * returned set contains all the exported packages. * * @param newer * @param older * @return null if ok, otherwise a set of suggested versions for all * packages (also the ones that were ok). * @throws Exception */ public Set<Info> baseline(Jar newer, Jar older, Instructions packageFilters) throws Exception { Tree n = differ.tree(newer); Parameters nExports = getExports(newer); Tree o = differ.tree(older); Parameters oExports = getExports(older); if (packageFilters == null) packageFilters = new Instructions(); return baseline(n, nExports, o, oExports, packageFilters); } public Set<Info> baseline(Tree n, Parameters nExports, Tree o, Parameters oExports, Instructions packageFilters) throws Exception { diff = n.diff(o); Diff apiDiff = diff.get("<api>"); infos = Create.set(); bsn = getBsn(n); newerVersion = getVersion(n); olderVersion = getVersion(o); boolean firstRelease = false; if (o.get("<manifest>") == null) { firstRelease = true; if (newerVersion.equals(Version.emptyVersion)) { newerVersion = Version.ONE; } } Delta highestDelta = Delta.UNCHANGED; for (Diff pdiff : apiDiff.getChildren()) { if (pdiff.getType() != Type.PACKAGE) // Just packages continue; if (pdiff.getName().startsWith("java.")) continue; if (!packageFilters.matches(pdiff.getName())) continue; final Info info = new Info(); infos.add(info); info.reason = getRootCauses(pdiff); info.packageDiff = pdiff; info.packageName = pdiff.getName(); info.attributes = nExports.get(info.packageName); logger.debug("attrs for {} {}", info.packageName, info.attributes); info.newerVersion = getVersion(info.attributes); info.olderVersion = getVersion(oExports.get(info.packageName)); if (pdiff.getDelta() == Delta.UNCHANGED) { info.suggestedVersion = info.olderVersion; // Fix previously released package containing version qualifier if (info.olderVersion.getQualifier() != null) { info.suggestedVersion = bump(Delta.MICRO, info.olderVersion, 1, 0); info.warning += "Found package version with qualifier. Bumping micro version"; } else if (!info.newerVersion.equals(info.olderVersion)) { info.warning += "No difference but versions are not equal"; } } else if (pdiff.getDelta() == Delta.REMOVED) { info.suggestedVersion = null; } else if (pdiff.getDelta() == Delta.ADDED) { info.suggestedVersion = info.newerVersion; } else { // We have an API change info.suggestedVersion = bump(pdiff.getDelta(), info.olderVersion, 1, 0); if (info.newerVersion.compareTo(info.suggestedVersion) < 0) { info.mismatch = true; // our suggested version is smaller // than // the // old version! // We can fix some major problems by assuming // that an interface is a provider interface if (pdiff.getDelta() == Delta.MAJOR) { info.providers = Create.set(); if (info.attributes != null) info.providers.addAll(Processor.split(info.attributes.get(Constants.PROVIDER_TYPE_DIRECTIVE))); // Calculate the new delta assuming we fix all the major // interfaces // by making them providers Delta tryDelta = pdiff.getDelta(new Ignore() { public boolean contains(Diff diff) { if (diff.getType() == Type.INTERFACE && diff.getDelta() == Delta.MAJOR) { info.providers.add(Descriptors.getShortName(diff.getName())); return true; } return false; } }); if (tryDelta != Delta.MAJOR) { info.suggestedIfProviders = bump(tryDelta, info.olderVersion, 1, 0); } } } } Delta content; switch (pdiff.getDelta()) { case IGNORED : case UNCHANGED : content = Delta.UNCHANGED; break; case ADDED : content = Delta.MINOR; break; case CHANGED : // cannot happen content = Delta.MICRO; break; case MICRO : content = pdiff.getDelta(); break; case MINOR : content = pdiff.getDelta(); break; case MAJOR : content = pdiff.getDelta(); break; case REMOVED : default : content = Delta.MAJOR; break; } if (content.compareTo(highestDelta) > 0) { highestDelta = content; } } // If this is a first release, or the base has a different symbolic // name, // then the newer version must be ok. Otherwise the version bump for // bundles with the same symbolic name should be at least as big as the // biggest semantic change if (firstRelease || !bsn.equals(getBsn(o))) { suggestedVersion = newerVersion; } else { suggestedVersion = bumpBundle(highestDelta, olderVersion, 1, 0); if (suggestedVersion.compareTo(newerVersion) < 0) suggestedVersion = newerVersion; } binfo.bsn = bsn; binfo.suggestedVersion = suggestedVersion; binfo.version = binfo.olderVersion = olderVersion; binfo.newerVersion = newerVersion; if (newerVersion.getWithoutQualifier().equals(olderVersion.getWithoutQualifier())) { // We have a special case, the current and repository revisions // have the same version, this happens after a release, only want // to generate an error when they really differ. if (getDiff().getDelta() == Delta.UNCHANGED) return infos; } // Ok, now our bundle version must be >= the suggestedVersion if (newerVersion.getWithoutQualifier().compareTo(getSuggestedVersion()) < 0) { binfo.mismatch = true; binfo.reason = getRootCauses(apiDiff); } return infos; } private String getRootCauses(Diff apiDiff) { try (Formatter f = new Formatter()) { getRootCauses(f, apiDiff, ""); return f.toString(); } } /* * Calculate the root causes for this diff. This means that we descent * through diffs with the same level (MAJOR/MINOR/MICRO) until we find an * add/remove. The first add/remove is seen as the root cause. */ private void getRootCauses(Formatter f, Diff diff, String path) { for (Diff child : diff.getChildren()) { String cpath = path + "/" + child.getName(); if (child.getDelta() == diff.getDelta()) { getRootCauses(f, child, cpath); } else { if (child.getDelta() == Delta.ADDED) { f.format("+ %s\n", cpath); } else if (child.getDelta() == Delta.REMOVED) { f.format("- %s\n", cpath); } } } } /** * Gets the generated diff * * @return the diff */ public Diff getDiff() { return diff; } public Set<Info> getPackageInfos() { if (infos == null) return Collections.emptySet(); return infos; } public String getBsn() { return bsn; } public Version getSuggestedVersion() { return suggestedVersion; } public void setSuggestedVersion(Version suggestedVersion) { this.suggestedVersion = suggestedVersion; } public Version getNewerVersion() { return newerVersion; } public Version getOlderVersion() { return olderVersion; } public String getReleaseRepository() { return releaseRepository; } public void setReleaseRepository(String releaseRepository) { this.releaseRepository = releaseRepository; } private Version bump(Delta delta, Version last, int offset, int base) { switch (delta) { case UNCHANGED : return last; case MINOR : return new Version(last.getMajor(), last.getMinor() + offset, base); case MAJOR : return new Version(last.getMajor() + 1, base, base); case ADDED : return last; default : return new Version(last.getMajor(), last.getMinor(), last.getMicro() + offset); } } private Version getVersion(Map<String,String> map) { if (map == null) return Version.LOWEST; return Version.parseVersion(map.get(Constants.VERSION_ATTRIBUTE)); } private Parameters getExports(Jar jar) throws Exception { Manifest m = jar.getManifest(); if (m == null) return new Parameters(); return OSGiHeader.parseHeader(m.getMainAttributes().getValue(Constants.EXPORT_PACKAGE)); } private Version getVersion(Tree top) { Tree manifest = top.get("<manifest>"); if (manifest == null) { return Version.emptyVersion; } for (Tree tree : manifest.getChildren()) { if (tree.getName().startsWith(Constants.BUNDLE_VERSION)) { return Version.parseVersion(tree.getName().substring(15)); } } return Version.emptyVersion; } private String getBsn(Tree top) { Tree manifest = top.get("<manifest>"); if (manifest == null) { return ""; } for (Tree tree : manifest.getChildren()) { if (tree.getName().startsWith(Constants.BUNDLE_SYMBOLICNAME) && tree.getChildren().length > 0) { return tree.getChildren()[0].getName(); } } return ""; } private Version bumpBundle(Delta delta, Version last, int offset, int base) { switch (delta) { case MINOR : return new Version(last.getMajor(), last.getMinor() + offset, base); case MAJOR : return new Version(last.getMajor() + offset, base, base); case ADDED : return new Version(last.getMajor(), last.getMinor() + offset, base); default : return new Version(last.getMajor(), last.getMinor(), last.getMicro() + offset); } } public BundleInfo getBundleInfo() { return binfo; } }