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

org.graalvm.buildtools.maven.sbom.ArtifactAdapterResolver Maven / Gradle / Ivy

Go to download

Plugin that provides support for building and testing of GraalVM native images (ahead-of-time compiled Java code)

The newest version!
/*
 * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.graalvm.buildtools.maven.sbom;

import org.apache.maven.model.Plugin;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.xml.Xpp3Dom;

import java.io.*;
import java.nio.file.FileSystem;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
 * Class that tries to resolve the additional fields of {@link ArtifactAdapter}, including the pacakge names, jar path,
 * and if it is prunable.
 */
final class ArtifactAdapterResolver {
    private final MavenProject mavenProject;
    /**
     * The shade plugin for this {@link ArtifactAdapterResolver#mavenProject} if used, otherwise null.
     */
    private final Plugin shadePlugin;
    /**
     * Set of possible directory paths containing class files in a jar file system. Examples of the keys are:
     * "org/json" and "org/apache/commons/collections/map".
     */
    private final Set pathToClassFilesDirectories;
    private final Set visitedPathToClassFileDirectories;
    private final String mainClass;
    private static final String mavenShadePluginName = "maven-shade-plugin";

    ArtifactAdapterResolver(MavenProject mavenProject, String mainClass) {
        this.mavenProject = mavenProject;
        this.shadePlugin = getShadePluginIfUsed(mavenProject);
        this.pathToClassFilesDirectories = new HashSet<>();
        this.visitedPathToClassFileDirectories = new HashSet<>();
        this.mainClass = mainClass;
    }

    /**
     * This method tries to populate the extra fields of the {@link ArtifactAdapter}, namely:
     * {@link ArtifactAdapter#packageNames}, {@link ArtifactAdapter#jarPath}, and {@link ArtifactAdapter#prunable}.
     * The method tries to derive the package names as Native Image will see it as it encounters class files,
     * meaning the final package names (possibly affected by shading) and the final jar path (possibly a fat
     * or shaded jar).
     *
     * NOTE:
     * - To improve chances of successful resolution, it is important to call this method with the main
     * artifact last.
     * - Should not be called with the same artifact more than once.
     * - Shaded dependencies to the main artifact are not handled. Currently, we disable any pruning by
     * Native Image of shaded dependencies since we cannot guarantee its correctness.
     *
     * @param jarPath  the jar path as reported by the original Artifact.
     * @param artifact the artifact with its class files inside the {@param jarPath}.
     * @return a new path to the directory containing the class file of this shaded artifact (if it is one).
     */
    Optional populateWithAdditionalFields(Path jarPath, ArtifactAdapter artifact) throws IOException {
        if (!Files.exists(jarPath) || !jarPath.toString().endsWith(".jar")) {
            return Optional.empty();
        }

        /* If the shade plugin is not used, then we are not dealing with a fat or shaded jar. */
        if (shadePlugin == null) {
            return handleNonShadedCase(artifact, jarPath);
        }

        /* Recover the path to the shaded jar by querying the shade plugin object. */
        Optional optionalShadedJarPath = getShadedJarPath();
        if (optionalShadedJarPath.isEmpty()) {
            return handleNonShadedCase(artifact, jarPath);
        }

        /* Check if artifact is part of the shading. */
        Path shadedJarPath = optionalShadedJarPath.get();
        FileSystem jarFileSystem = getOrCreateFileSystem(shadedJarPath);
        if (!isPartOfJar(jarFileSystem, artifact)) {
            return handleNonShadedCase(artifact, jarPath);
        }

        /* Derive the directories of this artifact containing class files and retrieve the package names from those files. */
        Optional> optionalClassFileDirectories = resolveArtifactClassFileDirectories(jarFileSystem, jarPath, artifact);
        if (optionalClassFileDirectories.isPresent()) {
            Set classFileDirectories = optionalClassFileDirectories.get();
            Set packageNames = new HashSet<>();
            for (var directory : classFileDirectories) {
                Set newPackageNames = FileWalkerUtility.collectPackageNamesFromFileSystem(jarFileSystem, directory)
                        .orElse(Set.of());
                packageNames.addAll(newPackageNames);
            }
            artifact.setPackageNames(packageNames);
            artifact.setJarPath(shadedJarPath.toUri());
            return Optional.of(artifact);
        }   
        return Optional.empty();
    }

