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 com.google.common.flogger.FluentLogger; 022import java.nio.file.Path; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.List; 026import java.util.Map; 027import java.util.Objects; 028import java.util.jar.Attributes; 029import java.util.stream.Stream; 030import org.jdrupes.builder.api.BuildException; 031import org.jdrupes.builder.api.ConfigurationException; 032import org.jdrupes.builder.api.Generator; 033import org.jdrupes.builder.api.InputResource; 034import org.jdrupes.builder.api.Intent; 035import static org.jdrupes.builder.api.Intent.*; 036import org.jdrupes.builder.api.Project; 037import org.jdrupes.builder.api.Resource; 038import org.jdrupes.builder.api.ResourceProvider; 039import org.jdrupes.builder.api.ResourceRequest; 040import org.jdrupes.builder.api.ResourceRetriever; 041import org.jdrupes.builder.api.ResourceType; 042import static org.jdrupes.builder.api.ResourceType.*; 043import org.jdrupes.builder.api.Resources; 044import org.jdrupes.builder.core.StreamCollector; 045import static org.jdrupes.builder.java.JavaTypes.*; 046 047/// A [Generator] for Java libraries packaged as jars. A library jar 048/// is expected to contain class files and supporting resources together 049/// with additional information in `META-INF/`. 050/// 051/// The generator provides two types of resources. 052/// 053/// 1. A [LibraryJarFile]. This type of resource is also returned if a more 054/// general [ResourceType] such as [CodeContribution] is requested. 055/// 056/// 2. An [AppJarFile]. When requesting this special jar type, the 057/// generator checks if a main class is specified. 058/// 059/// In addition to explicitly adding resources, this generator supports 060/// resource retrieval from added providers. The resources of type [ClassTree] 061/// and [JavaResourceTree] that the providers added with 062/// [ResourceRetriever#addFrom(ResourceProvider...)] [supply][Intent#Supply] 063/// are included in the library in addition to the explicitly added resources. 064/// 065/// The enables the simple standard pattern for creating a library: 066/// ```java 067/// generator(LibraryBuilder::new).addFrom(this); 068/// ``` 069/// 070/// ## JPMS support 071/// 072/// When the compiled classes contain a `module-info.class`, the JAR is 073/// a named module. If no `module-info.class` is present but [moduleName] 074/// is set (or auto-detected from the project name), an `Automatic-Module-Name` 075/// manifest attribute is added so the JAR is treated as an automatic module 076/// on the module-path. 077/// 078public class LibraryBuilder extends JarBuilder implements ResourceRetriever { 079 080 @SuppressWarnings({ "unused" }) 081 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 082 private final StreamCollector<ResourceProvider> providers 083 = StreamCollector.cached(); 084 private String mainClass; 085 private String moduleName; 086 087 /// Instantiates a new library generator. 088 /// 089 /// @param project the project 090 /// 091 public LibraryBuilder(Project project) { 092 super(project, LibraryJarFileType); 093 } 094 095 @Override 096 public LibraryBuilder name(String name) { 097 rename(name); 098 return this; 099 } 100 101 /// Returns the main class. 102 /// 103 /// @return the main class 104 /// 105 public String mainClass() { 106 return mainClass; 107 } 108 109 /// Sets the main class. 110 /// 111 /// @param mainClass the new main class 112 /// @return the library builder for method chaining 113 /// 114 public LibraryBuilder mainClass(String mainClass) { 115 this.mainClass = Objects.requireNonNull(mainClass); 116 return this; 117 } 118 119 /// Returns the module name for the `Automatic-Module-Name` manifest 120 /// attribute. If not set, the project name is sanitized for JPMS 121 /// requirements. 122 /// 123 /// @return the module name 124 /// 125 public String moduleName() { 126 return moduleName; 127 } 128 129 /// Sets the module name used for the `Automatic-Module-Name` 130 /// manifest attribute. This allows the JAR to be used as an 131 /// automatic module on the module-path. 132 /// 133 /// @param moduleName the module name 134 /// @return this builder 135 /// 136 public LibraryBuilder moduleName(String moduleName) { 137 this.moduleName = moduleName; 138 return this; 139 } 140 141 @Override 142 public LibraryBuilder addFrom(ResourceProvider... providers) { 143 addFrom(Stream.of(providers)); 144 return this; 145 } 146 147 @Override 148 public LibraryBuilder addFrom(Stream<ResourceProvider> providers) { 149 this.providers.add(providers.filter(p -> !p.equals(this))); 150 return this; 151 } 152 153 /// return the cached providers. 154 /// 155 /// @return the content providers 156 /// 157 protected StreamCollector<ResourceProvider> contentProviders() { 158 return providers; 159 } 160 161 @Override 162 protected void 163 collectContents(Map<Path, Resources<InputResource>> contents) { 164 super.collectContents(contents); 165 // Add main class if defined 166 if (mainClass() != null) { 167 attributes(Map.entry(Attributes.Name.MAIN_CLASS, mainClass())); 168 } 169 170 // Add Automatic-Module-Name if specified and not a named module 171 if (!contents.containsKey(Path.of("module-info.class")) 172 && moduleName() != null) { 173 attributes(Map.entry( 174 new Attributes.Name("Automatic-Module-Name"), moduleName())); 175 } 176 collectFromProviders(contents); 177 } 178 179 /// Collects the contents from the providers. This implementation 180 /// requests [ClassTree]s and [JavaResourceTree]s. 181 /// 182 /// @param contents the contents 183 /// 184 protected void collectFromProviders( 185 Map<Path, Resources<InputResource>> contents) { 186 contentProviders().stream() 187 .map(p -> p.resources(of(ClassTreeType).using(Supply))) 188 // Terminate to trigger all future stream evaluations before 189 // starting to process the results. Then collect in parallel. 190 .toList().stream().flatMap(s -> s).toList().parallelStream() 191 .forEach(t -> collect(contents, t)); 192 contentProviders().stream() 193 .map(p -> p.resources(of(JavaResourceTreeType).using(Supply))) 194 // Terminate to trigger all future stream evaluations before 195 // starting to process the results. Then collect in parallel. 196 .toList().stream().flatMap(s -> s).toList().parallelStream() 197 .forEach(t -> collect(contents, t)); 198 } 199 200 @Override 201 @SuppressWarnings({ "PMD.CollapsibleIfStatements", "unchecked", 202 "PMD.CyclomaticComplexity" }) 203 protected <T extends Resource> Collection<T> 204 doProvide(ResourceRequest<T> request) { 205 if (!request.accepts(LibraryJarFileType) 206 && !request.accepts(CleanlinessType)) { 207 return Collections.emptyList(); 208 } 209 210 // Maybe only delete 211 if (request.accepts(CleanlinessType)) { 212 destination().resolve(jarName()).toFile().delete(); 213 return Collections.emptyList(); 214 } 215 216 // Upgrade to most specific type to avoid duplicate generation 217 if (mainClass() != null && !request.type().equals(AppJarFileType)) { 218 return (Collection<T>) context() 219 .resources(this, project().of(AppJarFileType)).toList(); 220 } 221 if (mainClass() == null && !request.type().equals(LibraryJarFileType)) { 222 return (Collection<T>) context() 223 .resources(this, project().of(LibraryJarFileType)).toList(); 224 } 225 226 // Make sure mainClass is set for app jar 227 if (request.isFor(AppJarFileType) && mainClass() == null) { 228 throw new ConfigurationException().from(this).message( 229 "Main class must be set for %s", name()); 230 } 231 232 // Prepare jar file 233 var destDir = destination(); 234 if (!destDir.toFile().exists()) { 235 if (!destDir.toFile().mkdirs()) { 236 throw new BuildException().from(this) 237 .message("Cannot create directory " + destDir); 238 } 239 } 240 var jarResource = request.isFor(AppJarFileType) 241 ? AppJarFile.of(destDir.resolve(jarName())) 242 : LibraryJarFile.of(destDir.resolve(jarName())); 243 244 buildJar(jarResource); 245 return List.of((T) jarResource); 246 } 247}