/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.aries.versioning.check; import static org.apache.aries.versioning.utils.SemanticVersioningUtils.oneLineBreak; import static org.apache.aries.versioning.utils.SemanticVersioningUtils.twoLineBreaks; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.aries.util.filesystem.FileSystem; import org.apache.aries.util.filesystem.IDirectory; import org.apache.aries.util.filesystem.IFile; import org.apache.aries.util.io.IOUtils; import org.apache.aries.util.manifest.ManifestHeaderProcessor; import org.apache.aries.versioning.utils.BinaryCompatibilityStatus; import org.apache.aries.versioning.utils.ClassDeclaration; import org.apache.aries.versioning.utils.FieldDeclaration; import org.apache.aries.versioning.utils.MethodDeclaration; import org.apache.aries.versioning.utils.SemanticVersioningClassVisitor; import org.apache.aries.versioning.utils.SemanticVersioningUtils; import org.apache.aries.versioning.utils.SerialVersionClassVisitor; import org.objectweb.asm.ClassReader; import org.osgi.framework.Constants; import org.osgi.framework.Version; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @version $Rev:$ $Date:$ */ public class BundleCompatibility { private static final Logger _logger = LoggerFactory.getLogger(BundleCompatibility.class); private URLClassLoader oldJarsLoader; private URLClassLoader newJarsLoader; private String bundleSymbolicName; private String bundleElement; private boolean bundleVersionCorrect; private BundleInfo currentBundle; private BundleInfo baseBundle; private StringBuilder pkgElements = new StringBuilder(); private List<String> pkgElementsList = new ArrayList<String>(); private List<String> excludes; private VersionChange bundleChange; private final Map<String, VersionChange> packageChanges = new HashMap<String, VersionChange>(); public BundleCompatibility(String bundleSymbolicName, BundleInfo currentBundle, BundleInfo baseBundle, URLClassLoader oldJarsLoader, URLClassLoader newJarsLoader) { this(bundleSymbolicName, currentBundle, baseBundle, oldJarsLoader, newJarsLoader, null); } public BundleCompatibility(String bundleSymbolicName, BundleInfo currentBundle, BundleInfo baseBundle, URLClassLoader oldJarsLoader, URLClassLoader newJarsLoader, List<String> excludes) { this.bundleSymbolicName = bundleSymbolicName; this.currentBundle = currentBundle; this.baseBundle = baseBundle; this.oldJarsLoader = oldJarsLoader; this.newJarsLoader = newJarsLoader; this.excludes = excludes != null ? excludes : new ArrayList<String>(); } public VersionChange getBundleChange() { return bundleChange; } public Map<String, VersionChange> getPackageChanges() { return packageChanges; } public String getBundleElement() { return bundleElement; } public StringBuilder getPkgElements() { return pkgElements; } private boolean ignoreChange(String reason) { if ((reason == null) || (this.excludes.isEmpty())) return false; for (String exclude : this.excludes) { // Could have interpreted each exclude as a regex, but that makes it easy to write loose rules // that match more strings than intended. if ((reason != null) && reason.contains(exclude)) return true; } return false; } public boolean isBundleVersionCorrect() { return bundleVersionCorrect; } public BundleCompatibility invoke() throws IOException { String reason = null; // open the manifest and scan the export package and find the package name and exported version // The tool assume the one particular package just exports under one version Map<String, PackageContent> currBundleExpPkgContents = getAllExportedPkgContents(currentBundle); Map<String, PackageContent> baseBundleExpPkgContents; boolean pkg_major_change = false; boolean pkg_minor_change = false; String fatal_package = null; if (!!!currBundleExpPkgContents.isEmpty()) { baseBundleExpPkgContents = getAllExportedPkgContents(baseBundle); // compare each class right now for (Map.Entry<String, PackageContent> pkg : baseBundleExpPkgContents.entrySet()) { String pkgName = pkg.getKey(); Map<String, IFile> baseClazz = pkg.getValue().getClasses(); Map<String, IFile> baseXsds = pkg.getValue().getXsds(); PackageContent currPkgContents = currBundleExpPkgContents.get(pkgName); if (currPkgContents == null) { // The package is no longer exported any more. This should lead to bundle major version change. pkg_major_change = true; fatal_package = pkgName; _logger.debug("The package " + pkgName + " in the bundle of " + bundleSymbolicName + " is no longer to be exported. Major change."); } else { Map<String, IFile> curClazz = currPkgContents.getClasses(); Map<String, IFile> curXsds = currPkgContents.getXsds(); //check whether there should be major change/minor change/micro change in this package. //1. Use ASM to visit all classes in the package VersionChangeReason majorChange = new VersionChangeReason(); VersionChangeReason minorChange = new VersionChangeReason(); // check all classes to see whether there are minor or major changes visitPackage(pkgName, baseClazz, curClazz, majorChange, minorChange); // If there is no binary compatibility changes, check whether xsd files have been added, changed or deleted if (!!!majorChange.isChange()) { checkXsdChangesInPkg(pkgName, baseXsds, curXsds, majorChange); // If everything is ok with the existing classes. Need to find out whether there are more API (abstract classes) in the current bundle. // loop through curClazz and visit it and find out whether one of them is abstract. // check whether there are more xsd or abstract classes added if (!!!(majorChange.isChange() || minorChange.isChange())) { checkAdditionalClassOrXsds(pkgName, curClazz, curXsds, minorChange); } } // We have scanned the whole packages, report the result // if (majorChange.isChange() || minorChange.isChange()) { String oldVersion = pkg.getValue().getPackageVersion(); String newVersion = currPkgContents.getPackageVersion(); if (majorChange.isChange() && !!!ignoreChange(majorChange.getReason())) { packageChanges.put(pkgName, new VersionChange(VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion)); pkg_major_change = true; fatal_package = pkgName; if (!!!isVersionCorrect(VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion)) { pkgElementsList.add(getPkgStatusText(pkgName, VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion, majorChange.getReason(), majorChange.getChangeClass())); } } else if (minorChange.isChange() && !!!ignoreChange(minorChange.getReason())) { packageChanges.put(pkgName, new VersionChange(VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion)); pkg_minor_change = true; if (fatal_package == null) fatal_package = pkgName; if (!!!isVersionCorrect(VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion)) { pkgElementsList.add(getPkgStatusText(pkgName, VERSION_CHANGE_TYPE.MINOR_CHANGE, pkg.getValue().getPackageVersion(), currPkgContents.getPackageVersion(), minorChange.getReason(), minorChange.getChangeClass())); } } else { packageChanges.put(pkgName, new VersionChange(VERSION_CHANGE_TYPE.NO_CHANGE, oldVersion, newVersion)); pkgElementsList.add(getPkgStatusText(pkgName, VERSION_CHANGE_TYPE.NO_CHANGE, pkg.getValue().getPackageVersion(), currPkgContents.getPackageVersion(), "", "")); } } } // If there is a package version change, the bundle version needs to be updated. // If there is a major change in one of the packages, the bundle major version needs to be increased. // If there is a minor change in one of the packages, the bundle minor version needs to be increased. String oldVersion = baseBundle.getBundleManifest().getVersion().toString(); String newVersion = currentBundle.getBundleManifest().getVersion().toString(); if (pkg_major_change || pkg_minor_change) { if (pkg_major_change) { // The bundle version's major value should be increased. bundleChange = new VersionChange(VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion); reason = "Some packages have major changes. For an instance, the package " + fatal_package + " has major version changes."; bundleElement = getBundleStatusText(currentBundle.getBundle().getName(), bundleSymbolicName, VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion, reason); bundleVersionCorrect = isVersionCorrect(VERSION_CHANGE_TYPE.MAJOR_CHANGE, oldVersion, newVersion); } else if (pkg_minor_change) { bundleChange = new VersionChange(VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion); reason = "Some packages have minor changes. For an instance, the package " + fatal_package + " has minor version changes."; bundleElement = getBundleStatusText(currentBundle.getBundle().getName(), bundleSymbolicName, VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion, reason); bundleVersionCorrect = isVersionCorrect(VERSION_CHANGE_TYPE.MINOR_CHANGE, oldVersion, newVersion); } } else { bundleChange = new VersionChange(VERSION_CHANGE_TYPE.NO_CHANGE, oldVersion, newVersion); bundleVersionCorrect = isVersionCorrect(VERSION_CHANGE_TYPE.NO_CHANGE, oldVersion, newVersion); if (!bundleVersionCorrect) { reason = "The bundle has no version changes."; bundleElement = getBundleStatusText(currentBundle.getBundle().getName(), bundleSymbolicName, VERSION_CHANGE_TYPE.NO_CHANGE, oldVersion, newVersion, reason); } } } return this; } private Map<String, PackageContent> getAllExportedPkgContents(BundleInfo currentBundle) { String packageExports = currentBundle.getBundleManifest().getRawAttributes().getValue(Constants.EXPORT_PACKAGE); List<ManifestHeaderProcessor.NameValuePair> exportPackageLists = ManifestHeaderProcessor.parseExportString(packageExports); // only perform validation if there are some packages exported. Otherwise, not interested. Map<String, PackageContent> exportedPackages = new HashMap<String, PackageContent>(); if (!!!exportPackageLists.isEmpty()) { File bundleFile = currentBundle.getBundle(); IDirectory bundleDir = FileSystem.getFSRoot(bundleFile); for (ManifestHeaderProcessor.NameValuePair exportedPackage : exportPackageLists) { String packageName = exportedPackage.getName(); String packageVersion = exportedPackage.getAttributes().get(Constants.VERSION_ATTRIBUTE); // need to go through each package and scan every class exportedPackages.put(packageName, new PackageContent(packageName, packageVersion)); } // scan the jar and list all the files under each package List<IFile> allFiles = bundleDir.listAllFiles(); for (IFile file : allFiles) { String directoryFullPath = file.getName(); String directoryName = null; String fileName = null; if (file.isFile() && ((file.getName().endsWith(SemanticVersioningUtils.classExt) || (file.getName().endsWith(SemanticVersioningUtils.schemaExt))))) { if (directoryFullPath.lastIndexOf("/") != -1) { directoryName = directoryFullPath.substring(0, directoryFullPath.lastIndexOf("/")); fileName = directoryFullPath.substring(directoryFullPath.lastIndexOf("/") + 1); } } if (directoryName != null) { String pkgName = directoryName.replaceAll("/", "."); PackageContent pkgContent = exportedPackages.get(pkgName); if (pkgContent != null) { if (file.getName().endsWith(SemanticVersioningUtils.classExt)) { pkgContent.addClass(fileName, file); } else { pkgContent.addXsd(fileName, file); } exportedPackages.put(pkgName, pkgContent); } } } } return exportedPackages; } private String getBundleStatusText(String bundleFileName, String bundleSymbolicName, VERSION_CHANGE_TYPE status, String oldVersionStr, String newVersionStr, String reason) { if (!isVersionCorrect(status, oldVersionStr, newVersionStr)) { return "The bundle " + bundleSymbolicName + " has the following changes:\r\n" + reason + "\r\nThe bundle version should be " + getRecommendedVersion(status, oldVersionStr) + "."; } else { return ""; } } /** * Visit the whole package to scan each class to see whether we need to log minor or major changes. * * @param pkgName * @param baseClazz * @param curClazz * @param majorChange * @param minorChange */ private void visitPackage(String pkgName, Map<String, IFile> baseClazz, Map<String, IFile> curClazz, VersionChangeReason majorChange, VersionChangeReason minorChange) { StringBuilder major_reason = new StringBuilder(); StringBuilder minor_reason = new StringBuilder(); boolean is_major_change = false; boolean is_minor_change = false; String fatal_class = null; boolean foundNewAbstract = false; for (Map.Entry<String, IFile> file : baseClazz.entrySet()) { // scan the latest version of the class IFile curFile = curClazz.get(file.getKey()); String changeClass = file.getValue().getName(); //Scan the base version SemanticVersioningClassVisitor oldcv = getVisitor(file.getValue(), oldJarsLoader); // skip the property files as they are compiled as class file as well ClassDeclaration cd = oldcv.getClassDeclaration(); if ((cd != null) && (!SemanticVersioningUtils.isPropertyFile(cd))) { if (curFile == null) { // the class we are scanning has been deleted from the current version of the bundle // This should be a major increase major_reason.append(twoLineBreaks + "The class/interface " + getClassName(changeClass) + " has been deleted from the package."); //majorChange.update(reason, changeClass); is_major_change = true; // only replace the fatal class if not set as the class won't be found in cmvc due to the fact it has been deleted. if (fatal_class == null) { fatal_class = changeClass; } } else { // check for binary compatibility // load the class from the current version of the bundle // remove it from the curClazz collection as we would like to know whether there are more classes added curClazz.remove(file.getKey()); SemanticVersioningClassVisitor newcv = getVisitor(curFile, newJarsLoader); // check for binary compatibility ClassDeclaration newcd = newcv.getClassDeclaration(); BinaryCompatibilityStatus bcs = newcd.getBinaryCompatibleStatus(oldcv.getClassDeclaration()); if (!bcs.isCompatible()) { major_reason.append(twoLineBreaks + "In the " + getClassName(changeClass) + " class or its supers, the following changes have been made since the last release."); // break binary compatibility for (String reason : bcs) { major_reason.append(oneLineBreak).append(reason); } is_major_change = true; fatal_class = changeClass; } else { //check to see whether more methods are added ClassDeclaration oldcd = oldcv.getClassDeclaration(); Collection<MethodDeclaration> extraMethods = newcd.getExtraMethods(oldcd); boolean containsConcrete = false; boolean containsAbstract = false; boolean abstractClass = newcd.isAbstract(); StringBuilder subRemarks = new StringBuilder(); String concreteSubRemarks = null; for (MethodDeclaration extraMethod : extraMethods) { //only interested in the visible methods not the system generated ones if (!extraMethod.getName().contains("$")) { if (abstractClass) { if (extraMethod.isAbstract()) { foundNewAbstract = true; containsAbstract = true; subRemarks.append(oneLineBreak + SemanticVersioningUtils.getReadableMethodSignature(extraMethod.getName(), extraMethod.getDesc())); } else { //only list one abstract method, no need to list all containsConcrete = true; concreteSubRemarks = oneLineBreak + SemanticVersioningUtils.getReadableMethodSignature(extraMethod.getName(), extraMethod.getDesc()); } } else { containsConcrete = true; concreteSubRemarks = oneLineBreak + SemanticVersioningUtils.getReadableMethodSignature(extraMethod.getName(), extraMethod.getDesc()); break; } } } if (containsConcrete || containsAbstract) { is_minor_change = true; if (!is_major_change) { fatal_class = changeClass; } if (containsAbstract) { minor_reason.append(twoLineBreaks + "In the " + getClassName(changeClass) + " class or its supers, the following abstract methods have been added since the last release of this bundle."); minor_reason.append(subRemarks); } else { minor_reason.append(twoLineBreaks + "In the " + getClassName(changeClass) + " class or its supers, the following method has been added since the last release of this bundle."); minor_reason.append(concreteSubRemarks); } } //check to see whether there are extra public/protected fields if there is no additional methods if (!is_minor_change) { for (FieldDeclaration field : newcd.getExtraFields(oldcd)) { if (field.isPublic() || field.isProtected()) { is_minor_change = true; String extraFieldRemarks = oneLineBreak + " " + SemanticVersioningUtils.transform(field.getDesc()) + " " + field.getName(); if (!is_major_change) { fatal_class = changeClass; } minor_reason.append(twoLineBreaks + "In the " + getClassName(changeClass) + " class or its supers, the following fields have been added since the last release of this bundle."); minor_reason.append(extraFieldRemarks); break; } } } } } } } if (is_major_change) { majorChange.update(major_reason.toString(), fatal_class, false); } if (is_minor_change) { minorChange.update(minor_reason.toString(), fatal_class, (foundNewAbstract ? true : false)); } } /** * Check whether the package has xsd file changes or deleted. If yes, log a minor change. * * @param pkgName * @param baseXsds * @param curXsds * @param majorChange * @throws java.io.IOException */ private void checkXsdChangesInPkg(String pkgName, Map<String, IFile> baseXsds, Map<String, IFile> curXsds, VersionChangeReason majorChange) throws IOException { String reason; for (Map.Entry<String, IFile> file : baseXsds.entrySet()) { // scan the latest version of the class IFile curXsd = curXsds.get(file.getKey()); String changeClass = file.getValue().getName(); // check whether the xsd have been deleted or changed or added if (curXsd == null) { reason = "In the package " + pkgName + ", The schema file has been deleted: " + file.getKey() + "."; majorChange.update(reason, changeClass, false); break; } else { // check whether it is the same //read the current xsd file curXsds.remove(file.getKey()); String curFileContent = readXsdFile(curXsd.open()); String oldFileContent = readXsdFile(file.getValue().open()); if (!!!(curFileContent.equals(oldFileContent))) { reason = "In the package " + pkgName + ", The schema file has been updated: " + file.getKey() + "."; majorChange.update(reason, changeClass, false); break; } } } } /** * Check whether the package has gained additional class or xsd files. If yes, log a minor change. * * @param pkgName * @param curClazz * @param curXsds * @param minorChange */ private void checkAdditionalClassOrXsds(String pkgName, Map<String, IFile> curClazz, Map<String, IFile> curXsds, VersionChangeReason minorChange) { String reason; Collection<IFile> ifiles = curClazz.values(); Iterator<IFile> iterator = ifiles.iterator(); while (iterator.hasNext()) { IFile ifile = iterator.next(); String changeClass = ifile.getName(); SemanticVersioningClassVisitor cv = getVisitor(ifile, newJarsLoader); if (cv.getClassDeclaration() != null) { // If this is a public/protected class, it will need to increase the minor version of the package. minorChange.setChange(true); if (minorChange.isChange()) { reason = "The package " + pkgName + " has gained at least one class : " + getClassName(changeClass) + "."; minorChange.update(reason, changeClass, false); break; } } } if (!!!(minorChange.isChange() || curXsds.isEmpty())) { /// a new xsd file was added, it is a minor change IFile firstXsd = null; Iterator<IFile> xsdIterator = curXsds.values().iterator(); firstXsd = xsdIterator.next(); reason = "In the package " + pkgName + ", The schema file(s) are added: " + curXsds.keySet() + "."; minorChange.update(reason, firstXsd.getName(), false); } } static boolean isVersionCorrect(VERSION_CHANGE_TYPE status, String oldVersionStr, String newVersionStr) { boolean versionCorrect = false; Version oldVersion = Version.parseVersion(oldVersionStr); Version newVersion = Version.parseVersion(newVersionStr); if (status == VERSION_CHANGE_TYPE.MAJOR_CHANGE) { if (newVersion.getMajor() > oldVersion.getMajor()) { versionCorrect = true; } } else if (status == VERSION_CHANGE_TYPE.MINOR_CHANGE) { if ((newVersion.getMajor() > oldVersion.getMajor()) || (newVersion.getMinor() > oldVersion.getMinor())) { versionCorrect = true; } } else { if ((newVersion.getMajor() >= oldVersion.getMajor()) && (newVersion.getMinor() >= oldVersion.getMinor())) { versionCorrect = true; } } return versionCorrect; } private String getRecommendedVersion( VERSION_CHANGE_TYPE status, String oldVersionStr) { Version oldVersion = Version.parseVersion(oldVersionStr); Version recommendedNewVersion; if (status == BundleCompatibility.VERSION_CHANGE_TYPE.MAJOR_CHANGE) { recommendedNewVersion = new Version(oldVersion.getMajor() + 1, 0, 0); } else if (status == BundleCompatibility.VERSION_CHANGE_TYPE.MINOR_CHANGE) { recommendedNewVersion = new Version(oldVersion.getMajor(), oldVersion.getMinor() + 1, 0); } else { recommendedNewVersion = oldVersion; } return recommendedNewVersion.toString(); } private String getPkgStatusText(String pkgName, VERSION_CHANGE_TYPE status, String oldVersionStr, String newVersionStr, String reason, String key_class) { if (!isVersionCorrect(status, oldVersionStr, newVersionStr)) { return "The package " + pkgName + " has the following changes:" + reason + "\r\nThe package version should be " + getRecommendedVersion(status, oldVersionStr) + "."; } else { return ""; } } private String getClassName(String fullClassPath) { String[] chunks = fullClassPath.split("/"); String className = chunks[chunks.length - 1]; className = className.replace(SemanticVersioningUtils.classExt, SemanticVersioningUtils.javaExt); return className; } private String readXsdFile(InputStream is) { BufferedReader br = new BufferedReader(new InputStreamReader(is)); StringBuilder sb = new StringBuilder(); String line = null; try { while ((line = br.readLine()) != null) { sb.append(line); } } catch (IOException ioe) { IOUtils.close(br); } return sb.toString(); } private SemanticVersioningClassVisitor getVisitor(IFile file, URLClassLoader loader) { SerialVersionClassVisitor sv = new SerialVersionClassVisitor(null); SemanticVersioningClassVisitor oldcv = new SemanticVersioningClassVisitor(loader, sv); try { ClassReader cr = new ClassReader(file.open()); cr.accept(oldcv, 0); } catch (IOException ioe) { _logger.debug("The file " + file + "cannot be opened."); } return oldcv; } enum VERSION_CHANGE_TYPE { MAJOR_CHANGE("major"), MINOR_CHANGE("minor"), NO_CHANGE("no"); private final String text; VERSION_CHANGE_TYPE(String text) { this.text = text; } public String text() { return this.text; } } private static class PackageContent { private final String packageName; private final String packageVersion; private final Map<String, IFile> classes = new HashMap<String, IFile>(); private final Map<String, IFile> xsds = new HashMap<String, IFile>(); PackageContent(String pkgName, String pkgVersion) { packageName = pkgName; packageVersion = pkgVersion; } public void addClass(String className, IFile file) { classes.put(className, file); } public void addXsd(String className, IFile file) { xsds.put(className, file); } public Map<String, IFile> getClasses() { return classes; } public Map<String, IFile> getXsds() { return xsds; } public String getPackageVersion() { return packageVersion; } public String getPackageName() { return packageName; } } private static class VersionChangeReason { boolean change = false; String reason = null; String changeClass = null; boolean moreAbstractMethod = false; public boolean isMoreAbstractMethod() { return moreAbstractMethod; } public boolean isChange() { return change; } public void setChange(boolean change) { this.change = change; } public String getReason() { return reason; } public String getChangeClass() { return changeClass; } public void update(String reason, String changeClass, boolean moreAbstractMethod) { this.change = true; this.reason = reason; this.changeClass = changeClass; this.moreAbstractMethod = moreAbstractMethod; } } }