    static void markShadedArtifactsAsNonPrunable(Set artifacts) throws IOException {
        for (ArtifactAdapter artifact : artifacts) {
            if (isShaded(artifact)) {
                artifact.prunable = false;
            }
        }
    }

    private static boolean isShaded(ArtifactAdapter artifact) throws IOException {
        if (artifact.jarPath == null) {
            return false;
        }

        FileSystem jarFileSystem = getOrCreateFileSystem(Paths.get(artifact.jarPath));
        Optional optionalMetaInfPath = getMetaInfArtifactPath(jarFileSystem, artifact);
        if (optionalMetaInfPath.isEmpty()) {
            return false;
        }

        Path metaInfPath = optionalMetaInfPath.get();
        Path pomPath = jarFileSystem.getPath(metaInfPath.toString(), "pom.xml");
        try (InputStream pomInputStream = Files.newInputStream(pomPath);
             BufferedReader reader = new BufferedReader(new InputStreamReader(pomInputStream))) {
            return reader.lines()
                    .anyMatch(line -> line.contains(String.format("%s", mavenShadePluginName)));
        } catch (IOException e) {
            return false;
        }
    }

    private Optional handleNonShadedCase(ArtifactAdapter artifactAdapter, Path jarPath) throws IOException {
        FileSystem fileSystem = getOrCreateFileSystem(jarPath);
        Set packageNames = FileWalkerUtility.collectPackageNamesFromFileSystem(fileSystem, fileSystem.getPath("/")).orElse(Set.of());
        artifactAdapter.setPackageNames(packageNames);
        artifactAdapter.setJarPath(jarPath.toUri());
        return Optional.of(artifactAdapter);
    }

    private Optional getShadedJarPath() {
        Path targetDirectory = Paths.get(mavenProject.getBuild().getDirectory());

        Optional outputFile = getParameterFromPlugin(shadePlugin, "outputFile");
        if (outputFile.isPresent()) {
            Path outputPath = Paths.get(outputFile.get());
            if (Files.exists(outputPath)) {
                return Optional.of(outputPath);
            }
        }

        Optional finalName = getParameterFromPlugin(shadePlugin, "finalName");
        if (finalName.isPresent()) {
            Path finalJarPath = targetDirectory.resolve(finalName.get() + ".jar");
            if (Files.exists(finalJarPath)) {
                return Optional.of(finalJarPath);
            }
        }

        Path defaultJarPath = targetDirectory.resolve(mavenProject.getArtifactId() + "-" + mavenProject.getVersion() + ".jar");
        if (Files.exists(defaultJarPath)) {
            return Optional.of(defaultJarPath);
        }

        return Optional.empty();
    }

    private boolean isPartOfJar(FileSystem jarFileSystem, ArtifactAdapter artifact) throws IOException {
        Optional optionalMetaInfPath = getMetaInfArtifactPath(jarFileSystem, artifact);
        if (optionalMetaInfPath.isEmpty()) {
            return false;
        }
        Path metaInfPath = optionalMetaInfPath.get();

        /* Handle case where there are multiple versions under this artifact. */
        try (DirectoryStream stream = Files.newDirectoryStream(metaInfPath)) {
            int versionCount = 0;
            for (Path path : stream) {
                if (Files.isDirectory(path)) {
                    versionCount++;
                }
            }

            if (versionCount > 1) {
                Path versionedPath = metaInfPath.resolve(artifact.version);
                return Files.isDirectory(versionedPath);
            } else {
                return true;
            }
        }
    }

    private static Optional getMetaInfArtifactPath(FileSystem jarFileSystem, ArtifactAdapter artifact) {
        Path path = jarFileSystem.getPath("META-INF", "maven", artifact.groupId, artifact.artifactId);
        if (!Files.isDirectory(path)) {
            return Optional.empty();
        }
        return Optional.of(path);
    }

