001/*
002 * JDrupes Builder
003 * Copyright (C) 2025, 2026 Michael N. Lipp
004 * 
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.builder.java;
020
021import java.io.IOException;
022import java.lang.module.ModuleFinder;
023import java.lang.module.ModuleReference;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.util.Optional;
027import org.jdrupes.builder.api.Resource;
028
029/// Common base interface for resources that can be used in either a
030/// classpath or a module-path.
031///
032public interface CodeContribution extends Resource {
033
034    /// Return the representation of the element as it must appear
035    /// on a classpath or module-path.
036    ///
037    /// @return the path
038    ///
039    Path toPath();
040
041    /// Checks whether the code contribution is modular. This works for
042    /// both JAR files and directories. Both are considered as modular
043    /// if they contain a `module-info.class`. JARs are also considered
044    /// as modular if they have an `Automatic-Module-Name` set in their
045    /// `MANIFEST.MF`.
046    /// 
047    /// This is provided as static method in order to simplify reuse
048    /// in classes implementing this interface.
049    ///
050    /// @param contribution the contribution
051    /// @return true, if the contribution is modular
052    ///
053    static boolean isModular(CodeContribution contribution) {
054        var path = contribution.toPath();
055        if (Files.isDirectory(path)) {
056            return Files.exists(path.resolve("module-info.class"));
057        }
058        if (!path.toString().endsWith(".jar")) {
059            return false;
060        }
061        try (var jar = new java.util.jar.JarFile(path.toFile())) {
062            var manifest = jar.getManifest();
063            if (manifest != null) {
064                var name = manifest.getMainAttributes()
065                    .getValue("Automatic-Module-Name");
066                if (name != null && !name.isBlank()) {
067                    return true;
068                }
069            }
070            // This checks for the existence of module-info.class
071            // in root and in META-INF/versions
072            return ModuleFinder.of(path).findAll().stream()
073                .map(ModuleReference::descriptor)
074                .anyMatch(d -> !d.isAutomatic());
075        } catch (IOException e) { // NOPMD
076            // fall through
077        }
078        return false;
079    }
080
081    /// Checks whether this code contribution is modular. This works for
082    /// both JAR files and directories. Both are considered as modular
083    /// if they contain a `module-info.class`. JARs are also considered
084    /// as modular if they have an `Automatic-Module-Name` set in their
085    /// `MANIFEST.MF`.
086    ///
087    /// @return true, if the contribution is modular
088    ///
089    default boolean isModular() {
090        return isModular(this);
091    }
092
093    /// Returns the module name if this element is a named module.
094    ///
095    /// @return the optional module name
096    ///
097    @SuppressWarnings("PMD.AvoidCatchingGenericException")
098    default Optional<String> moduleName() {
099        // Check first, because ModuleFinder returns a name even for
100        // JARs without Automatic-Module-Name
101        if (!isModular()) {
102            return Optional.empty();
103        }
104        try {
105            var finder = ModuleFinder.of(toPath());
106            return finder.findAll().stream()
107                .map(m -> m.descriptor().name()).findFirst();
108        } catch (Exception e) {
109            return Optional.empty();
110        }
111    }
112
113}