| /* |
| * 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.jackrabbit.filevault.maven.packaging.impl; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| 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.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.jar.Manifest; |
| |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.artifact.DependencyResolutionRequiredException; |
| import org.apache.maven.artifact.resolver.filter.ArtifactFilter; |
| import org.apache.maven.project.MavenProject; |
| import org.codehaus.plexus.util.DirectoryScanner; |
| import org.codehaus.plexus.util.StringUtils; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import aQute.bnd.header.Attrs; |
| import aQute.bnd.header.Parameters; |
| import aQute.bnd.osgi.Analyzer; |
| import aQute.bnd.osgi.Clazz; |
| import aQute.bnd.osgi.Constants; |
| import aQute.bnd.osgi.Descriptors; |
| import aQute.bnd.osgi.FileResource; |
| import aQute.bnd.osgi.Processor; |
| import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; |
| import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult; |
| |
| import static org.apache.commons.io.FileUtils.listFiles; |
| import static org.apache.commons.io.FilenameUtils.normalize; |
| |
| /** |
| * The import package builder is used to analyze the classes and dependencies of the project and calculate the |
| * import-package statement for this package. |
| */ |
| public class ImportPackageBuilder { |
| |
| /** |
| * class file directory |
| */ |
| private File classFileDirectory; |
| |
| /** |
| * list of class files. initialized during {@link #analyze()} |
| */ |
| private List<File> classFiles; |
| |
| /** |
| * list of artifacts relevant for analysis |
| */ |
| private List<Artifact> artifacts; |
| |
| /** |
| * BND analyzer |
| */ |
| private Analyzer analyzer; |
| |
| /** |
| * The scan result from the fast-classpath analyzer |
| */ |
| private ScanResult scanResult; |
| |
| /** |
| * artifact-id -> bundle info mapping |
| */ |
| private Map<String, BundleInfo> bundles = new HashMap<String, BundleInfo>(); |
| |
| /** |
| * map of all exported packaged by the bundles. |
| */ |
| private Map<String, PackageInfo> exported = new HashMap<String, PackageInfo>(); |
| |
| /** |
| * map of all classes |
| */ |
| private Map<String, ClassInfo> classes = new HashMap<String, ClassInfo>(); |
| |
| /** |
| * the calculated import parameters. |
| */ |
| private Map<String, Attrs> importParameters = Collections.emptyMap(); |
| |
| /** |
| * specifies if unused packages should be included if there are not classes in the project |
| */ |
| private boolean includeUnused; |
| |
| /** |
| * filter for project artifacts |
| */ |
| private ArtifactFilter filter = new ArtifactFilter() { |
| @Override |
| public boolean include(Artifact artifact) { |
| return true; |
| } |
| }; |
| |
| /** |
| * Sets the class files directory |
| * @param classes the directory |
| * @return this. |
| */ |
| @NotNull |
| public ImportPackageBuilder withClassFileDirectory(File classes) { |
| classFileDirectory = classes; |
| return this; |
| } |
| |
| /** |
| * defines the project from which the artifacts should be loaded. |
| * The current implementation requires the filter to be set before calling this method. |
| * @param project the maven project |
| * @return this |
| */ |
| @NotNull |
| public ImportPackageBuilder withDependenciesFromProject(@NotNull MavenProject project) { |
| artifacts = new ArrayList<Artifact>(); |
| for (Artifact a : project.getDependencyArtifacts()) { |
| if (!filter.include(a)) { |
| continue; |
| } |
| // skip all test dependencies (all other scopes are potentially relevant) |
| if (Artifact.SCOPE_TEST.equals(a.getScope())) { |
| continue; |
| } |
| // type of the considered dependencies must be either "jar" or "bundle" |
| if (!"jar".equals(a.getType()) && (!"bundle".equals(a.getType()))) { |
| continue; |
| } |
| artifacts.add(a); |
| } |
| return this; |
| } |
| |
| /** |
| * defines if unused packages should be included if no classes exist in the project. |
| * @param includeUnused {@code true} to include unused. |
| * @return this |
| */ |
| @NotNull |
| public ImportPackageBuilder withIncludeUnused(boolean includeUnused) { |
| this.includeUnused = includeUnused; |
| return this; |
| } |
| |
| /** |
| * defines the filter for the project artifact |
| * @param filter the filter |
| * @return this |
| */ |
| @NotNull |
| public ImportPackageBuilder withFilter(@NotNull ArtifactFilter filter) { |
| this.filter = filter; |
| return this; |
| } |
| |
| /** |
| * analyzes the imports |
| * @return this |
| * @throws IOException if an error occurrs. |
| */ |
| @NotNull |
| public ImportPackageBuilder analyze() throws IOException { |
| initClassFiles(); |
| initAnalyzer(); |
| scanClassPath(); |
| scanBundles(); |
| scanClasses(); |
| calculateImportParameters(); |
| return this; |
| } |
| |
| /** |
| * returns the import parameter header. only available after {@link #analyze()} |
| * @return the parameters |
| */ |
| @NotNull |
| public Map<String, Attrs> getImportParameters() { |
| return importParameters; |
| } |
| |
| /** |
| * generates a package report |
| * @return the report |
| */ |
| @NotNull |
| public String createExportPackageReport() { |
| TreeSet<String> unusedBundles = new TreeSet<String>(bundles.keySet()); |
| StringBuilder report = new StringBuilder("Export package report:\n\n"); |
| List<String> packages = new ArrayList<String>(exported.keySet()); |
| Collections.sort(packages); |
| int pad = 18; |
| for (String packageName : packages) { |
| pad = Math.max(pad, packageName.length()); |
| } |
| pad += 2; |
| |
| report |
| .append(StringUtils.rightPad("Exported packages", pad)) |
| .append(StringUtils.rightPad("Uses", 5)) |
| .append(StringUtils.rightPad("Version", 10)) |
| .append("Dependency\n"); |
| report.append(StringUtils.repeat("-", pad + 30)).append("\n"); |
| for (String packageName : packages) { |
| PackageInfo info = exported.get(packageName); |
| |
| report.append(StringUtils.rightPad(packageName, pad)); |
| report.append(StringUtils.rightPad(String.valueOf(info.usedBy.size()), 5)); |
| boolean first = true; |
| for (BundleInfo bInfo : info.bundles.values()) { |
| if (first) { |
| String version = bInfo.packageVersions.get(packageName); |
| if (StringUtils.isEmpty(version)) { |
| version = "0.0.0"; |
| } |
| report.append(StringUtils.rightPad(version, 10)); |
| report.append(bInfo.getId()); |
| first = false; |
| } |
| if (!info.usedBy.isEmpty()) { |
| unusedBundles.remove(bInfo.getId()); |
| } |
| } |
| if (first) { |
| report.append(StringUtils.rightPad("n/a", 10)); |
| } |
| report.append("\n"); |
| } |
| |
| report.append("\n").append(unusedBundles.size()).append(" unused bundles\n"); |
| report.append("------------------------------\n"); |
| for (String bundleId : unusedBundles) { |
| report.append(bundleId).append("\n"); |
| } |
| |
| report.append("\nPackages used in the analyzed classes: \n"); |
| report.append("------------------------------\n"); |
| for (Map.Entry<String, Attrs> e: importParameters.entrySet()) { |
| report.append(e.getKey()); |
| try { |
| Processor.printClause(e.getValue(), report); |
| } catch (IOException e1) { |
| throw new IllegalStateException("Internal error while generating report", e1); |
| } |
| report.append("\n"); |
| } |
| |
| return report.toString(); |
| } |
| |
| /** |
| * internally scans all the class files. |
| */ |
| private void initClassFiles() { |
| if (!classFileDirectory.exists()) { |
| classFiles = Collections.emptyList(); |
| return; |
| } |
| DirectoryScanner scanner = new DirectoryScanner(); |
| scanner.setBasedir(classFileDirectory); |
| scanner.setIncludes(new String[]{"**/*.class"}); |
| scanner.scan(); |
| String[] paths = scanner.getIncludedFiles(); |
| classFiles = new ArrayList<File>(paths.length); |
| for (String path : paths) { |
| File file = new File(path); |
| if (!file.isAbsolute()) { |
| file = new File(classFileDirectory, path); |
| } |
| classFiles.add(file); |
| } |
| } |
| |
| /** |
| * Returns the classloader for the analysis. this should exclude the classes of the maven runtime. |
| * |
| * @throws IOException If an error occurs |
| */ |
| private ClassLoader getClassLoader() throws IOException, |
| DependencyResolutionRequiredException { |
| List<URL> classPath = new ArrayList<URL>(); |
| |
| // add output directory to classpath |
| classPath.add(classFileDirectory.toURI().toURL()); |
| |
| // add artifacts from project |
| for (Artifact a: artifacts) { |
| classPath.add(a.getFile().toURI().toURL()); |
| } |
| |
| // use our parent as parent, in order to exclude the maven runtime. |
| return new URLClassLoader(classPath.toArray(new URL[classPath.size()]), this.getClass().getClassLoader().getParent()); |
| } |
| |
| /** |
| * scans the classpath |
| * @throws IOException if an error occurrs. |
| */ |
| private void scanClassPath() throws IOException { |
| try { |
| scanResult = new FastClasspathScanner() |
| .overrideClassLoaders(getClassLoader()) |
| .scan(); |
| |
| } catch (Exception e) { |
| throw new IOException("Failed to scan the classpath", e); |
| } |
| } |
| |
| /** |
| * initializes the bnd analyzer |
| */ |
| private void initAnalyzer() { |
| analyzer = new Analyzer(); |
| } |
| |
| /** |
| * scans all the bundles and initializes their export packages. |
| * @throws IOException if an error occurrs |
| */ |
| private void scanBundles() throws IOException { |
| for (Artifact a : artifacts) { |
| BundleInfo info = new BundleInfo(a); |
| bundles.put(info.getId(), info); |
| |
| // update the reverse map |
| for (String pkgName : info.packageVersions.keySet()) { |
| PackageInfo pkg = exported.get(pkgName); |
| if (pkg == null) { |
| pkg = new PackageInfo(pkgName); |
| exported.put(pkgName, pkg); |
| } |
| pkg.bundles.put(info.getId(), info); |
| } |
| } |
| } |
| |
| /** |
| * Registers the package reference from the given class |
| * @param info the class info that references the package |
| * @param pkgName the package that is referenced |
| */ |
| private void registerPackageReference(ClassInfo info, String pkgName) { |
| PackageInfo pkgInfo = exported.get(pkgName); |
| if (pkgInfo == null) { |
| pkgInfo = new PackageInfo(pkgName); |
| exported.put(pkgName, pkgInfo); |
| } |
| info.resolved.put(pkgName, pkgInfo); |
| pkgInfo.usedBy.add(info.getName()); |
| } |
| |
| /** |
| * scans the classes and resolves them against the bundles. |
| * @throws IOException if an error occurrs. |
| */ |
| private void scanClasses() throws IOException { |
| for (File file : classFiles) { |
| try { |
| Clazz clazz = new Clazz(analyzer, file.getPath(), new FileResource(file)); |
| clazz.parseClassFile(); |
| ClassInfo info = new ClassInfo(clazz); |
| classes.put(info.getName(), info); |
| |
| String myPackage = getPackageName(info.getName()); |
| for (Descriptors.PackageRef ref : clazz.getReferred()) { |
| String importPkgName = ref.getFQN(); |
| if (!importPkgName.equals(myPackage)) { |
| registerPackageReference(info, importPkgName); |
| } |
| } |
| |
| // checking for super classes |
| io.github.lukehutch.fastclasspathscanner.scanner.ClassInfo clzInfo = scanResult.getClassNameToClassInfo().get(clazz.getFQN()); |
| if (clzInfo != null) { |
| for (String name: clzInfo.getNamesOfImplementedInterfaces()) { |
| registerPackageReference(info, getPackageName(name)); |
| } |
| for (String name: clzInfo.getNamesOfSuperclasses()) { |
| registerPackageReference(info, getPackageName(name)); |
| } |
| } |
| } catch (Exception e) { |
| throw new IOException("Error while parsing class: " + file.getPath(), e); |
| } |
| } |
| } |
| |
| /** |
| * Returns the package name for the given class name |
| * @param className the class name |
| * @return the package name |
| */ |
| private static String getPackageName(String className) { |
| return StringUtils.chomp(className, "."); |
| } |
| |
| /** |
| * Calculates returns the import parameter header. |
| */ |
| private void calculateImportParameters() { |
| importParameters = new TreeMap<String, Attrs>(); |
| for (PackageInfo info : exported.values()) { |
| if (!classFiles.isEmpty() && info.usedBy.isEmpty()) { |
| // skip if not used. |
| continue; |
| } |
| if (classFiles.isEmpty() && !includeUnused) { |
| continue; |
| } |
| if (info.bundles.isEmpty()) { |
| // skip if no bundle |
| continue; |
| } |
| // get first version |
| BundleInfo bInfo = info.bundles.values().iterator().next(); |
| String version = bInfo.packageVersions.get(info.getName()); |
| Attrs options = new Attrs(); |
| if (!StringUtils.isEmpty(version)) { |
| options.put(Constants.VERSION_ATTRIBUTE, new aQute.bnd.version.VersionRange("@" + version).toString()); |
| } |
| importParameters.put(info.getName(), options); |
| } |
| } |
| |
| |
| private static class PackageInfo { |
| |
| private final String name; |
| |
| private final Map<String, BundleInfo> bundles = new HashMap<String, BundleInfo>(); |
| |
| private final Set<String> usedBy = new HashSet<String>(); |
| |
| private PackageInfo(String name) { |
| this.name = name; |
| } |
| |
| public String getName() { |
| return name; |
| } |
| } |
| |
| private static class BundleInfo { |
| |
| private final String id; |
| |
| private final Map<String, String> packageVersions = new HashMap<String, String>(); |
| |
| private BundleInfo(Artifact artifact) throws IOException { |
| id = artifact.getId(); |
| File file = artifact.getFile(); |
| |
| // In case of an internal dependency in a multi-module project, the dependency may be represented by a directory |
| // rather than a JAR file if the maven lifecycle phase does not include binding the JAR file to the dependency. |
| Dependency dependency = file.isDirectory() ? new DirectoryDependency(file) : new JarBasedDependency(file); |
| |
| Manifest manifest = dependency.getManifest(); |
| String exportPackages = manifest == null ? null : manifest.getMainAttributes().getValue(Constants.EXPORT_PACKAGE); |
| if (exportPackages != null) { |
| for (Map.Entry<String, Attrs> entry : new Parameters(exportPackages).entrySet()) { |
| Attrs options = entry.getValue(); |
| String version = options.getVersion(); |
| packageVersions.put(entry.getKey(), version == null ? "" : version); |
| } |
| } else { |
| // scan the class files and associate the version |
| for (String path : dependency.getClassFiles()) { |
| // skip internal / impl |
| if (path.contains("/impl/") || path.contains("/internal/")) { |
| continue; |
| } |
| path = StringUtils.chomp(path, "/"); |
| if (path.charAt(0) == '/') { |
| path = path.substring(1); |
| } |
| String packageName = path.replaceAll("/", "."); |
| packageVersions.put(packageName, ""); |
| } |
| } |
| } |
| |
| public String getId() { |
| return id; |
| } |
| } |
| |
| private interface Dependency { |
| |
| /** |
| * Returns the Manifest of the dependency. |
| * @return the Manifest. |
| */ |
| @Nullable |
| Manifest getManifest() throws IOException; |
| |
| /** |
| * Returns the paths representing .class files in their java package directory with *nix-style path separators, |
| * e.g. {@code /some/package/name/ClassFile.class}. |
| * @return the paths. |
| */ |
| @NotNull |
| Collection<String> getClassFiles() throws IOException; |
| } |
| |
| /** |
| * Represents the JAR file of an {@link Artifact} dependency. |
| */ |
| private static class JarBasedDependency implements Dependency { |
| |
| private final File file; |
| |
| private JarBasedDependency(File file) { |
| this.file = file; |
| } |
| |
| @Override |
| @Nullable |
| public Manifest getManifest() throws IOException { |
| try (JarFile jarFile = new JarFile(this.file)) { |
| return jarFile.getManifest(); |
| } |
| } |
| |
| @Override |
| @NotNull |
| public Collection<String> getClassFiles() throws IOException { |
| List<String> fileNames = new LinkedList<>(); |
| try (JarFile jar = new JarFile(this.file)) { |
| Enumeration<JarEntry> entries = jar.entries(); |
| while (entries.hasMoreElements()) { |
| JarEntry e = entries.nextElement(); |
| if (e.isDirectory()) { |
| continue; |
| } |
| String path = e.getName(); |
| if (path.endsWith(".class")) { |
| fileNames.add(path); |
| } |
| } |
| } |
| |
| return fileNames; |
| } |
| } |
| |
| /** |
| * Represents an {@link Artifact} dependency to another module of a multi-module setup |
| * which may point to the target/classes directory of the module rather than a jar file. |
| */ |
| private static class DirectoryDependency implements Dependency { |
| |
| private final File directory; |
| |
| private DirectoryDependency(File directory) { |
| this.directory = directory; |
| } |
| |
| @Override |
| @Nullable |
| public Manifest getManifest() throws IOException { |
| File manifest = new File(this.directory, "META-INF/MANIFEST.MF"); |
| if (!manifest.exists()) { |
| return null; |
| } |
| try (InputStream in = new FileInputStream(manifest)) { |
| return new Manifest(in); |
| } |
| } |
| |
| @Override |
| @NotNull |
| public Collection<String> getClassFiles() throws IOException { |
| Collection<File> files = listFiles(this.directory, new String[]{"class"}, true); |
| String basePath = this.directory.getCanonicalPath(); |
| Collection<String> fileNames = new ArrayList<>(files.size()); |
| for (File file : files) { |
| // Use the relative file path as the the path segments will be used |
| // to calculate the package name. |
| String relativePath = file.getCanonicalPath().substring(basePath.length()); |
| // The path may contain platform-specific paths that must be normalized to unix paths. |
| fileNames.add(normalize(relativePath, true)); |
| } |
| return fileNames; |
| } |
| } |
| |
| private static class ClassInfo { |
| |
| private final Clazz clazz; |
| |
| private final Map<String, PackageInfo> resolved = new HashMap<String, PackageInfo>(); |
| |
| private ClassInfo(Clazz clazz) { |
| this.clazz = clazz; |
| } |
| |
| public String getName() { |
| return clazz.getFQN(); |
| } |
| } |
| } |