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}