    /**
     * Finds the paths to the directories containing the class files for the {@param artifact} inside the jar.
     * For example, if {@param artifact} represents commons-validator and the content of a fat jar looks like this:
     *
     * 
     * org/
     * ├── apache/
     * │   ├── commons/
     * │   │   ├── validator/
     * │   │   │   ├── UrlValidator.class
     * │   │   │   ├── routines/
     * │   │   │   │   └── ValidatorUtils.class
     * │   │   │   ├── util/
     * │   │   │   │   └── Flags.class
     * │   │   ├── digester/
     * │   │   │   ├── plugins/
     * │   │   │   │   └── strategies/
     * │   │   │   │       └── DigesterPlugin.class
     * ├── json/
     * │   └── org/
     * │       └── json/
     * │           └── JSONObject.class
     * META-INF/...
     * 
* * Then the method would return only the path to the directories of commons-validator: * *
     * org/apache/commons/validator/
     * org/apache/commons/validator/routines/
     * org/apache/commons/validator/util/
     * 
* * NOTE: partial relocations--when a subset of the class files are relocated--is not supported and * {@link Optional#empty()} is returned in these cases. * * @param jarFileSystem The filesystem representing the JAR. * @param jarPath The path within the JAR file to search. * @param artifact The artifact whose class directories should be found. * @return A list of paths containing the class files for the artifact. * @throws IOException if an error occurs while reading the JAR. */ private Optional> resolveArtifactClassFileDirectories(FileSystem jarFileSystem, Path jarPath, ArtifactAdapter artifact) throws IOException { if (pathToClassFilesDirectories.isEmpty()) { Set potentialDirectories = resolveDirectoriesContainingClassFiles(jarFileSystem.getPath("/")); if (potentialDirectories.isEmpty()) { return Optional.empty(); } pathToClassFilesDirectories.addAll(potentialDirectories); } if (pathToClassFilesDirectories.size() == 1) { Path onlyPossiblePath = pathToClassFilesDirectories.stream().findFirst().get(); visitedPathToClassFileDirectories.add(onlyPossiblePath); return Optional.of(Set.of(onlyPossiblePath)); } /* If all but one path has been visited, then that path must be the correct one for this artifact. */ Set difference = notVisitedPaths(); if (difference.size() == 1) { Path onlyPossiblePath = difference.stream().findFirst().get(); visitedPathToClassFileDirectories.add(onlyPossiblePath); return Optional.of(Set.of(onlyPossiblePath)); } /* * Try to match directly with the GAV coordinates. */ Optional resolvedPath = tryResolveUsingGAVCoordinates(jarFileSystem.getPath("/"), artifact); if (resolvedPath.isPresent()) { return Optional.of(Set.of(resolvedPath.get())); } boolean isMainArtifact = artifact.equals(mavenProject.getArtifact()); if (isMainArtifact) { resolvedPath = findTopClassDirectory(jarFileSystem.getPath("/"), mainClass); if (resolvedPath.isPresent()) { return Optional.of(Set.of(resolvedPath.get())); } resolvedPath = tryResolveUsingGAVCoordinates(jarFileSystem.getPath("/"), artifact); return resolvedPath.map(Set::of); } /* * To derive the directory path when relocation is used we apply a matching strategy on the class names. * We collect the class file names of the original jar and searches the directories in the shaded/fat jar * and define a match to be when all class file names match the class files in the original jar. */ FileSystem fileSystemOriginalJar = getOrCreateFileSystem(jarPath); Set originalClassFiles = new HashSet<>(); FileWalkerUtility.walkFileTreeWithExtensions(fileSystemOriginalJar.getPath("/"), Set.of(".class", ".java"), file -> { Path fileName = file.getFileName(); if (fileName != null) { originalClassFiles.add(fileName.toString()); } }); Optional> optionalPaths = resolveDirectoriesFromClassNameMatching(artifact, originalClassFiles); if (optionalPaths.isPresent()) { Set paths = optionalPaths.get(); visitedPathToClassFileDirectories.addAll(paths); return Optional.of(paths); } return Optional.empty(); } /** * Resolves the top directory containing class files by traversing backwards from the main class location. * * @param qualifiedName the qualified name of the class to start the search from. * @param rootPath the root of the file system. * @return a path of the top directory containing class files. */ private Optional findTopClassDirectory(Path rootPath, String qualifiedName) throws IOException { String mainClassPath = qualifiedName.replace('.', File.separatorChar) + ".class"; Path classFilePath = rootPath.resolve(mainClassPath); Path currentPath = classFilePath.getParent(); while (currentPath != null && !Files.isSameFile(currentPath, rootPath)) { if (FileWalkerUtility.containsClassFiles(currentPath)) { return Optional.of(currentPath); } currentPath = currentPath.getParent(); } return Optional.empty(); } /** * Helper method to resolve GAV coordinates and check if they exist in the class files directory. * Tries both with and without using the artifactId. */ private Optional tryResolveUsingGAVCoordinates(Path rootPath, ArtifactAdapter artifact) throws IOException { Optional resolvedPath = resolveGAVCoordinates(rootPath, artifact, true); if (resolvedPath.isPresent()) return resolvedPath; return resolveGAVCoordinates(rootPath, artifact, false); } /** * Helper method to resolve GAV coordinates for a specific configuration (with or without artifactId). */ private Optional resolveGAVCoordinates(Path rootPath, ArtifactAdapter artifact, boolean useArtifactId) throws IOException { Path gavPath = pathFromGAVCoordinates(rootPath, artifact, useArtifactId); var pathsAsStrings = pathToClassFilesDirectories.stream() .map(Path::toString) .collect(Collectors.toSet()); if (pathsAsStrings.contains(gavPath.toString())) { visitedPathToClassFileDirectories.add(gavPath); return Optional.of(gavPath); } return Optional.empty(); } private Set notVisitedPaths() { Set difference = new HashSet<>(pathToClassFilesDirectories); difference.removeAll(visitedPathToClassFileDirectories); return difference; } private Optional> resolveDirectoriesFromClassNameMatching(ArtifactAdapter artifact, Set originalClassFiles) throws IOException { Set matchingDirectories = new HashSet<>(); for (Path potentialDirectory : pathToClassFilesDirectories) { AtomicBoolean successfulMatching = new AtomicBoolean(true); try (DirectoryStream stream = Files.newDirectoryStream(potentialDirectory)) { for (Path file : stream) { if (Files.isRegularFile(file) && file.toString().endsWith(".class")) { String fileName = file.getFileName().toString(); if (!originalClassFiles.contains(fileName)) { successfulMatching.set(false); break; } } } } if (successfulMatching.get()) { matchingDirectories.add(potentialDirectory); } } if (matchingDirectories.isEmpty()) { return Optional.empty(); } return Optional.of(matchingDirectories); } private Set resolveDirectoriesContainingClassFiles(Path rootPath) throws IOException { Set directories = new HashSet<>(); Files.walkFileTree(rootPath, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { if (file.toString().endsWith(".class")) { Path classDirectory = file.getParent(); directories.add(classDirectory); return FileVisitResult.CONTINUE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { Path fileName = dir.getFileName(); if (fileName != null && fileName.toString().equals("META-INF")) { return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } }); return directories; } private Path pathFromGAVCoordinates(Path basePath, ArtifactAdapter artifact, boolean useArtifactId) throws IOException { FileSystem fileSystem = basePath.getFileSystem(); Path expectedPath = basePath.resolve(fileSystem.getPath( artifact.groupId.replace('.', '/') )); if (useArtifactId) { expectedPath = expectedPath.resolve(artifact.artifactId.replace('.', '/')); } /* Handle case where there are multiple versions. */ if (Files.isDirectory(expectedPath)) { try (DirectoryStream stream = Files.newDirectoryStream(expectedPath)) { boolean hasMultipleDirectories = StreamSupport.stream(stream.spliterator(), false) .filter(Files::isDirectory) .count() > 1; /* If multiple directories exist, append the version information. */ if (hasMultipleDirectories) { expectedPath = expectedPath.resolve(artifact.version); } } } return expectedPath; } private static Optional getParameterFromPlugin(Plugin plugin, String parameter) { Xpp3Dom configuration = (Xpp3Dom) plugin.getConfiguration(); if (configuration != null && parameter != null && !parameter.isEmpty()) { Xpp3Dom parameterNode = configuration.getChild(parameter); if (parameterNode != null) { return Optional.of(parameterNode.getValue()); } } return Optional.empty(); } private static Plugin getShadePluginIfUsed(MavenProject mavenProject) { return mavenProject.getBuildPlugins().stream() .filter(v -> mavenShadePluginName.equals(v.getArtifactId())) .findFirst() .orElse(null); } private static FileSystem getOrCreateFileSystem(Path jarPath) throws IOException { try { return FileSystems.newFileSystem(jarPath, null); } catch (FileSystemAlreadyExistsException e) { /* If the file system already exists, return the existing file system. */ return FileSystems.getFileSystem(jarPath.toUri()); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy