/* * $Id$ * * Copyright (C) 2003-2015 JNode.org * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; If not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.jnode.build.dependencies; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.bcel.classfile.Constant; import org.apache.bcel.classfile.ConstantClass; import org.apache.bcel.classfile.ConstantNameAndType; import org.apache.bcel.classfile.ConstantPool; import org.apache.bcel.classfile.Field; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.classfile.Method; import org.apache.bcel.generic.Type; import org.apache.bcel.util.ClassPath; import org.apache.bcel.util.SyntheticRepository; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.types.FileSet; import org.jnode.build.AbstractPluginTask; import org.jnode.plugin.FragmentDescriptor; import org.jnode.plugin.PluginDescriptor; import org.jnode.plugin.PluginPrerequisite; import org.jnode.plugin.PluginReference; /** * Task used to check dependencies between plugins. * * @author Fabien DUMINY * @author Sebastian Lohmeier */ public class PluginDependencyChecker extends AbstractPluginTask { private List<FileSet> descriptorSets = new ArrayList<FileSet>(256); private List<FileSet> pluginSets = new ArrayList<FileSet>(256); private List<Plugin> plugins; private List<Fragment> fragments; private Set<Plugin> systemPlugins; public FileSet createDescriptors() { final FileSet fs = new FileSet(); descriptorSets.add(fs); return fs; } public FileSet createPlugins() { final FileSet fs = new FileSet(); pluginSets.add(fs); return fs; } /** * @throws BuildException * @see org.apache.tools.ant.Task#execute() */ public void execute() throws BuildException { if (descriptorSets.isEmpty()) { throw new BuildException("At least 1 descriptors element is required"); } if (pluginSets.isEmpty()) { throw new BuildException("At least 1 plugins element is required"); } Map<String, Plugin> containedClasses = new HashMap<String, Plugin>(); if (createPlugins(containedClasses) > 0) { System.err.println("\nCan't proceed with more in-depth checks. Please fix above errors first."); } else { analyzePlugins(containedClasses, plugins); } } private Plugin findPlugin(String fullPluginId) { // TODO ... consider replacing this with a map. for (Plugin plugin : plugins) { if (plugin.fullPluginId.equals(fullPluginId)) { return plugin; } } return null; } private Set<Fragment> findFragmentsOwnedByPlugin(Plugin plugin) { String fullPluginId = plugin.getFullPluginId(); Set<Fragment> results = new HashSet<Fragment>(); for (Fragment fragment : fragments) { if (fragment.getFullPluginIdOfOwningPlugin().equals(fullPluginId)) { results.add(fragment); } } return results; } private Collection<Plugin> findSystemPlugins() { if (systemPlugins != null) { return systemPlugins; } else { systemPlugins = new HashSet<Plugin>(); for (Plugin plugin : plugins) { if (plugin.isSystemPlugin()) { systemPlugins.add(plugin); } } return systemPlugins; } } /** * @param containedClasses * @param plugins */ private void analyzePlugins(Map<String, Plugin> containedClasses, List<Plugin> plugins) { Collections.sort(plugins); for (Plugin plugin : plugins) { analyzePlugin(containedClasses, plugin); } } /** * @param containedClasses * @param plugin */ private void analyzePlugin(Map<String, Plugin> containedClasses, Plugin plugin) { plugin.verifyDescriptor(containedClasses); } /** * Creates the Plugin instances, reading and parsing the XML descriptors * and the JAR files of all plugins. * * @param containedClasses * @return The number of errors found. */ private int createPlugins(Map<String, Plugin> containedClasses) { JarFiles jarFiles = new JarFiles(pluginSets); plugins = new ArrayList<Plugin>(); fragments = new ArrayList<Fragment>(); int errorCount = 0; for (File descFile : getDescriptorFiles()) { Plugin plugin = processPlugin(descFile, jarFiles); if (plugin != null) { plugins.add(plugin); if (plugin instanceof Fragment) { fragments.add((Fragment) plugin); } for (String containedClass : plugin.containedClasses) { if (!containedClasses.containsKey(containedClass)) { containedClasses.put(containedClass, plugin); } else { System.err.println("WARNING: Class " + containedClass + " contained in both plugins:"); System.err.println(containedClasses.get(containedClass).getFullPluginId() + " and " + plugin.getFullPluginId()); errorCount++; } } } } return errorCount; } protected File[] getDescriptorFiles() { List<File> files = new ArrayList<File>(); for (FileSet fs : descriptorSets) { final DirectoryScanner ds = fs.getDirectoryScanner(getProject()); for (String incFile : ds.getIncludedFiles()) { files.add(new File(ds.getBasedir(), incFile)); } } return files.toArray(new File[files.size()]); } /** * @param jarFiles * @param descriptor * @throws BuildException */ protected Plugin processPlugin(File descriptor, JarFiles jarFiles) throws BuildException { try { final PluginDescriptor descr = readDescriptor(descriptor); String fullPluginId = descr.getId() + "_" + descr.getVersion(); if (!jarFiles.hasPluginJar(fullPluginId)) { System.out.println("WARNING: no Jar file found for plugin " + fullPluginId); return null; } if (descr instanceof FragmentDescriptor) { return new Fragment(jarFiles.getPluginJar(fullPluginId), (FragmentDescriptor) descr); } else { return new Plugin(jarFiles.getPluginJar(fullPluginId), descr); } } catch (IOException e) { throw new BuildException(e); } } private class Plugin implements Comparable<Plugin> { private final String classSuffix = ".class"; private final Pattern typePattern = Pattern.compile("\u004C[a-zA-Z_0-9/\u002E\u0024]*;"); protected final String fullPluginId; protected final PluginDescriptor descr; protected final Set<String> containedClasses = new HashSet<String>(); protected final Map<String, Set<String>> usedClasses = new HashMap<String, Set<String>>(); private Plugin(JarFile jarFile, PluginDescriptor descr) { this.fullPluginId = createFullPluginId(descr.getPluginReference()); this.descr = descr; initContainedClasses(jarFile); initUsedClasses(jarFile); } private boolean isSystemPlugin() { return descr.isSystemPlugin(); } protected String createFullPluginId(PluginReference reference) { return reference.getId() + "_" + reference.getVersion(); } protected String getFullPluginId() { return fullPluginId; } protected void verifyDescriptor(Map<String, Plugin> containedClasses) { boolean errorFound = false; StringBuffer buffer = new StringBuffer("The plugin " + fullPluginId + ""); buffer.append("\n------------------------------------------------------------------\n"); errorFound |= collectUnmatchedDependencies(buffer, containedClasses); errorFound |= isNotUseful(buffer); Set<String> allClasses = getAllUsedClasses(); errorFound |= assortClassesContainedInImportedPlugins(buffer, this, true, allClasses); for (Plugin plugin : findSystemPlugins()) { allClasses.removeAll(plugin.containedClasses); } //errorFound |= isMissingImportDeclarations(buffer, allClasses); if (errorFound) { System.out.println(buffer.toString()); } } private boolean isNotUseful(StringBuffer buffer) { if (descr.getRuntime() != null) return false; if ((descr.getExtensions() != null) && (descr.getExtensions().length > 0)) return false; if (descr.hasCustomPluginClass()) return false; buffer.append(" * neither exports classes, nor is an extension, nor provides a plugin class\n"); return true; } /** * @param buffer * @param containedClasses */ private boolean collectUnmatchedDependencies(StringBuffer buffer, Map<String, Plugin> containedClasses) { Map<String, List<String>> unmatchedDependencies = new HashMap<String, List<String>>(); for (String usingClass : usedClasses.keySet()) { for (String className : usedClasses.get(usingClass)) { if (!containedClasses.containsKey(className)) { if (unmatchedDependencies.containsKey(className)) { unmatchedDependencies.get(className).add(usingClass); } else { List<String> list = new ArrayList<String>(); list.add(usingClass); unmatchedDependencies.put(className, list); } } } } if (!unmatchedDependencies.isEmpty()) { dumpUnmatchedDependencies(buffer, unmatchedDependencies); return true; } else { return false; } } /** * @param buffer * @param unmatchedDependencies */ private void dumpUnmatchedDependencies(StringBuffer buffer, Map<String, List<String>> unmatchedDependencies) { buffer.append(" * has unresolved class dependencies:\n"); List<String> keys = new ArrayList<String>(unmatchedDependencies.keySet()); Collections.sort(keys); for (String className : keys) { List<String> usedClasses = unmatchedDependencies.get(className); Collections.sort(usedClasses); buffer.append(" " + className + "\n"); boolean first = true; for (String usedClass : usedClasses) { buffer.append(" " + (first ? "is used by " : "and ") + usedClass + "\n"); first = false; } } } private boolean assortClassesContainedInImportedPlugins(StringBuffer buffer, Plugin plugin, boolean isFirst, Set<String> allClasses) { boolean error = false; Map<String, Plugin> usedPlugins = new HashMap<String, Plugin>(); if (plugin instanceof Fragment) { String idOfOwningPlugin = ((Fragment) plugin).getFullPluginIdOfOwningPlugin(); usedPlugins.put(idOfOwningPlugin, findPlugin(idOfOwningPlugin)); } for (Fragment fragment : findFragmentsOwnedByPlugin(plugin)) { usedPlugins.put(fragment.getFullPluginId(), fragment); } for (PluginPrerequisite prerequisite : plugin.descr.getPrerequisites()) { String idOfUsedPlugin = createFullPluginId(prerequisite.getPluginReference()); usedPlugins.put(idOfUsedPlugin, findPlugin(idOfUsedPlugin)); } for (String idOfUsedPlugin : usedPlugins.keySet()) { Plugin usedPlugin = usedPlugins.get(idOfUsedPlugin); if (usedPlugin == null) { buffer.append(" * references unknown plugin " + idOfUsedPlugin + "\n"); error = true; continue; } if (usedPlugin.containedClasses.size() > 0 && !allClasses.removeAll(usedPlugin.containedClasses)) { if (isFirst && !(plugin instanceof Fragment) && (usedPlugin.descr.getPrerequisites() == null || usedPlugin.descr.getPrerequisites().length == 0)) { buffer.append(" * references plugin " + idOfUsedPlugin + ", which is not actually required\n"); error = true; // don't need to analyze recursively, since the plugin // imports no other plugins } } else { // recursively analyze this plugin ... error |= assortClassesContainedInImportedPlugins(buffer, usedPlugin, false, allClasses); } } return error; } @SuppressWarnings("unused") private boolean isMissingImportDeclarations(StringBuffer buffer, Set<String> allClasses) { if (!allClasses.isEmpty()) { buffer.append(" * is missing import declarations covering the following classes: " + allClasses + "\n"); return true; } else { return false; } } private Set<String> getAllUsedClasses() { HashSet<String> allClasses = new HashSet<String>(); for (Set<String> classNames : usedClasses.values()) { allClasses.addAll(classNames); } return allClasses; } private void initContainedClasses(JarFile jarFile) { Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String entryName = entry.getName(); if (!entry.isDirectory() && entryName.endsWith(classSuffix)) { String name = entryName.substring(0, entryName.length() - classSuffix.length()); if (name.indexOf(".") != -1) { System.err.println(name); } name = name.replace('/', '.'); name = name.replace('\\', '.'); containedClasses.add(name); } } } private void initUsedClasses(JarFile jarFile) { SyntheticRepository repository = SyntheticRepository.getInstance(new ClassPath(jarFile.getName())); for (String className : containedClasses) { try { analyzeClass(className, repository.loadClass(className)); } catch (ClassNotFoundException cnfe) { cnfe.printStackTrace(); } } } private void analyzeClass(String className, JavaClass javaClass) { analyzeSuperClass(className, javaClass); analyzeInterfaces(className, javaClass); analyzeFields(className, javaClass); analyzeMethods(className, javaClass); analyzeConstantPool(className, javaClass); } private void analyzeSuperClass(String usingClass, JavaClass javaClass) { addUsedClass(usingClass, javaClass.getSuperclassName()); } private void analyzeInterfaces(String usingClass, JavaClass javaClass) { addUsedClasses(usingClass, javaClass.getInterfaceNames()); } private void analyzeFields(String usingClass, JavaClass javaClass) { for (Field field : javaClass.getFields()) { addUsedClasses(usingClass, decodeSignature(field.getType().getSignature())); } } private void analyzeMethods(String usingClass, JavaClass javaClass) { for (Method method : javaClass.getMethods()) { for (Type argument : method.getArgumentTypes()) { String typeName = decodeTypeName(argument.getSignature()); if (typeName != null) { addUsedClass(usingClass, typeName); } } addUsedClasses(usingClass, decodeSignature(method.getReturnType().getSignature())); } } private void analyzeConstantPool(String usingClass, JavaClass javaClass) { final ConstantPool constantPool = javaClass.getConstantPool(); for (Constant constant : constantPool.getConstantPool()) { if (constant instanceof ConstantClass) { String signature = ((ConstantClass) constant).getBytes(constantPool); if (signature != null) { if (signature.startsWith("[")) { signature = decodeTypeName(signature); } if (signature != null) { addUsedClass(usingClass, signature); } } } else if (constant instanceof ConstantNameAndType) { for (String typeName : decodeSignature( ((ConstantNameAndType) constant).getSignature(constantPool))) { if (typeName != null) { addUsedClass(usingClass, typeName); } } } } } private void addUsedClasses(String usingClass, Collection<String> classNames) { for (String className : classNames) { addUsedClass(usingClass, className); } } private void addUsedClasses(String usingClass, String[] classNames) { for (String className : classNames) { addUsedClass(usingClass, className); } } private void addUsedClass(String usingClass, String usedClass) { usedClass = usedClass.replace('/', '.'); if (!containedClasses.contains(usedClass)) { if (usedClasses.containsKey(usingClass)) { usedClasses.get(usingClass).add(usedClass); } else { Set<String> set = new HashSet<String>(); set.add(usedClass); usedClasses.put(usingClass, set); } } } private String decodeTypeName(String signature) { Matcher matcher = typePattern.matcher(signature); if (matcher.find()) { String objectTypeName = matcher.group(); return objectTypeName.substring(1, objectTypeName.length() - 1); } else { return null; } } /** * @return Returns a list of classnames referenced by the * signature. */ private List<String> decodeSignature(String signature) { return getObjectTypeNames(signature); } private List<String> getObjectTypeNames(String signature) { List<String> objectTypeNames = new ArrayList<String>(); Matcher matcher = typePattern.matcher(signature); while (matcher.find()) { String objectTypeName = matcher.group(); objectTypeName = objectTypeName.substring(1, objectTypeName.length() - 1); objectTypeNames.add(objectTypeName); } return objectTypeNames; } public String toString() { return "Plugin " + fullPluginId + " contained=" + containedClasses + " used=" + usedClasses; } @Override public int compareTo(Plugin other) { return fullPluginId.compareTo(other.fullPluginId); } } private class Fragment extends Plugin { private String fullPluginIdOfOwningPlugin; private Fragment(JarFile jarFile, FragmentDescriptor descr) { super(jarFile, descr); this.fullPluginIdOfOwningPlugin = createFullPluginId(descr.getPluginReference()); } private String getFullPluginIdOfOwningPlugin() { return fullPluginIdOfOwningPlugin; } public String toString() { return "Fragment " + fullPluginId + " owned by=" + fullPluginIdOfOwningPlugin + " contained=" + containedClasses + " used=" + usedClasses; } } private class JarFiles { /** * Maps plugin names to paths of jar files. */ private final Map<String, String> jarFiles = new HashMap<String, String>(); private JarFiles(List<FileSet> pluginSets) { for (FileSet fs : pluginSets) { addFileSet(fs); } } private void addFileSet(FileSet fileSet) { final DirectoryScanner ds = fileSet.getDirectoryScanner(getProject()); final String[] files = ds.getIncludedFiles(); String baseDir = ds.getBasedir().getAbsolutePath(); //System.err.println("getAllJars() iterating ... basedir="+baseDir+" files.length="+files.length); for (String file : files) { jarFiles.put(file, baseDir + File.separatorChar + file); } } private boolean hasPluginJar(String fullPluginId) { return jarFiles.containsKey(fullPluginId + ".jar"); } private JarFile getPluginJar(String fullPluginId) throws IOException { return new JarFile(jarFiles.get(fullPluginId + ".jar")); } } }