001/* 002 * JDrupes Builder 003 * Copyright (C) 2025 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.uberjar; 020 021import java.io.IOException; 022import java.nio.file.Path; 023import java.util.Map; 024import java.util.concurrent.ConcurrentHashMap; 025import java.util.function.Predicate; 026import java.util.jar.JarEntry; 027import java.util.stream.Stream; 028import org.jdrupes.builder.api.BuildException; 029import org.jdrupes.builder.api.FileTree; 030import org.jdrupes.builder.api.Generator; 031import org.jdrupes.builder.api.IOResource; 032import org.jdrupes.builder.api.Project; 033import org.jdrupes.builder.api.Resource; 034import org.jdrupes.builder.api.ResourceRequest; 035import org.jdrupes.builder.api.ResourceType; 036import static org.jdrupes.builder.api.ResourceType.*; 037import org.jdrupes.builder.api.Resources; 038import org.jdrupes.builder.java.AppJarFile; 039import org.jdrupes.builder.java.ClasspathElement; 040import org.jdrupes.builder.java.JarFile; 041import org.jdrupes.builder.java.JarFileEntry; 042import static org.jdrupes.builder.java.JavaTypes.*; 043import org.jdrupes.builder.java.LibraryGenerator; 044import org.jdrupes.builder.java.RuntimeResources; 045import org.jdrupes.builder.java.ServicesEntryResource; 046import org.jdrupes.builder.mvnrepo.MvnRepoJarFile; 047import org.jdrupes.builder.mvnrepo.MvnRepoLookup; 048import org.jdrupes.builder.mvnrepo.MvnRepoResource; 049import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*; 050 051/// A [Generator] for uber jars. 052/// 053/// Depending on the request, the generator provides two types of resources. 054/// 055/// 1. A [JarFile]. This type of resource is also returned if a more 056/// general [ResourceType] such as [ClasspathElement] is requested. 057/// 058/// 2. An [AppJarFile]. When requesting this special jar type, the 059/// generator checks if a main class is specified. 060/// 061/// The generator takes the following approach: 062/// 063/// * Request all [ClasspathElement]s from the providers. Add the 064/// resource trees and the jar files to the sources to be processed. 065/// Ignore jar files from maven repositories (instances of 066/// [MvnRepoJarFile]). 067/// * Request all [MvnRepoResource]s from the providers and use them for 068/// a dependency resolution. Add the jar files from the dependency 069/// resolution to the resources to be processed. 070/// * Add resources from the sources to the uber jar. Merge the files in 071/// `META-INF/services/` that have the same name by concatenating them. 072/// * Filter out any other duplicate direct child files of `META-INF`. 073/// These files often contain information related to the origin jar 074/// that is not applicable to the uber jar. 075/// * Filter out any module-info.class entries. 076/// 077/// Note that the resource type of the uber jar generator's output is one 078/// of the resource types of its inputs, because uber jars can also be used 079/// as [ClasspathElement]. Therefore, if you want to create an uber jar 080/// from all resources provided by a project, you must not add the 081/// generator to the project like this: 082/// ```java 083/// generator(UberJarGenerator::new).add(this); // Circular dependency 084/// ``` 085/// 086/// This would add the project as provider and thus make the uber jar 087/// generator as supplier to the project its own provider (via 088/// [Project.provide][Project#provide]). Rather, you have to use this 089/// slightly more complicated approach to adding providers to the uber 090/// jar generator: 091/// ```java 092/// generator(UberJarGenerator::new) 093/// .addAll(providers(EnumSet.of(Forward, Expose, Supply))); 094/// ``` 095/// This requests the same providers from the project as 096/// [Project.provide][Project#provide] does, but allows the uber jar 097/// generator's [from] method to filter out the uber jar 098/// generator itself from the providers. The given intends can 099/// vary depending on the requirements. 100/// 101/// If you don't want the generated uber jar to be available to other 102/// generators of your project, you can also add it to a project like this: 103/// ```java 104/// dependency(new UberJarGenerator(this) 105/// .from(providers(EnumSet.of(Forward, Expose, Supply))), Intend.Forward) 106/// ``` 107/// 108/// Of course, the easiest thing to do is separate the generation of 109/// class trees or library jars from the generation of the uber jar by 110/// generating the uber jar in a project of its own. Often the root 111/// project can be used for this purpose. 112/// 113public class UberJarGenerator extends LibraryGenerator { 114 115 private Map<Path, java.util.jar.JarFile> openJars = Map.of(); 116 117 /// Instantiates a new uber jar generator. 118 /// 119 /// @param project the project 120 /// 121 public UberJarGenerator(Project project) { 122 super(project); 123 } 124 125 @Override 126 @SuppressWarnings("PMD.UseDiamondOperator") 127 protected void 128 collectFromProviders(Map<Path, Resources<IOResource>> contents) { 129 openJars = new ConcurrentHashMap<>(); 130 project().getFrom(providers().stream(), 131 new ResourceRequest<ClasspathElement>( 132 new ResourceType<RuntimeResources>() {})) 133 .parallel().forEach(cpe -> { 134 if (cpe instanceof FileTree<?> fileTree) { 135 collect(contents, fileTree); 136 } else if (cpe instanceof JarFile jarFile 137 && !(jarFile instanceof MvnRepoJarFile)) { 138 addJarFile(contents, jarFile, openJars); 139 } 140 }); 141 var lookup = new MvnRepoLookup(); 142 project().getFrom(providers().stream(), 143 new ResourceRequest<>(MvnRepoDependenciesType)) 144 .forEach(d -> lookup.resolve(d.coordinates())); 145 project().context().get(lookup, new ResourceRequest<ClasspathElement>( 146 new ResourceType<RuntimeResources>() {})) 147 .parallel().forEach(cpe -> { 148 if (cpe instanceof MvnRepoJarFile jarFile) { 149 addJarFile(contents, jarFile, openJars); 150 } 151 }); 152 } 153 154 private void addJarFile(Map<Path, Resources<IOResource>> entries, 155 JarFile jarFile, Map<Path, java.util.jar.JarFile> openJars) { 156 @SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.CloseResource" }) 157 java.util.jar.JarFile jar 158 = openJars.computeIfAbsent(jarFile.path(), _ -> { 159 try { 160 return new java.util.jar.JarFile(jarFile.path().toFile()); 161 } catch (IOException e) { 162 throw new BuildException("Cannot open resource " + jarFile 163 + ": " + e.getMessage()); 164 } 165 }); 166 jar.stream().filter(Predicate.not(JarEntry::isDirectory)) 167 .filter(e -> !Path.of(e.getName()) 168 .endsWith(Path.of("module-info.class"))) 169 .filter(e -> { 170 // Filter top-level entries in META-INF/ 171 var segs = Path.of(e.getRealName()).iterator(); 172 if (segs.next().equals(Path.of("META-INF"))) { 173 segs.next(); 174 return segs.hasNext(); 175 } 176 return true; 177 }).forEach(e -> { 178 var relPath = Path.of(e.getRealName()); 179 entries.computeIfAbsent(relPath, 180 _ -> project().newResource(IOResourcesType)) 181 .add(new JarFileEntry(jar, e)); 182 }); 183 } 184 185 @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition", 186 "PMD.PreserveStackTrace", "PMD.UselessPureMethodCall" }) 187 @Override 188 protected void resolveDuplicates(Map<Path, Resources<IOResource>> entries) { 189 entries.entrySet().parallelStream().forEach(item -> { 190 var candidates = item.getValue(); 191 if (candidates.stream().count() == 1) { 192 return; 193 } 194 var entryName = item.getKey(); 195 if (entryName.startsWith("META-INF/services")) { 196 var combined = new ServicesEntryResource(); 197 candidates.stream().forEach(service -> { 198 try { 199 combined.add(service); 200 } catch (IOException e) { 201 throw new BuildException("Cannot read " + service); 202 } 203 }); 204 candidates.clear(); 205 candidates.add(combined); 206 return; 207 } 208 if (entryName.startsWith("META-INF")) { 209 candidates.clear(); 210 } 211 candidates.stream().reduce((a, b) -> { 212 log.warning(() -> "Entry " + entryName + " from " + a 213 + " duplicates entry from " + b + " and is skipped."); 214 return a; 215 }); 216 }); 217 } 218 219 @Override 220 @SuppressWarnings({ "PMD.CollapsibleIfStatements", "unchecked", 221 "PMD.CloseResource", "PMD.UseTryWithResources" }) 222 protected <T extends Resource> Stream<T> 223 doProvide(ResourceRequest<T> requested) { 224 if (!requested.includes(AppJarFileType) 225 && !requested.includes(CleanlinessType)) { 226 return Stream.empty(); 227 } 228 229 // Make sure mainClass is set for app jar 230 if (AppJarFileType.isAssignableFrom(requested.type().containedType()) 231 && mainClass() == null) { 232 throw new BuildException("Main class must be set for " 233 + name() + " in " + project()); 234 } 235 236 // Prepare jar file 237 var destDir = destination(); 238 if (!destDir.toFile().exists()) { 239 if (!destDir.toFile().mkdirs()) { 240 throw new BuildException("Cannot create directory " + destDir); 241 } 242 } 243 var jarResource 244 = AppJarFileType.isAssignableFrom(requested.type().containedType()) 245 ? project().newResource(AppJarFileType, 246 destDir.resolve(jarName())) 247 : project().newResource(LibraryJarFileType, 248 destDir.resolve(jarName())); 249 250 // Maybe only delete 251 if (requested.includes(CleanlinessType)) { 252 jarResource.delete(); 253 return Stream.empty(); 254 } 255 256 try { 257 buildJar(jarResource); 258 } finally { 259 // buidJar indirectly calls collectFromProviders which opens 260 // resources that are used in buildJar. Close them now. 261 for (var jarFile : openJars.values()) { 262 try { 263 jarFile.close(); 264 } catch (IOException e) { // NOPMD 265 // Ignore, just trying to be nice. 266 } 267 } 268 269 } 270 return Stream.of((T) jarResource); 271 } 272}