desruisseaux commented on code in PR #508:
URL: https://github.com/apache/maven-jar-plugin/pull/508#discussion_r3923642621


##########
src/main/java/org/apache/maven/plugins/jar/PomDerivation.java:
##########
@@ -0,0 +1,529 @@
+/*
+ * 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.maven.plugins.jar;
+
+import javax.xml.stream.XMLStreamException;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReference;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import org.apache.maven.api.DependencyCoordinates;
+import org.apache.maven.api.JavaPathType;
+import org.apache.maven.api.Session;
+import org.apache.maven.api.Type;
+import org.apache.maven.api.model.Dependency;
+import org.apache.maven.api.model.Model;
+import org.apache.maven.api.model.Parent;
+import org.apache.maven.api.plugin.MojoException;
+import org.apache.maven.api.services.DependencyCoordinatesFactory;
+import org.apache.maven.api.services.DependencyCoordinatesFactoryRequest;
+import org.apache.maven.api.services.DependencyResolver;
+import org.apache.maven.api.services.DependencyResolverRequest;
+import org.apache.maven.api.services.DependencyResolverResult;
+import org.apache.maven.api.services.ModelBuilderException;
+import org.apache.maven.model.v4.MavenStaxWriter;
+
+/**
+ * A mapper from Maven model dependencies to Java module names.
+ * A single instance of this class is created for a Maven project,
+ * then shared by all {@link ForModule} instances (one per module to archive).
+ */
+final class PomDerivation {
+    /**
+     * Whether to expand the list of transitive dependencies in the generated 
<abbr>POM</abbr>.
+     */
+    private static final boolean EXPAND_TRANSITIVE = false;
+
+    /**
+     * Copy of {@link AbstractJarMojo#session}.
+     */
+    private final Session session;
+
+    /**
+     * The project model, which includes dependencies of all modules.
+     */
+    private final Model projectModel;
+
+    /**
+     * The factory to use for creating temporary {@link DependencyCoordinates} 
instances.
+     */
+    private final DependencyCoordinatesFactory coordinateFactory;
+
+    /**
+     * Provide module descriptors from module names.
+     * May be {@code null} if no {@code module-info} was found.
+     */
+    private final ModuleFinder moduleFinder;
+
+    /**
+     * Module references from paths to the <abbr>JAR</abbr> file or root 
directory.
+     */
+    private final Map<URI, ModuleReference> fromURI;
+
+    /**
+     * Module names associated with Maven dependencies.
+     * This map contains {@link DependencyCoordinates#getId()} as keys and 
module references as values.
+     * This is used to detect which dependencies are really used according to 
{@code module-info.class}.
+     *
+     * @todo The keys should be instances of {@link DependencyCoordinates}. 
Unfortunately, as of Maven 4.0.0-rc-5
+     *       that interface does not define the {@code equals} and {@code 
hashCode} contracts.
+     */
+    private final Map<String, ModuleReference> fromDependency;
+
+    /**
+     * Modules that are built by the project. Keys are module names.
+     */
+    private final Map<String, Dependency> builtModules;
+
+    /**
+     * Creates a new mapper from Maven dependency to module name.
+     *
+     * @param mojo the enclosing <abbr>MOJO</abbr>
+     * @param moduleRoots paths to root directories of each module to archive 
in a module hierarchy
+     * @throws IOException if an I/O error occurred while fetching dependencies
+     * @throws MavenException if an error occurred while fetching dependencies 
for a reason other than I/O.
+     */
+    PomDerivation(final AbstractJarMojo mojo, final List<Path> moduleRoots) 
throws IOException {
+        this.session = mojo.session;
+        projectModel = mojo.project.getModel();
+        coordinateFactory = 
session.getService(DependencyCoordinatesFactory.class);
+        DependencyResolver resolver = 
session.getService(DependencyResolver.class);
+        DependencyResolverResult result = 
resolver.resolve(DependencyResolverRequest.builder()
+                .session(session)
+                .project(mojo.project)
+                .requestType(DependencyResolverRequest.RequestType.RESOLVE)
+                .pathScope(mojo.getDependencyScope())
+                .pathTypeFilter(Set.of(JavaPathType.MODULES, 
JavaPathType.CLASSES))
+                .build());
+
+        rethrow(result);
+        final Map<org.apache.maven.api.Dependency, Path> dependencies = 
result.getDependencies();
+        final Path[] allModulePaths = toRealPaths(moduleRoots, 
dependencies.values());
+        fromURI = new HashMap<>(allModulePaths.length); // TODO: use 
newHashMap with JDK19.
+        moduleFinder = ModuleFinder.of(allModulePaths);
+        if (moduleFinder != null) {
+            for (ModuleReference reference : moduleFinder.findAll()) {
+                reference.location().ifPresent((location) -> 
fromURI.put(location, reference));
+            }
+        }
+        fromDependency = new HashMap<>(dependencies.size()); // TODO: use 
newHashMap with JDK19.
+        for (Map.Entry<org.apache.maven.api.Dependency, Path> entry : 
dependencies.entrySet()) {
+            Path modulePath = entry.getValue().toRealPath();
+            ModuleReference reference = fromURI.get(modulePath.toUri());
+            if (reference != null) {
+                DependencyCoordinates coordinates = 
entry.getKey().toCoordinates();
+                String id = coordinates.getId();
+                ModuleReference old = fromDependency.putIfAbsent(id, 
reference);
+                if (old == null) {
+                    coordinates = withoutVersion(coordinates);
+                    id = coordinates.getId();
+                    old = fromDependency.putIfAbsent(id, reference);
+                }
+                if (old != null && !old.equals(reference)) {
+                    mojo.log.warn("The \"" + id + "\" dependency is declared 
twice with different module names: \""
+                            + old.descriptor().name() + "\" and \""
+                            + reference.descriptor().name() + "\".");
+                }
+            }
+        }
+        builtModules = new HashMap<>(moduleRoots.size()); // TODO: use 
newHashMap with JDK19.
+        for (Path root : moduleRoots) {
+            root = root.toRealPath();
+            ModuleReference reference = fromURI.get(root.toUri());
+            if (reference != null) { // Actually a null value would be a bug, 
but be tolerant.
+                String moduleName = reference.descriptor().name();
+                builtModules.put(
+                        moduleName,
+                        Dependency.newBuilder()
+                                .groupId(projectModel.getGroupId())
+                                .artifactId(moduleName)
+                                .version(projectModel.getVersion())
+                                .type(Type.MODULAR_JAR)
+                                .build());
+            }
+        }
+    }
+
+    /**
+     * Rebuilds the given dependency coordinates without version.
+     *
+     * Note: I'm not sure if it is necessary. This is done in case version 
numbers are not resolved
+     * in the dependencies of the model returned by {@code 
Project.getModel()}, or are not resolved
+     * in the same way as what we get from {@link DependencyResolver}.
+     */
+    private DependencyCoordinates withoutVersion(DependencyCoordinates 
coordinates) {
+        return 
coordinateFactory.create(DependencyCoordinatesFactoryRequest.builder()
+                .session(session)
+                .groupId(coordinates.getGroupId())
+                .artifactId(coordinates.getArtifactId())
+                .extension(coordinates.getExtension())
+                .classifier(coordinates.getClassifier())
+                .build());
+    }
+
+    /**
+     * If the resolver failed, propagates its exception.
+     *
+     * @param result the resolver result
+     * @throws IOException if the result contains an I/O error
+     */
+    private static void rethrow(DependencyResolverResult result) throws 
IOException {
+        Exception exception = null;
+        for (Exception cause : result.getExceptions()) {
+            if (cause instanceof UncheckedIOException e) {
+                cause = e.getCause();
+            }
+            if (exception != null) {
+                exception.addSuppressed(cause);
+            } else if (cause instanceof RuntimeException || cause instanceof 
IOException) {
+                exception = cause;
+            } else {
+                exception = new MojoException("Cannot collect the runtime 
dependencies.", cause);
+            }
+        }
+        if (exception != null) {
+            if (exception instanceof IOException e) {
+                throw e;
+            } else {
+                throw (RuntimeException) exception; // A ClassCastException 
here would be a bug in above loop.
+            }
+        }
+    }
+
+    /**
+     * Returns the real paths of the given collections, in iteration order and 
without duplicated values.
+     */
+    private static Path[] toRealPaths(Collection<Path> moduleRoots, 
Collection<Path> dependencies) throws IOException {
+        // TODO: use newLinkedHashSet(int) after we are allowed to compile for 
JDK19.
+        final var paths = new LinkedHashSet<Path>(moduleRoots.size() + 
dependencies.size());
+        for (Path path : moduleRoots) {
+            paths.add(path.toRealPath());
+        }
+        for (Path path : dependencies) {
+            paths.add(path.toRealPath());
+        }
+        return paths.toArray(Path[]::new);
+    }
+
+    /**
+     * Returns the module descriptor for the {@code module-info.class} at the 
given path.
+     *
+     * @param moduleInfo path to a {@code module-info.class} file
+     * @return module descriptor for the specified file
+     * @throws IOException if an error occurred while reading the file
+     */
+    private ModuleDescriptor findModuleDescriptor(Path moduleInfo) throws 
IOException {
+        Path directory = moduleInfo.toRealPath().getParent();
+        ModuleReference reference = fromURI.get(directory.toUri());
+        if (reference != null) {
+            return reference.descriptor();
+        }
+        try (InputStream in = new 
BufferedInputStream(Files.newInputStream(moduleInfo))) {
+            return ModuleDescriptor.read(in);
+        }
+    }
+
+    /**
+     * Returns the module descriptor for the specified Maven dependency.
+     *
+     * @param dependency dependency for which to get the module descriptor
+     * @return Java module descriptor for the given Maven dependency
+     */
+    private Optional<ModuleDescriptor> findModuleDescriptor(Dependency 
dependency) {
+        DependencyCoordinates coordinates = coordinateFactory.create(session, 
dependency);
+        ModuleReference reference = fromDependency.get(coordinates.getId());
+        if (reference == null) {
+            coordinates = withoutVersion(coordinates);
+            reference = fromDependency.get(coordinates.getId());
+            if (reference == null) {
+                return Optional.empty();
+            }
+        }
+        return Optional.of(reference.descriptor());
+    }
+
+    /**
+     * Returns the module descriptor for the specified module name.
+     *
+     * @param moduleName name of the module for which to get the descriptor
+     * @return module descriptor for the specified module name
+     */
+    private Optional<ModuleDescriptor> findModuleDescriptor(String moduleName) 
{
+        if (moduleFinder == null) {
+            return Optional.empty();
+        }
+        return moduleFinder.find(moduleName).map(ModuleReference::descriptor);
+    }
+
+    /**
+     * Derives a <abbr>POM</abbr> as the intersection of the consumer 
<abbr>POM</abbr>
+     * and the dependencies required by {@code module-info}.
+     */
+    final class ForModule {
+        /**
+         * Value of the {@code <name>} element in the derived 
<abbr>POM</abbr>, or {@code null} if none.
+         */
+        private String name;
+
+        /**
+         * Name of the module for which to derive a <abbr>POM</abbr> file.
+         */
+        private final String moduleName;
+
+        /**
+         * Whether a dependency is optional or required only at runtime.
+         */
+        private enum Modifier {
+            OPTIONAL,
+            RUNTIME
+        }
+
+        /**
+         * The required dependencies as Java module names, including 
transitive dependencies.
+         * Values tell whether the dependency is optional or should have 
runtime scope.
+         */
+        private final Map<String, EnumSet<Modifier>> requires;
+
+        /**
+         * Path to the <abbr>POM</abbr> file written by this class.
+         */
+        final Path pomFile;
+
+        /**
+         * Creates a new <abbr>POM</abbr> generator for the given archive.
+         *
+         * @param archive the archive for which to generate a <abbr>POM</abbr>
+         * @param manifest manifest to use for deriving project name, or 
{@code null} if none
+         * @throws IOException if an error occurred while reading the {@code 
module-info.class} file
+         */
+        ForModule(final Archive archive, final Manifest manifest) throws 
IOException {
+            moduleName = archive.moduleName;
+            pomFile = derivePathToPOM(archive.jarFile);
+            requires = new LinkedHashMap<>();
+            for (Path file : archive.moduleInfoFiles()) {
+                addDependencies(findModuleDescriptor(file), 
EnumSet.noneOf(Modifier.class));
+            }
+            if (manifest != null) {
+                name = (String) 
manifest.getMainAttributes().get(Attributes.Name.IMPLEMENTATION_TITLE);
+                if (name == null) {
+                    name = (String) 
manifest.getMainAttributes().get(Attributes.Name.SPECIFICATION_TITLE);
+                }
+            }
+        }
+
+        /**
+         * Add the dependencies of the given module, including transitive 
dependencies.
+         * If the same dependency is added twice with different optional flags,
+         * the {@code false} value (i.e., mandatory dependency) has precedence.
+         *
+         * @param descriptor description of the module for which to add 
dependencies
+         * @param parentModifiers modifiers of the parent module for which 
this method adds dependencies
+         */
+        private void addDependencies(final ModuleDescriptor descriptor, final 
EnumSet<Modifier> parentModifiers) {
+            for (ModuleDescriptor.Requires r : descriptor.requires()) {
+                final EnumSet<Modifier> modifiers = parentModifiers.clone();
+                if 
(r.modifiers().contains(ModuleDescriptor.Requires.Modifier.STATIC)) {
+                    modifiers.add(Modifier.OPTIONAL);
+                }
+                if 
(!r.modifiers().contains(ModuleDescriptor.Requires.Modifier.TRANSITIVE)) {
+                    modifiers.add(Modifier.RUNTIME);
+                }
+                EnumSet<Modifier> current = requires.computeIfAbsent(r.name(), 
(key) -> modifiers);
+                if (EXPAND_TRANSITIVE && (current == modifiers || 
current.retainAll(modifiers))) {
+                    // Transitive dependencies if not already added or if it 
needs to update modifiers.
+                    findModuleDescriptor(r.name()).ifPresent((td) -> 
addDependencies(td, modifiers));
+                }
+            }
+        }
+
+        /**
+         * Derives a <abbr>POM</abbr> file for the archive specified ad 
construction time.
+         *
+         * @throws IOException if an error occurred while writing the 
<abbr>POM</abbr> file
+         *
+         * @see #writeParentPOM(Archive)
+         */
+        void writeModulePOM() throws IOException {
+            try {
+                writePOM(deriveModulePOM(), pomFile);
+            } catch (ModelBuilderException | XMLStreamException e) {
+                throw new MojoException("Cannot derive a POM file for the \"" 
+ moduleName + "\" module.", e);
+            }
+        }
+
+        /**
+         * Derives the module <abbr>POM</abbr> file as the intersection of the 
project <abbr>POM</abbr> and the archive.
+         *
+         * @return intersection of {@link #projectModel} and {@code 
module-info.class}
+         * @throws ModelBuilderException if an error occurred while building 
the model
+         */
+        private Model deriveModulePOM() throws ModelBuilderException {
+            Model.Builder builder = Model.newBuilder()
+                    .modelVersion("4.0.0")
+                    .groupId(projectModel.getGroupId())
+                    .artifactId(moduleName)
+                    .version(projectModel.getVersion())
+                    .parent(Parent.newBuilder()
+                            .groupId(projectModel.getGroupId())
+                            .artifactId(projectModel.getArtifactId())
+                            .version(projectModel.getVersion())
+                            .build());
+            if (name != null) {
+                builder = builder.name(name);
+            }
+            /*
+             * Filter the dependencies by keeping only the one declared in a 
`requires` statement of the
+             * `module-info.class` of the module that we are archiving. Also 
adjust the `<optional>` and
+             * `<scope>` values. The dependencies that we found are removed 
from the `requires` map as a
+             * way to make sure that we do not add them twice. In principle, 
the map should become empty
+             * at the end of this loop.
+             */
+            final List<Dependency> dependencies = 
projectModel.getDependencies();
+            if (dependencies != null) {
+                final var filteredDependencies = new 
ArrayList<Dependency>(dependencies.size());
+                for (var iterator = requires.entrySet().iterator(); 
iterator.hasNext(); ) {
+                    Map.Entry<String, EnumSet<Modifier>> entry = 
iterator.next();
+                    Dependency dependency = builtModules.get(entry.getKey());
+                    if (dependency != null) {
+                        filteredDependencies.add(amend(dependency, 
entry.getValue()));
+                        iterator.remove();
+                    }
+                }
+                for (Dependency dependency : dependencies) {
+                    String dependencyModuleName = 
findModuleDescriptor(dependency)
+                            .map(ModuleDescriptor::name)
+                            .orElse(null);
+                    /*
+                     * If `dependencyModuleName` is null, then the dependency 
scope is "test" or some other scope
+                     * that resolver has chosen to exclude. Note that this is 
true even for JAR on the classpath,
+                     * because we stored the automatic module name in 
`PomDerivation`. Next, if `modifiers` is null,
+                     * then the dependency has compile or runtime scope but is 
not used by the module to archive.
+                     */
+                    if (dependencyModuleName != null) {
+                        EnumSet<Modifier> modifiers = 
requires.remove(dependencyModuleName);
+                        if (modifiers != null) {
+                            filteredDependencies.add(amend(dependency, 
modifiers));
+                        }
+                    }
+                }
+                builder.dependencies(filteredDependencies);
+            }
+            /*
+             * Replace the `<name>` element by the equivalent value defined in 
MANIFEST.MF.
+             * We do this replacement because the `<name>` of the project 
model applies to all modules,
+             * while the MANIFEST.MF has more chances to be specific to the 
module that we are archiving.
+             */
+            return builder.build();
+        }
+
+        /**
+         * Modifies the optional and scope elements of the given dependency 
according to the given modifiers.
+         *
+         * @param dependency the dependency to amend
+         * @param modifiers the modifiers to apply
+         * @return the amended dependency
+         */
+        private static Dependency amend(Dependency dependency, 
EnumSet<Modifier> modifiers) {
+            String scope = modifiers.contains(Modifier.RUNTIME) ? "runtime" : 
null;
+            if (!Objects.equals(scope, dependency.getScope())) {
+                dependency = dependency.withScope(scope);
+            }
+            boolean optional = modifiers.contains(Modifier.OPTIONAL);
+            if (Boolean.parseBoolean(dependency.getOptional()) != optional) {
+                dependency = 
dependency.withOptional(Boolean.toString(optional));
+            }
+            return dependency;
+        }
+    }
+
+    /**
+     * Writes the parent <abbr>POM</abbr> file as the project file but without 
the dependencies.
+     * The dependencies are removed because they will declared on a 
module-by-module basis.
+     *
+     * @param packageHierarchy value of {@link FileCollector#packageHierarchy}
+     * @return path to the file that has been written
+     * @throws IOException if an I/O error occurred while writing the model
+     *
+     * @see ForModule#writeModulePOM()
+     */
+    Path writeParentPOM(final Archive packageHierarchy) throws IOException {
+        Path pomFile = derivePathToPOM(packageHierarchy.jarFile);
+        Model.Builder builder = Model.newBuilder(projectModel, true);
+        builder = builder.root(false).modules(null).subprojects(null);
+        builder = 
builder.dependencies(null).build(null).reporting(null).distributionManagement(null);
+        builder = builder.preserveModelVersion(false).modelVersion("4.0.0");
+        try {
+            writePOM(builder.build(), pomFile);
+        } catch (ModelBuilderException | XMLStreamException e) {
+            throw new MojoException("Cannot write the parent POM.", e);
+        }
+        return pomFile;
+    }
+
+    /**
+     * Derives the path to the <abbr>POM</abbr> file to generate.
+     *
+     * @param jarFile path to the <abbr>JAR</abbr> file (the file does not 
need to exist)
+     * @return path to the <abbr>POM</abbr> file to generate
+     */
+    private static Path derivePathToPOM(final Path jarFile) {
+        String filename = jarFile.getFileName().toString();
+        filename = filename.substring(0, filename.lastIndexOf('.') + 1) + 
"pom";
+        return jarFile.resolveSibling(filename);
+    }
+
+    /**
+     * Writes the given model in the given file.
+     *
+     * @param model the model to write
+     * @param file the destination file
+     * @throws IOException if an I/O error occurred while writing the model
+     * @throws XMLStreamException if a XML error occurred while writing the 
model
+     */
+    private static void writePOM(Model model, Path file) throws IOException, 
XMLStreamException {
+        try (BufferedWriter out = Files.newBufferedWriter(file)) {
+            var sw = new MavenStaxWriter();

Review Comment:
   Done. I used `xmlWriter` in case some peoples are not familiar about STAX. 
They get that this is some kind of XML writer, and the constructor tells us 
which one exactly.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to