package aQute.bnd.differ; import static aQute.bnd.service.diff.Delta.CHANGED; import static aQute.bnd.service.diff.Delta.IGNORED; import static aQute.bnd.service.diff.Delta.MAJOR; import static aQute.bnd.service.diff.Delta.MICRO; import static aQute.bnd.service.diff.Delta.MINOR; import static aQute.bnd.service.diff.Type.ACCESS; import static aQute.bnd.service.diff.Type.ANNOTATED; import static aQute.bnd.service.diff.Type.ANNOTATION; import static aQute.bnd.service.diff.Type.API; import static aQute.bnd.service.diff.Type.CLASS; import static aQute.bnd.service.diff.Type.CLASS_VERSION; import static aQute.bnd.service.diff.Type.CONSTANT; import static aQute.bnd.service.diff.Type.ENUM; import static aQute.bnd.service.diff.Type.EXTENDS; import static aQute.bnd.service.diff.Type.FIELD; import static aQute.bnd.service.diff.Type.IMPLEMENTS; import static aQute.bnd.service.diff.Type.INTERFACE; import static aQute.bnd.service.diff.Type.METHOD; import static aQute.bnd.service.diff.Type.PACKAGE; import static aQute.bnd.service.diff.Type.PROPERTY; import static aQute.bnd.service.diff.Type.RETURN; import static aQute.bnd.service.diff.Type.VERSION; import java.lang.reflect.Array; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.jar.Manifest; import aQute.bnd.header.Attrs; import aQute.bnd.header.OSGiHeader; import aQute.bnd.osgi.Analyzer; import aQute.bnd.osgi.Annotation; import aQute.bnd.osgi.ClassDataCollector; import aQute.bnd.osgi.Clazz; import aQute.bnd.osgi.Clazz.JAVA; import aQute.bnd.osgi.Clazz.MethodDef; import aQute.bnd.osgi.Constants; import aQute.bnd.osgi.Descriptors.PackageRef; import aQute.bnd.osgi.Descriptors.TypeRef; import aQute.bnd.osgi.Instructions; import aQute.bnd.osgi.Packages; import aQute.bnd.service.diff.Delta; import aQute.bnd.service.diff.Type; import aQute.bnd.version.Version; import aQute.lib.collections.MultiMap; import aQute.libg.generics.Create; /** * 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 PROTECTED_PROVIDER = new Element(ACCESS, "protected", null, MINOR, 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); // Common return type elements static final Element VOID_R = new Element(RETURN, "void"); static final Element BOOLEAN_R = new Element(RETURN, "boolean"); static final Element BYTE_R = new Element(RETURN, "byte"); static final Element SHORT_R = new Element(RETURN, "short"); static final Element CHAR_R = new Element(RETURN, "char"); static final Element INT_R = new Element(RETURN, "int"); static final Element LONG_R = new Element(RETURN, "long"); static final Element FLOAT_R = new Element(RETURN, "float"); static final Element DOUBLE_R = new Element(RETURN, "double"); static final Element OBJECT_R = new Element(RETURN, "java.lang.Object"); final Analyzer analyzer; final Map<PackageRef,Instructions> providerMatcher = Create.map(); final Set<TypeRef> notAccessible = Create.set(); final Map<Object,Element> cache = Create.map(); final MultiMap<PackageRef,Element> packages; 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.isSynthetic()) continue; 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 { Set<Element> result = new HashSet<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(VERSION, v.getWithoutQualifier().toString(), null, IGNORED, IGNORED, null)); } Element pd = new Element(PACKAGE, entry.getKey().getFQN(), set, MINOR, MAJOR, null); result.add(pd); } for (JAVA java : javas) { result.add(new Element(CLASS_VERSION, java.toString(), null, CHANGED, CHANGED, null)); } return new Element(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 * @throws Exception */ Element classElement(final Clazz clazz) throws Exception { Element e = cache.get(clazz); if (e != null) return e; final Set<Element> members = Create.set(); 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 if (last != null) 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(EXTENDS, name.getFQN(), null, MICRO, MAJOR, comment)); } @Override public void implementsInterfaces(TypeRef names[]) throws Exception { Arrays.sort(names); // ignore type reordering for (TypeRef name : names) { String comment = null; if (clazz.isInterface() || clazz.isAbstract()) comment = inherit(members, name); members.add(new Element(IMPLEMENTS, name.getFQN(), null, MINOR, MAJOR, comment)); } } /** */ 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); if (c == null) { // Bnd fails on Java 9 class files #1598 // Caused by Java 9 not making class rsources // available return null; } 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; } if (isStatic(child)) continue; OBJECT.add(child); } } } members.addAll(OBJECT); } else { Clazz c = analyzer.findClass(name); if (c == null) { return inherit(members, analyzer.getTypeRef("java/lang/Object")); } Element s = classElement(c); for (Element child : s.children) { if (isStatic(child)) continue; if (INHERITED.contains(child.type) && !child.name.startsWith("<")) { members.add(child); } } } return null; } private boolean isStatic(Element child) { boolean isStatic = child.get("static") != null; return isStatic; } /** * Deprecated annotations and Provider/Consumer Type (both bnd and * OSGi) are treated special. Other annotations are turned into a * tree. Starting with ANNOTATED, and then properties. A property is * a PROPERTY property or an ANNOTATED property if it is an * annotation. If it is an array, the key is suffixed with the * index. * * <pre> * public @interface Outer { Inner[] value(); } * public @interface Inner { String[] value(); } @Outer( * { @Inner("1","2"}) } class Xyz {} ANNOTATED Outer * (CHANGED/CHANGED) ANNOTATED Inner (CHANGED/CHANGED) PROPERTY * value.0=1 (CHANGED/CHANGED) PROPERTY value.1=2 (CHANGED/CHANGED) * </pre> */ @Override public void annotation(Annotation annotation) { if (Deprecated.class.getName().equals(annotation.getName().getFQN())) { if (memberEnd) clazz.setDeprecated(true); else if (last != null) last.setDeprecated(true); return; } Element e = annotatedToElement(annotation); if (memberEnd) { members.add(e); // // Check for the provider/consumer. We use strings because // these are not officially // released yet // String name = annotation.getName().getFQN(); if ("aQute.bnd.annotation.ProviderType".equals(name) || "org.osgi.annotation.versioning.ProviderType".equals(name)) { provider.set(true); } else if ("aQute.bnd.annotation.ConsumerType".equals(name) || "org.osgi.annotation.versioning.ConsumerType".equals(name)) { provider.set(false); } } else if (last != null) annotations.add(last, e); } /* * Return an ANNOTATED element for this annotation. An ANNOTATED * element contains either PROPERTY children or ANNOTATED children. */ private Element annotatedToElement(Annotation annotation) { Collection<Element> properties = Create.set(); for (String key : annotation.keySet()) { addAnnotationMember(properties, key, annotation.get(key)); } return new Element(ANNOTATED, annotation.getName().getFQN(), properties, CHANGED, CHANGED, null); } /* * This method detects 3 cases: An Annotation, which means it * creates a new child ANNOTATED element, an array, which means it * will repeat recursively but suffixes the key with the index, or a * simple value which is turned into a string. */ private void addAnnotationMember(Collection<Element> properties, String key, Object member) { if (member instanceof Annotation) { properties.add(annotatedToElement((Annotation) member)); } else if (member.getClass().isArray()) { int l = Array.getLength(member); for (int i = 0; i < l; i++) { addAnnotationMember(properties, key + "." + i, Array.get(member, i)); } } else { StringBuilder sb = new StringBuilder(); sb.append(key); sb.append('='); if (member instanceof String) { sb.append("'"); sb.append(member); sb.append("'"); } else sb.append(member); properties.add(new Element(PROPERTY, sb.toString(), null, CHANGED, CHANGED, null)); } } @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 = ANNOTATION; else type = INTERFACE; else if (clazz.isEnum()) type = ENUM; else type = CLASS; if (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 for // providers, however, consumers could potentially // call through this interface :-( remove = MAJOR; } } else { // Adding a method to a class can never do any harm // except when the class is extended and the new // method clashes with the new method. That is // why API classes in general should be final, at // least not extended by consumers. add = MINOR; // Removing it will likely hurt consumers remove = MAJOR; } for (MethodDef m : methods) { if (m.isSynthetic()) { // Ignore synthetic methods continue; } Collection<Element> children = annotations.get(m); if (children == null) children = new HashSet<Element>(); access(children, m.getAccess(), m.isDeprecated(), provider.get()); // 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); children.add(getReturn(m.getType())); // // Java default methods are concrete implementations of methods // on an interface. // if (clazz.isInterface() && !m.isAbstract()) { // // We have a Java 8 default method! // Such a method is always a minor update // add = MINOR; } String signature = m.getName() + toString(m.getPrototype()); Element member = new Element(METHOD, signature, children, add, provider.get() && !m.isPublic() ? MINOR : remove, null); if (!members.add(member)) { members.remove(member); members.add(member); } } for (Clazz.FieldDef f : fields) { if (f.isSynthetic()) { // Ignore synthetic fields continue; } Collection<Element> children = annotations.get(f); if (children == null) children = new HashSet<Element>(); // Fields can have a constant value, this is a new element if (f.getConstant() != null) { children.add(new Element(CONSTANT, f.getConstant().toString(), null, CHANGED, CHANGED, null)); } access(children, f.getAccess(), f.isDeprecated(), provider.get()); children.add(getReturn(f.getType())); Element member = new Element(FIELD, f.getName(), children, MINOR, provider.get() && !f.isPublic() ? MINOR : MAJOR, null); if (!members.add(member)) { members.remove(member); members.add(member); } } access(members, clazz.getAccess(), clazz.isDeprecated(), provider.get()); // And make the result Element s = new Element(type, fqn, members, MINOR, MAJOR, null); 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(); } private Element getReturn(TypeRef type) { if (!type.isPrimitive()) { return type.isObject() ? OBJECT_R : new Element(RETURN, type.getFQN()); } switch (type.getBinary().charAt(0)) { case 'V' : return VOID_R; case 'Z' : return BOOLEAN_R; case 'S' : return SHORT_R; case 'I' : return INT_R; case 'B' : return BYTE_R; case 'C' : return CHAR_R; case 'J' : return LONG_R; case 'F' : return FLOAT_R; case 'D' : return DOUBLE_R; default : throw new IllegalArgumentException("Unknown primitive " + type); } } private static void access(Collection<Element> children, int access, @SuppressWarnings("unused") boolean deprecated, boolean provider) { if (!Modifier.isPublic(access)) children.add(provider ? PROTECTED_PROVIDER : PROTECTED); if (Modifier.isAbstract(access)) children.add(ABSTRACT); if (Modifier.isFinal(access)) children.add(FINAL); if (Modifier.isStatic(access)) children.add(STATIC); } }