All Downloads are FREE. Search and download functionalities are using the official Maven repository.

aQute.bnd.differ.JavaElement Maven / Gradle / Ivy

There is a newer version: 7.0.0
Show newest version
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.CLASS_VERSION;
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.METHOD;
import static aQute.bnd.service.diff.Type.RETURN;
import static java.lang.reflect.Modifier.isAbstract;
import static java.lang.reflect.Modifier.isFinal;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;

import java.lang.reflect.Array;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;

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.
 * 

* Packages *

    *
  • MAJOR - Remove a public type *
  • MINOR - Add a public class *
  • MINOR - Add an interface *
  • MINOR - Add a method to a class *
  • MINOR - Add a method to a provider interface *
  • MAJOR - Add a method to a consumer interface *
  • MINOR - Add a field *
  • MICRO - Add an annotation to a member *
  • MINOR - Change the value of a constant *
  • MICRO - -abstract *
  • MICRO - -final *
  • MICRO - -protected *
  • MAJOR - +abstract *
  • MAJOR - +final *
  • MAJOR - +protected *
*/ class JavaElement { static Pattern PARAMETERS_P = Pattern.compile(".*(\\(.*\\)).*"); final static EnumSet 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 providerMatcher = Create.map(); final Set notAccessible = Create.set(); final Map cache = Create.map(); MultiMap packages; final MultiMap covariant = new MultiMap(); final Set 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 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 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(); 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 result = new HashSet(); for (Map.Entry> entry : packages.entrySet()) { List set = entry.getValue(); for (Iterator 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, "", 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 StringBuilder comment = new StringBuilder(); final Set members = new LinkedHashSet(); final Set methods = new LinkedHashSet(); final Set fields = Create.set(); final MultiMap annotations = new MultiMap(); 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(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)); } } /** */ Set OBJECT = Create.set(); public String inherit(final Set 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("") || "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 "Cannot load " + name; } 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. * *
			 *  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)
			 * 
*/ @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 properties = Create.set(); for (String key : annotation.keySet()) { addAnnotationMember(properties, key, annotation.get(key)); } return new Element(Type.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 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(Type.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 = Type.ANNOTATION; else type = Type.INTERFACE; 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 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; } // Remove all synthetic methods, we need // to treat them special for the covariant returns // We must do this in the correct order so please // keep the linked hashset in place! Set synthetic = new LinkedHashSet(); for (Iterator i = methods.iterator(); i.hasNext();) { MethodDef m = i.next(); if (m.isSynthetic()) { synthetic.add(m); i.remove(); } } // System.out.println("methods/synthetic " + clazz+ " " + methods + " " // + synthetic); for (MethodDef m : methods) { Collection children = annotations.get(m); if (children == null) children = new HashSet(); 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(), m.getGenericReturnType()); /** * No longer includes synthetic methods in the tree */ // for (Iterator 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()); // } // } String signature = m.getSignature(); Matcher matcher; if (signature != null && (matcher = PARAMETERS_P.matcher(signature)).matches()) { signature = matcher.group(1); } else signature = toString(m.getPrototype()); // // 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; } Element member = new Element(Type.METHOD, m.getName() + signature, children, add, remove, null); if (!members.add(member)) { members.remove(member); members.add(member); } } /** * Repeat for the remaining synthetic methods After long discussions * with BJ we decided to skip the synthetic methods since they do not * seem to contribute to the binary compatibility. They also seem to * cause problems between compilers. ECJ onlyu adds a synthetic methods * for the super class while Javac traverse the whole super chain. I've * actually not been able to figure out how ECJ gets away with this but * the behavior seems ok. */ // for (MethodDef m : synthetic) { // Collection children = annotations.get(m); // if (children == null) // children = new HashSet(); // 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 // // done 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) { Collection children = annotations.get(f); if (children == null) children = new HashSet(); // 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 elements, TypeRef type, String generics) 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; } Element current = new Element(RETURN, generics); elements.add(current); // List 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 children, int access, @SuppressWarnings("unused") 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy