Use local copy of latest bndlib code for pre-release testing purposes
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1347815 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/bundleplugin/src/main/java/aQute/bnd/differ/Baseline.java b/bundleplugin/src/main/java/aQute/bnd/differ/Baseline.java
new file mode 100644
index 0000000..eee3e27
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/differ/Baseline.java
@@ -0,0 +1,174 @@
+package aQute.bnd.differ;
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+import aQute.bnd.service.diff.*;
+import aQute.bnd.service.diff.Diff.Ignore;
+import aQute.lib.osgi.*;
+import aQute.libg.generics.*;
+import aQute.libg.header.*;
+import aQute.libg.reporter.*;
+import aQute.libg.version.*;
+
+/**
+ * This class maintains
+ *
+ */
+public class Baseline {
+
+ 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="";
+
+ }
+
+ final Differ differ;
+ final Reporter bnd;
+
+ 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 diff = n.diff(o).get("<api>");
+ Set<Info> infos = Create.set();
+
+ for (Diff pdiff : diff.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.packageDiff = pdiff;
+ info.packageName = pdiff.getName();
+ info.attributes = nExports.get(info.packageName);
+ bnd.trace("attrs for %s %s", 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;
+ if( !info.newerVersion.equals(info.olderVersion)) {
+ info.warning += "No difference but versions are equal";
+ }
+ } else if (pdiff.getDelta() == Delta.REMOVED) {
+ info.suggestedVersion = null;
+ } else if (pdiff.getDelta() == Delta.ADDED) {
+ info.suggestedVersion = Version.ONE;
+ } 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);
+ }
+ }
+ }
+ }
+ }
+ return infos;
+ }
+
+ 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));
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/differ/DiffImpl.java b/bundleplugin/src/main/java/aQute/bnd/differ/DiffImpl.java
new file mode 100644
index 0000000..7b675c2
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/differ/DiffImpl.java
@@ -0,0 +1,210 @@
+package aQute.bnd.differ;
+
+import static aQute.bnd.service.diff.Delta.*;
+
+import java.util.*;
+
+import aQute.bnd.service.diff.*;
+
+/**
+ * A DiffImpl class compares a newer Element to an older Element. The Element
+ * classes hide all the low level details. A Element class is either either
+ * Structured (has children) or it is a Leaf, it only has a value. The
+ * constructor will first build its children (if any) and then calculate the
+ * delta. Each comparable element is translated to an Element. If necessary the
+ * Element can be sub classed to provide special behavior.
+ */
+
+public class DiffImpl implements Diff, Comparable<DiffImpl> {
+
+ final Element older;
+ final Element newer;
+ final Collection<DiffImpl> children;
+ final Delta delta;
+
+ /**
+ * The transitions table defines how the state is escalated depending on the
+ * children. horizontally is the current delta and this is indexed with the
+ * child delta for each child. This escalates deltas from below up.
+ */
+ final static Delta[][] TRANSITIONS = {
+ { IGNORED, UNCHANGED, CHANGED, MICRO, MINOR, MAJOR }, // IGNORED
+ { IGNORED, UNCHANGED, CHANGED, MICRO, MINOR, MAJOR }, // UNCHANGED
+ { IGNORED, CHANGED, CHANGED, MICRO, MINOR, MAJOR }, // CHANGED
+ { IGNORED, MICRO, MICRO, MICRO, MINOR, MAJOR }, // MICRO
+ { IGNORED, MINOR, MINOR, MINOR, MINOR, MAJOR }, // MINOR
+ { IGNORED, MAJOR, MAJOR, MAJOR, MAJOR, MAJOR }, // MAJOR
+ { IGNORED, MAJOR, MAJOR, MAJOR, MAJOR, MAJOR }, // REMOVED
+ { IGNORED, MINOR, MINOR, MINOR, MINOR, MAJOR }, // ADDED
+ };
+
+ /**
+ * Compares the newer against the older, traversing the children if
+ * necessary.
+ *
+ * @param newer
+ * The newer Element
+ * @param older
+ * The older Element
+ * @param types
+ */
+ DiffImpl(Element newer, Element older) {
+ assert newer != null || older != null;
+ this.older = older;
+ this.newer = newer;
+
+ // Either newer or older can be null, indicating remove or add
+ // so we have to be very careful.
+ Element[] newerChildren = newer == null ? Element.EMPTY : newer.children;
+ Element[] olderChildren = older == null ? Element.EMPTY : older.children;
+
+ int o = 0;
+ int n = 0;
+ List<DiffImpl> children = new ArrayList<DiffImpl>();
+ while (true) {
+ Element nw = n < newerChildren.length ? newerChildren[n] : null;
+ Element ol = o < olderChildren.length ? olderChildren[o] : null;
+ DiffImpl diff;
+
+ if (nw == null && ol == null)
+ break;
+
+ if (nw != null && ol != null) {
+ // we have both sides
+ int result = nw.compareTo(ol);
+ if (result == 0) {
+ // we have two equal named elements
+ // use normal diff
+ diff = new DiffImpl(nw, ol);
+ n++;
+ o++;
+ } else if (result > 0) {
+ // we newer > older, so there is no newer == removed
+ diff = new DiffImpl(null, ol);
+ o++;
+ } else {
+ // we newer < older, so there is no older == added
+ diff = new DiffImpl(nw, null);
+ n++;
+ }
+ } else {
+ // we reached the end of one of the list
+ diff = new DiffImpl(nw, ol);
+ n++;
+ o++;
+ }
+ children.add(diff);
+ }
+
+ // make sure they're read only
+ this.children = Collections.unmodifiableCollection(children);
+ delta = getDelta(null);
+ }
+
+ /**
+ * Return the absolute delta. Also see
+ * {@link #getDelta(aQute.bnd.service.diff.Diff.Ignore)} that allows you to
+ * ignore Diff objects on the fly (and calculate their parents accordingly).
+ */
+ public Delta getDelta() {
+ return delta;
+ }
+
+ /**
+ * This getDelta calculates the delta but allows the caller to ignore
+ * certain Diff objects by calling back the ignore call back parameter. This
+ * can be useful to ignore warnings/errors.
+ */
+
+ public Delta getDelta(Ignore ignore) {
+
+ // If ignored, we just return ignore.
+ if (ignore != null && ignore.contains(this))
+ return IGNORED;
+
+ if (newer == null) {
+ return REMOVED;
+ } else if (older == null) {
+ return ADDED;
+ } else {
+ // now we're sure newer and older are both not null
+ assert newer != null && older != null;
+ assert newer.getClass() == older.getClass();
+
+ Delta local = Delta.UNCHANGED;
+
+ for (DiffImpl child : children) {
+ Delta sub = child.getDelta(ignore);
+ if (sub == REMOVED)
+ sub = child.older.remove;
+ else if (sub == ADDED)
+ sub = child.newer.add;
+
+ // The escalate method is used to calculate the default
+ // transition in the
+ // delta based on the children. In general the delta can
+ // only escalate, i.e.
+ // move up in the chain.
+
+ local = TRANSITIONS[sub.ordinal()][local.ordinal()];
+ }
+ return local;
+ }
+ }
+
+ public Type getType() {
+ return (newer == null ? older : newer).getType();
+ }
+
+ public String getName() {
+ return (newer == null ? older : newer).getName();
+ }
+
+ public Collection<? extends Diff> getChildren() {
+ return children;
+ }
+
+ public String toString() {
+ return String.format("%-10s %-10s %s", getDelta(), getType(), getName());
+ }
+
+ public boolean equals(Object other) {
+ if (other instanceof DiffImpl) {
+ DiffImpl o = (DiffImpl) other;
+ return getDelta() == o.getDelta() && getType() == o.getType()
+ && getName().equals(o.getName());
+ }
+ return false;
+ }
+
+ public int hashCode() {
+ return getDelta().hashCode() ^ getType().hashCode() ^ getName().hashCode();
+ }
+
+ public int compareTo(DiffImpl other) {
+ if (getDelta() == other.getDelta()) {
+ if (getType() == other.getType()) {
+ return getName().compareTo(other.getName());
+ } else
+ return getType().compareTo(other.getType());
+ } else
+ return getDelta().compareTo(other.getDelta());
+ }
+
+ public Diff get(String name) {
+ for (DiffImpl child : children) {
+ if (child.getName().equals(name))
+ return child;
+ }
+ return null;
+ }
+
+ public Tree getOlder() {
+ return older;
+ }
+
+ public Tree getNewer() {
+ return newer;
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/differ/DiffPluginImpl.java b/bundleplugin/src/main/java/aQute/bnd/differ/DiffPluginImpl.java
new file mode 100644
index 0000000..e3a1308
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/differ/DiffPluginImpl.java
@@ -0,0 +1,181 @@
+package aQute.bnd.differ;
+
+import static aQute.bnd.service.diff.Delta.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+import aQute.bnd.service.diff.*;
+import aQute.bnd.service.diff.Tree.Data;
+import aQute.lib.hex.*;
+import aQute.lib.io.*;
+import aQute.lib.osgi.*;
+import aQute.libg.cryptography.*;
+import aQute.libg.header.*;
+
+
+/**
+ * This Diff Plugin Implementation will compare JARs for their API (based on the
+ * Bundle Class Path and exported packages), the Manifest, and the resources.
+ * The Differences are represented in a {@link Diff} tree.
+ */
+public class DiffPluginImpl implements Differ {
+
+ /**
+ * Headers that are considered major enough to parse according to spec and
+ * compare their constituents
+ */
+ final static Set<String> MAJOR_HEADERS = new TreeSet<String>(
+ String.CASE_INSENSITIVE_ORDER);
+
+ /**
+ * Headers that are considered not major enough to be considered
+ */
+ final static Set<String> IGNORE_HEADERS = new TreeSet<String>(
+ String.CASE_INSENSITIVE_ORDER);
+
+ static {
+ MAJOR_HEADERS.add(Constants.EXPORT_PACKAGE);
+ MAJOR_HEADERS.add(Constants.IMPORT_PACKAGE);
+ MAJOR_HEADERS.add(Constants.REQUIRE_BUNDLE);
+ MAJOR_HEADERS.add(Constants.FRAGMENT_HOST);
+ MAJOR_HEADERS.add(Constants.BUNDLE_SYMBOLICNAME);
+ MAJOR_HEADERS.add(Constants.BUNDLE_LICENSE);
+ MAJOR_HEADERS.add(Constants.BUNDLE_NATIVECODE);
+ MAJOR_HEADERS.add(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT);
+ MAJOR_HEADERS.add(Constants.DYNAMICIMPORT_PACKAGE);
+
+ IGNORE_HEADERS.add(Constants.TOOL);
+ IGNORE_HEADERS.add(Constants.BND_LASTMODIFIED);
+ IGNORE_HEADERS.add(Constants.CREATED_BY);
+ }
+
+ /**
+ *
+ * @see aQute.bnd.service.diff.Differ#diff(aQute.lib.resource.Jar,
+ * aQute.lib.resource.Jar)
+ */
+ public Tree tree(File newer) throws Exception {
+ Jar jnewer = new Jar(newer);
+ try {
+ return tree(jnewer);
+ } finally {
+ jnewer.close();
+ }
+ }
+
+ /**
+ *
+ * @see aQute.bnd.service.diff.Differ#diff(aQute.lib.resource.Jar,
+ * aQute.lib.resource.Jar)
+ */
+ public Tree tree(Jar newer) throws Exception {
+ Analyzer anewer = new Analyzer();
+ try {
+ anewer.setJar(newer);
+ return tree(anewer);
+ } finally {
+ anewer.setJar((Jar) null);
+ anewer.close();
+ }
+ }
+
+ public Tree tree(Analyzer newer) throws Exception {
+ return bundleElement(newer);
+ }
+
+ /**
+ * Create an element representing a bundle from the Jar.
+ *
+ * @param infos
+ * @param jar
+ * The Jar to be analyzed
+ * @return the elements that should be compared
+ * @throws Exception
+ */
+ private Element bundleElement(Analyzer analyzer) throws Exception {
+ List<Element> result = new ArrayList<Element>();
+
+ Manifest manifest = analyzer.getJar().getManifest();
+
+ if (manifest != null) {
+ result.add(JavaElement.getAPI(analyzer));
+ result.add(manifestElement(manifest));
+ }
+ result.add(resourcesElement(analyzer.getJar()));
+ return new Element(Type.BUNDLE, analyzer.getJar().getName(), result, CHANGED, CHANGED,
+ null);
+ }
+
+ /**
+ * Create an element representing all resources in the JAR
+ *
+ * @param jar
+ * @return
+ * @throws Exception
+ */
+ private Element resourcesElement(Jar jar) throws Exception {
+ List<Element> resources = new ArrayList<Element>();
+ for (Map.Entry<String, Resource> entry : jar.getResources().entrySet()) {
+
+ InputStream in = entry.getValue().openInputStream();
+ try {
+ Digester<SHA1> digester = SHA1.getDigester();
+ IO.copy(in, digester);
+ String value = Hex.toHexString(digester.digest().digest());
+ resources
+ .add(new Element(Type.RESOURCE, entry.getKey()+"="+value, null, CHANGED, CHANGED, null));
+ } finally {
+ in.close();
+ }
+ }
+ return new Element(Type.RESOURCES, "<resources>", resources, CHANGED, CHANGED, null);
+ }
+
+
+
+
+ /**
+ * Create an element for each manifest header. There are
+ * {@link #IGNORE_HEADERS} and {@link #MAJOR_HEADERS} that will be treated
+ * differently.
+ *
+ * @param manifest
+ * @return
+ */
+
+ private Element manifestElement(Manifest manifest) {
+ List<Element> result = new ArrayList<Element>();
+
+ for (Object key : manifest.getMainAttributes().keySet()) {
+ String header = key.toString();
+ String value = manifest.getMainAttributes().getValue(header);
+ if (IGNORE_HEADERS.contains(header))
+ continue;
+
+ if (MAJOR_HEADERS.contains(header)) {
+ Parameters clauses = OSGiHeader.parseHeader(value);
+ Collection<Element> clausesDef = new ArrayList<Element>();
+ for (Map.Entry<String, Attrs> clause : clauses.entrySet()) {
+ Collection<Element> parameterDef = new ArrayList<Element>();
+ for (Map.Entry<String, String> parameter : clause.getValue().entrySet()) {
+ parameterDef.add(new Element(Type.PARAMETER, parameter.getKey() + ":" + parameter
+ .getValue(), null, CHANGED, CHANGED, null));
+ }
+ clausesDef.add(new Element(Type.CLAUSE, clause.getKey(), parameterDef,
+ CHANGED, CHANGED, null));
+ }
+ result.add(new Element(Type.HEADER, header, clausesDef, CHANGED, CHANGED, null));
+ } else {
+ result.add(new Element(Type.HEADER, header +":"+ value, null,CHANGED, CHANGED, null));
+ }
+ }
+ return new Element(Type.MANIFEST, "<manifest>", result, CHANGED, CHANGED, null);
+ }
+
+ public Tree deserialize(Data data) throws Exception {
+ return new Element(data);
+ }
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/differ/Element.java b/bundleplugin/src/main/java/aQute/bnd/differ/Element.java
new file mode 100644
index 0000000..602f95c
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/differ/Element.java
@@ -0,0 +1,144 @@
+package aQute.bnd.differ;
+
+import java.util.*;
+
+import aQute.bnd.service.diff.*;
+
+/**
+ * An element can be compared to another element of the same type. Elements with
+ * the same name and same place in the hierarchy should have the same type. The
+ * idea is that for a certain resource type you create an element (Structured or
+ * Leaf). This process is done for the newer and older resource.
+ * <p>
+ * A Leaf type has a value, comparison is rather simple in this case.
+ * <p>
+ * A Structured type has named children. The comparison between the newer and
+ * older child elements is then done on their name. Two elements with the same
+ * name are then matched.
+ * <p>
+ * The classes are prepared for extension but so far it turned out to be
+ * unnecessary.
+ */
+
+class Element implements Comparable<Element>, Tree {
+ final static Element[] EMPTY = new Element[0];
+ final Type type;
+ final String name;
+ final Delta add;
+ final Delta remove;
+ final String comment;
+ final Element[] children;
+
+ Element(Type type, String name) {
+ this(type, name, null, Delta.MINOR, Delta.MAJOR, null);
+ }
+
+ Element(Type type, String name, Element... children) {
+ this(type, name, Arrays.asList(children), Delta.MINOR, Delta.MAJOR, null);
+ }
+
+ Element(Type type, String name, Collection<? extends Element> children, Delta add,
+ Delta remove, String comment) {
+ this.type = type;
+ this.name = name;
+ this.add = add;
+ this.remove = remove;
+ this.comment = comment;
+ if (children != null && children.size() > 0) {
+ this.children = children.toArray(new Element[children.size()]);
+ Arrays.sort(this.children);
+ } else
+ this.children = EMPTY;
+ }
+
+ public Element(Data data) {
+ this.name = data.name;
+ this.type = data.type;
+ this.comment = data.comment;
+ this.add = data.add;
+ this.remove = data.rem;
+ if (data.children == null)
+ children = EMPTY;
+ else {
+ this.children = new Element[data.children.length];
+ for (int i = 0; i < children.length; i++)
+ children[i] = new Element(data.children[i]);
+ Arrays.sort(this.children);
+ }
+ }
+ public Data serialize() {
+ Data data = new Data();
+ data.type = this.type;
+ data.name = this.name;
+ data.add = this.add;
+ data.rem = this.remove;
+ data.comment = this.comment;
+ if (children.length != 0) {
+ data.children = new Data[children.length];
+ for (int i = 0; i < children.length; i++) {
+ data.children[i] = children[i].serialize();
+ }
+ }
+ return data;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ String getComment() {
+ return comment;
+ }
+
+ public int compareTo(Element other) {
+ if (type == other.type)
+ return name.compareTo(other.name);
+ else
+ return type.compareTo(other.type);
+ }
+
+ public boolean equals(Object other) {
+ if (other == null || getClass() != other.getClass())
+ return false;
+
+ return compareTo((Element) other) == 0;
+ }
+
+ public int hashCode() {
+ return type.hashCode() ^ name.hashCode();
+ }
+
+ public Tree[] getChildren() {
+ return children;
+ }
+
+ public Delta ifAdded() {
+ return add;
+ }
+
+ public Delta ifRemoved() {
+ return remove;
+ }
+
+ public Diff diff(Tree older) {
+ return new DiffImpl(this, (Element) older);
+ }
+
+ public Element get(String name) {
+ for (Element e : children) {
+ if (e.name.equals(name))
+ return e;
+ }
+ return null;
+ }
+
+ public String toString() {
+ return type + " " + name + " (" + add + "/" + remove + ")";
+ }
+
+
+}
diff --git a/bundleplugin/src/main/java/aQute/bnd/differ/JavaElement.java b/bundleplugin/src/main/java/aQute/bnd/differ/JavaElement.java
new file mode 100644
index 0000000..e9a77a5
--- /dev/null
+++ b/bundleplugin/src/main/java/aQute/bnd/differ/JavaElement.java
@@ -0,0 +1,655 @@
+package aQute.bnd.differ;
+
+import static aQute.bnd.service.diff.Delta.*;
+import static aQute.bnd.service.diff.Type.*;
+import static java.lang.reflect.Modifier.*;
+
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.concurrent.atomic.*;
+import java.util.jar.*;
+
+import aQute.bnd.annotation.*;
+import aQute.bnd.service.diff.*;
+import aQute.bnd.service.diff.Type;
+import aQute.lib.collections.*;
+import aQute.lib.osgi.*;
+import aQute.lib.osgi.Clazz.JAVA;
+import aQute.lib.osgi.Clazz.MethodDef;
+import aQute.lib.osgi.Descriptors.PackageRef;
+import aQute.lib.osgi.Descriptors.TypeRef;
+import aQute.libg.generics.*;
+import aQute.libg.header.*;
+import aQute.libg.version.Version;
+
+/**
+ * An element that compares the access field in a binary compatible way. This
+ * element is used for classes, methods, constructors, and fields. For that
+ * reason we also included the only method that uses this class as a static
+ * method.
+ * <p>
+ * Packages
+ * <ul>
+ * <li>MAJOR - Remove a public type
+ * <li>MINOR - Add a public class
+ * <li>MINOR - Add an interface
+ * <li>MINOR - Add a method to a class
+ * <li>MINOR - Add a method to a provider interface
+ * <li>MAJOR - Add a method to a consumer interface
+ * <li>MINOR - Add a field
+ * <li>MICRO - Add an annotation to a member
+ * <li>MINOR - Change the value of a constant
+ * <li>MICRO - -abstract
+ * <li>MICRO - -final
+ * <li>MICRO - -protected
+ * <li>MAJOR - +abstract
+ * <li>MAJOR - +final
+ * <li>MAJOR - +protected
+ * </ul>
+ *
+ */
+
+class JavaElement {
+ final static EnumSet<Type> INHERITED = EnumSet.of(FIELD, METHOD, EXTENDS,
+ IMPLEMENTS);
+ private static final Element PROTECTED = new Element(ACCESS, "protected", null,
+ MAJOR, MINOR, null);
+ private static final Element STATIC = new Element(ACCESS, "static", null,
+ MAJOR, MAJOR, null);
+ private static final Element ABSTRACT = new Element(ACCESS, "abstract", null,
+ MAJOR, MINOR, null);
+ private static final Element FINAL = new Element(ACCESS, "final", null, MAJOR,
+ MINOR, null);
+ // private static final Element DEPRECATED = new Element(ACCESS,
+ // "deprecated", null,
+ // CHANGED, CHANGED, null);
+
+ final Analyzer analyzer;
+ final Map<PackageRef, Instructions> providerMatcher = Create.map();
+ final Set<TypeRef> notAccessible = Create.set();
+ final Map<Object, Element> cache = Create.map();
+ MultiMap<PackageRef, //
+ Element> packages;
+ final MultiMap<TypeRef, //
+ Element> covariant = new MultiMap<TypeRef, Element>();
+ final Set<JAVA> javas = Create.set();
+ final Packages exports;
+
+ /**
+ * Create an element for the API. We take the exported packages and traverse
+ * those for their classes. If there is no manifest or it does not describe
+ * a bundle we assume the whole contents is exported.
+ *
+ * @param infos
+ */
+ JavaElement(Analyzer analyzer) throws Exception {
+ this.analyzer = analyzer;
+
+ Manifest manifest = analyzer.getJar().getManifest();
+ if (manifest != null
+ && manifest.getMainAttributes().getValue(Constants.BUNDLE_MANIFESTVERSION) != null) {
+ exports = new Packages();
+ for (Map.Entry<String, Attrs> entry : OSGiHeader.parseHeader(
+ manifest.getMainAttributes().getValue(Constants.EXPORT_PACKAGE)).entrySet())
+ exports.put(analyzer.getPackageRef(entry.getKey()), entry.getValue());
+ } else
+ exports = analyzer.getContained();
+ //
+ // We have to gather the -providers and parse them into instructions
+ // so we can efficiently match them during class parsing to find
+ // out who the providers and consumers are
+ //
+
+ for (Entry<PackageRef, Attrs> entry : exports.entrySet()) {
+ String value = entry.getValue().get(Constants.PROVIDER_TYPE_DIRECTIVE);
+ if (value != null) {
+ providerMatcher.put(entry.getKey(), new Instructions(value));
+ }
+ }
+
+ // we now need to gather all the packages but without
+ // creating the packages yet because we do not yet know
+ // which classes are accessible
+
+ packages = new MultiMap<PackageRef, Element>();
+
+ for (Clazz c : analyzer.getClassspace().values()) {
+ if (c.isPublic() || c.isProtected()) {
+ PackageRef packageName = c.getClassName().getPackageRef();
+
+ if (exports.containsKey(packageName)) {
+ Element cdef = classElement(c);
+ packages.add(packageName, cdef);
+ }
+ }
+ }
+
+ }
+
+ static Element getAPI(Analyzer analyzer) throws Exception {
+ analyzer.analyze();
+ JavaElement te = new JavaElement(analyzer);
+ return te.getLocalAPI();
+ }
+
+ private Element getLocalAPI() throws Exception {
+ List<Element> result = new ArrayList<Element>();
+
+ for (Map.Entry<PackageRef, List<Element>> entry : packages.entrySet()) {
+ List<Element> set = entry.getValue();
+ for (Iterator<Element> i = set.iterator(); i.hasNext();) {
+
+ if (notAccessible.contains( analyzer.getTypeRefFromFQN(i.next().getName())))
+ i.remove();
+
+ }
+ String version = exports.get(entry.getKey()).get(Constants.VERSION_ATTRIBUTE);
+ if (version != null) {
+ Version v = new Version(version);
+ set.add(new Element(Type.VERSION, v.getWithoutQualifier().toString(), null,
+ IGNORED, IGNORED, null));
+ }
+ Element pd = new Element(Type.PACKAGE, entry.getKey().getFQN(), set, MINOR, MAJOR, null);
+ result.add(pd);
+ }
+
+ for (JAVA java : javas) {
+ result.add(new Element(CLASS_VERSION, java.toString(), null, Delta.CHANGED,
+ Delta.CHANGED, null));
+ }
+
+ return new Element(Type.API, "<api>", result, CHANGED, CHANGED, null);
+ }
+
+ /**
+ * Calculate the class element. This requires parsing the class file and
+ * finding all the methods that were added etc. The parsing will take super
+ * interfaces and super classes into account. For this reason it maintains a
+ * queue of classes/interfaces to parse.
+ *
+ * @param analyzer
+ * @param clazz
+ * @param infos
+ * @return
+ * @throws Exception
+ */
+ Element classElement(final Clazz clazz) throws Exception {
+ Element e = cache.get(clazz);
+ if (e != null)
+ return e;
+
+ final StringBuilder comment = new StringBuilder();
+ final Set<Element> members = new HashSet<Element>();
+ final Set<MethodDef> methods = Create.set();
+ final Set<Clazz.FieldDef> fields = Create.set();
+ final MultiMap<Clazz.Def, Element> annotations = new MultiMap<Clazz.Def, Element>();
+
+ final TypeRef name = clazz.getClassName();
+
+ final String fqn = name.getFQN();
+ final String shortName = name.getShortName();
+
+ // Check if this clazz is actually a provider or not
+ // providers must be listed in the exported package in the
+ // PROVIDER_TYPE directive.
+ Instructions matchers = providerMatcher.get(name.getPackageRef());
+ boolean p = matchers != null && matchers.matches(shortName);
+ final AtomicBoolean provider = new AtomicBoolean(p);
+
+ //
+ // Check if we already had this clazz in the cache
+ //
+
+ Element before = cache.get(clazz); // for super classes
+ if (before != null)
+ return before;
+
+ clazz.parseClassFileWithCollector(new ClassDataCollector() {
+ boolean memberEnd;
+ Clazz.FieldDef last;
+
+ @Override public void version(int minor, int major) {
+ javas.add(Clazz.JAVA.getJava(major, minor));
+ }
+
+ @Override public void method(MethodDef defined) {
+ if ((defined.isProtected() || defined.isPublic())) {
+ last = defined;
+ methods.add(defined);
+ } else {
+ last = null;
+ }
+ }
+
+ @Override public void deprecated() {
+ if (memberEnd)
+ clazz.setDeprecated(true);
+ else
+ last.setDeprecated(true);
+ }
+
+ @Override public void field(Clazz.FieldDef defined) {
+ if (defined.isProtected() || defined.isPublic()) {
+ last = defined;
+ fields.add(defined);
+ } else
+ last = null;
+ }
+
+ @Override public void constant(Object o) {
+ if (last != null) {
+ // Must be accessible now
+ last.setConstant(o);
+ }
+ }
+
+ @Override public void extendsClass(TypeRef name) throws Exception {
+ String comment = null;
+ if (!clazz.isInterface())
+ comment = inherit(members, name);
+
+ Clazz c = analyzer.findClass(name);
+ if ((c == null || c.isPublic()) && !name.isObject())
+ members.add(new Element(Type.EXTENDS, name.getFQN(), null, MICRO, MAJOR,
+ comment));
+ }
+
+ @Override public void implementsInterfaces(TypeRef names[]) throws Exception {
+ // TODO is interface reordering important for binary
+ // compatibility??
+
+ for (TypeRef name : names) {
+
+ String comment = null;
+ if (clazz.isInterface() || clazz.isAbstract())
+ comment = inherit(members, name);
+ members.add(new Element(Type.IMPLEMENTS, name.getFQN(), null, MINOR, MAJOR,
+ comment));
+ }
+ }
+
+ /**
+ * @param members
+ * @param name
+ * @param comment
+ * @return
+ */
+ Set<Element> OBJECT = Create.set();
+
+ public String inherit(final Set<Element> members, TypeRef name) throws Exception {
+ if (name.isObject()) {
+ if (OBJECT.isEmpty()) {
+ Clazz c = analyzer.findClass(name);
+ Element s = classElement(c);
+ for (Element child : s.children) {
+ if (INHERITED.contains(child.type)) {
+ String n = child.getName();
+ if (child.type == METHOD) {
+ if (n.startsWith("<init>")
+ || "getClass()".equals(child.getName())
+ || n.startsWith("wait(") || n.startsWith("notify(")
+ || n.startsWith("notifyAll("))
+ continue;
+ }
+ OBJECT.add(child);
+ }
+ }
+ }
+ members.addAll(OBJECT);
+ } else {
+
+ Clazz c = analyzer.findClass(name);
+ if (c == null) {
+ return "Cannot load " + name;
+ } else {
+ Element s = classElement(c);
+ for (Element child : s.children) {
+ if (INHERITED.contains(child.type) && !child.name.startsWith("<")) {
+ members.add(child);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override public void annotation(Annotation annotation) {
+ Collection<Element> properties = Create.set();
+ if (Deprecated.class.getName().equals(annotation.getName().getFQN())) {
+ if (memberEnd)
+ clazz.setDeprecated(true);
+ else
+ last.setDeprecated(true);
+ return;
+ }
+
+ for (String key : annotation.keySet()) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(key);
+ sb.append('=');
+ toString(sb, annotation.get(key));
+
+ properties.add(new Element(Type.PROPERTY, sb.toString(), null, CHANGED,
+ CHANGED, null));
+ }
+
+ if (memberEnd) {
+ members.add(new Element(Type.ANNOTATED, annotation.getName().getFQN(),
+ properties, CHANGED, CHANGED, null));
+ if (ProviderType.class.getName().equals(annotation.getName().getFQN())) {
+ provider.set(true);
+ } else if (ConsumerType.class.getName().equals(annotation.getName().getFQN())) {
+ provider.set(false);
+ }
+ } else if (last != null)
+ annotations.add(last, new Element(Type.ANNOTATED,
+ annotation.getName().getFQN(), properties, CHANGED, CHANGED, null));
+ }
+
+ private void toString(StringBuilder sb, Object object) {
+
+ if (object.getClass().isArray()) {
+ sb.append('[');
+ int l = Array.getLength(object);
+ for (int i = 0; i < l; i++)
+ toString(sb, Array.get(object, i));
+ sb.append(']');
+ } else
+ sb.append(object);
+ }
+
+ @Override public void innerClass(TypeRef innerClass, TypeRef outerClass,
+ String innerName, int innerClassAccessFlags) throws Exception {
+ Clazz clazz = analyzer.findClass(innerClass);
+ if (clazz != null)
+ clazz.setInnerAccess(innerClassAccessFlags);
+
+ if (Modifier.isProtected(innerClassAccessFlags)
+ || Modifier.isPublic(innerClassAccessFlags))
+ return;
+ notAccessible.add(innerClass);
+ }
+
+ @Override public void memberEnd() {
+ memberEnd = true;
+ }
+ });
+
+ // This is the heart of the semantic versioning. If we
+ // add or remove a method from an interface then
+ Delta add;
+ Delta remove;
+ Type type;
+
+ // Calculate the type of the clazz. A class
+ // can be an interface, class, enum, or annotation
+
+ if (clazz.isInterface())
+ if (clazz.isAnnotation())
+ type = Type.INTERFACE;
+ else
+ type = Type.ANNOTATION;
+ else if (clazz.isEnum())
+ type = Type.ENUM;
+ else
+ type = Type.CLASS;
+
+ if (type == Type.INTERFACE) {
+ if (provider.get()) {
+ // Adding a method for a provider is not an issue
+ // because it must be aware of the changes
+ add = MINOR;
+
+ // Removing a method influences consumers since they
+ // tend to call this guy.
+ remove = MAJOR;
+ } else {
+ // Adding a method is a major change
+ // because the consumer has to implement it
+ // or the provider will call a non existent
+ // method on the consumer
+ add = MAJOR;
+
+ // Removing a method is not an issue because the
+ // provider, which calls this contract must be
+ // aware of the removal
+
+ remove = MINOR;
+ }
+ } else {
+ // Adding a method to a class can never do any harm
+ add = MINOR;
+
+ // Removing it will likely hurt consumers
+ remove = MAJOR;
+ }
+
+ // Remove all synthetic methods, we need
+ // to treat them special for the covariant returns
+
+ Set<MethodDef> synthetic = Create.set();
+ for (Iterator<MethodDef> i = methods.iterator(); i.hasNext();) {
+ MethodDef m = i.next();
+ if (m.isSynthetic()) {
+ synthetic.add(m);
+ i.remove();
+ }
+ }
+
+ for (MethodDef m : methods) {
+ List<Element> children = annotations.get(m);
+ if (children == null)
+ children = new ArrayList<Element>();
+
+ access(children, m.getAccess(), m.isDeprecated());
+
+ // A final class cannot be extended, ergo,
+ // all methods defined in it are by definition
+ // final. However, marking them final (either
+ // on the method or inheriting it from the class)
+ // will create superfluous changes if we
+ // override a method from a super class that was not
+ // final. So we actually remove the final for methods
+ // in a final class.
+ if (clazz.isFinal())
+ children.remove(FINAL);
+
+ // for covariant types we need to add the return types
+ // and all the implemented and extended types. This is already
+ // do for us when we get the element of the return type.
+
+ getCovariantReturns(children, m.getType());
+
+ for (Iterator<MethodDef> i = synthetic.iterator(); i.hasNext();) {
+ MethodDef s = i.next();
+ if (s.getName().equals(m.getName())
+ && Arrays.equals(s.getPrototype(), m.getPrototype())) {
+ i.remove();
+ getCovariantReturns(children, s.getType());
+ }
+ }
+
+ Element member = new Element(Type.METHOD, m.getName() + toString(m.getPrototype()),
+ children, add, remove, null);
+
+ if (!members.add(member)) {
+ members.remove(member);
+ members.add(member);
+ }
+ }
+
+ /**
+ * Repeat for the remaining synthetic methods
+ */
+ for (MethodDef m : synthetic) {
+ List<Element> children = annotations.get(m);
+ if (children == null)
+ children = new ArrayList<Element>();
+ access(children, m.getAccess(), m.isDeprecated());
+
+ // A final class cannot be extended, ergo,
+ // all methods defined in it are by definition
+ // final. However, marking them final (either
+ // on the method or inheriting it from the class)
+ // will create superfluous changes if we
+ // override a method from a super class that was not
+ // final. So we actually remove the final for methods
+ // in a final class.
+ if (clazz.isFinal())
+ children.remove(FINAL);
+
+ // for covariant types we need to add the return types
+ // and all the implemented and extended types. This is already
+ // do for us when we get the element of the return type.
+
+ getCovariantReturns(children, m.getType());
+
+ Element member = new Element(Type.METHOD, m.getName() + toString(m.getPrototype()),
+ children, add, remove, "synthetic");
+
+ if (!members.add(member)) {
+ members.remove(member);
+ members.add(member);
+ }
+ }
+
+ for (Clazz.FieldDef f : fields) {
+ List<Element> children = annotations.get(f);
+ if (children == null)
+ children = new ArrayList<Element>();
+
+ // Fields can have a constant value, this is a new element
+ if (f.getConstant() != null) {
+ children.add(new Element(Type.CONSTANT, f.getConstant().toString(), null, CHANGED,
+ CHANGED, null));
+ }
+
+ access(children, f.getAccess(), f.isDeprecated());
+ Element member = new Element(Type.FIELD, f.getType().getFQN() + " " + f.getName(),
+ children, MINOR, MAJOR, null);
+
+ if (!members.add(member)) {
+ members.remove(member);
+ members.add(member);
+ }
+ }
+
+ access(members, clazz.getAccess(), clazz.isDeprecated());
+
+ // And make the result
+ Element s = new Element(type, fqn, members, MINOR, MAJOR, comment.length() == 0 ? null
+ : comment.toString());
+ cache.put(clazz, s);
+ return s;
+ }
+
+ private String toString(TypeRef[] prototype) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("(");
+ String del = "";
+ for (TypeRef ref : prototype) {
+ sb.append(del);
+ sb.append(ref.getFQN());
+ del = ",";
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ static Element BOOLEAN_R = new Element(RETURN, "boolean");
+ static Element BYTE_R = new Element(RETURN, "byte");
+ static Element SHORT_R = new Element(RETURN, "short");
+ static Element CHAR_R = new Element(RETURN, "char");
+ static Element INT_R = new Element(RETURN, "int");
+ static Element LONG_R = new Element(RETURN, "long");
+ static Element FLOAT_R = new Element(RETURN, "float");
+ static Element DOUBLE_R = new Element(RETURN, "double");
+
+ private void getCovariantReturns(Collection<Element> elements, TypeRef type) throws Exception {
+ if (type == null || type.isObject())
+ return;
+
+ if (type.isPrimitive()) {
+ if (type.getFQN().equals("void"))
+ return;
+
+ String name = type.getBinary();
+ Element e;
+ switch (name.charAt(0)) {
+ case 'Z':
+ e = BOOLEAN_R;
+ break;
+ case 'S':
+ e = SHORT_R;
+ break;
+ case 'I':
+ e = INT_R;
+ break;
+ case 'B':
+ e = BYTE_R;
+ break;
+ case 'C':
+ e = CHAR_R;
+ break;
+ case 'J':
+ e = LONG_R;
+ break;
+ case 'F':
+ e = FLOAT_R;
+ break;
+ case 'D':
+ e = DOUBLE_R;
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown primitive " + type);
+ }
+ elements.add(e);
+ return;
+ }
+
+ List<Element> set = covariant.get(type);
+ if (set != null) {
+ elements.addAll(set);
+ return;
+ }
+
+ Element current = new Element(RETURN, type.getFQN());
+ Clazz clazz = analyzer.findClass(type);
+ if (clazz == null) {
+ elements.add(current);
+ return;
+ }
+
+ set = Create.list();
+ set.add(current);
+ getCovariantReturns(set, clazz.getSuper());
+
+ TypeRef[] interfaces = clazz.getInterfaces();
+ if (interfaces != null)
+ for (TypeRef intf : interfaces) {
+ getCovariantReturns(set, intf);
+ }
+
+ covariant.put(type, set);
+ elements.addAll(set);
+ }
+
+ private static void access(Collection<Element> children, int access, boolean deprecated) {
+ if (!isPublic(access))
+ children.add(PROTECTED);
+ if (isAbstract(access))
+ children.add(ABSTRACT);
+ if (isFinal(access))
+ children.add(FINAL);
+ if (isStatic(access))
+ children.add(STATIC);
+
+ // Ignore for now
+ // if (deprecated)
+ // children.add(DEPRECATED);
+
+ }
+
+}
\ No newline at end of file