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.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.IOResource; 034import static org.jdrupes.builder.api.Intent.*; 035import org.jdrupes.builder.api.Project; 036import org.jdrupes.builder.api.Resource; 037import org.jdrupes.builder.api.ResourceProvider; 038import org.jdrupes.builder.api.ResourceRequest; 039import org.jdrupes.builder.api.ResourceRetriever; 040import org.jdrupes.builder.api.ResourceType; 041import static org.jdrupes.builder.api.ResourceType.*; 042import org.jdrupes.builder.api.Resources; 043import org.jdrupes.builder.core.StreamCollector; 044import static org.jdrupes.builder.java.JavaTypes.*; 045 046/// A [Generator] for Java libraries packaged as jars. A library jar 047/// is expected to contain class files and supporting resources together 048/// with additional information in `META-INF/`. 049/// 050/// The generator provides two types of resources. 051/// 052/// 1. A [LibraryJarFile]. This type of resource is also returned if a more 053/// general [ResourceType] such as [ClasspathElement] is requested. 054/// 055/// 2. An [AppJarFile]. When requesting this special jar type, the 056/// generator checks if a main class is specified. 057/// 058/// In addition to explicitly adding resources, this generator supports 059/// resource retrieval from added providers. The resources of type [ClassTree] 060/// and [JavaResourceTree] that the providers supply will be used in 061/// addition to the explicitly added resources. To make adding providers 062/// easier, the [LibraryBuilder] itself is automatically filtered from the 063/// added providers. This makes it possible to add a project as provider 064/// even if this includes the [LibraryBuilder]. 065/// 066/// The standard pattern for creating a library is: 067/// ```java 068/// generator(LibraryGenerator::new).addFrom(providers().select(Supply)); 069/// ``` 070/// 071public class LibraryBuilder extends JarBuilder 072 implements ResourceRetriever { 073 074 @SuppressWarnings({ "unused" }) 075 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 076 private final StreamCollector<ResourceProvider> providers 077 = StreamCollector.cached(); 078 private String mainClass; 079 080 /// Instantiates a new library generator. 081 /// 082 /// @param project the project 083 /// 084 public LibraryBuilder(Project project) { 085 super(project, LibraryJarFileType); 086 } 087 088 @Override 089 public LibraryBuilder name(String name) { 090 rename(name); 091 return this; 092 } 093 094 /// Returns the main class. 095 /// 096 /// @return the main class 097 /// 098 public String mainClass() { 099 return mainClass; 100 } 101 102 /// Sets the main class. 103 /// 104 /// @param mainClass the new main class 105 /// @return the jar generator for method chaining 106 /// 107 public LibraryBuilder mainClass(String mainClass) { 108 this.mainClass = Objects.requireNonNull(mainClass); 109 return this; 110 } 111 112 @Override 113 public LibraryBuilder addFrom(ResourceProvider... providers) { 114 addFrom(Stream.of(providers)); 115 return this; 116 } 117 118 @Override 119 public LibraryBuilder addFrom(Stream<ResourceProvider> providers) { 120 this.providers.add(providers.filter(p -> !p.equals(this))); 121 return this; 122 } 123 124 /// return the cached providers. 125 /// 126 /// @return the cached stream 127 /// 128 protected StreamCollector<ResourceProvider> contentProviders() { 129 return providers; 130 } 131 132 @Override 133 protected void collectContents(Map<Path, Resources<IOResource>> contents) { 134 super.collectContents(contents); 135 // Add main class if defined 136 if (mainClass() != null) { 137 attributes(Map.entry(Attributes.Name.MAIN_CLASS, mainClass())); 138 } 139 collectFromProviders(contents); 140 } 141 142 /// Collects the contents from the providers. This implementation 143 /// requests [ClassTree]s and [JavaResourceTree]s. 144 /// 145 /// @param contents the contents 146 /// 147 protected void collectFromProviders( 148 Map<Path, Resources<IOResource>> contents) { 149 contentProviders().stream() 150 .map(p -> p.resources(of(ClassTreeType).using(Supply))) 151 // Terminate to trigger all future stream evaluations before 152 // starting to process the results. Then collect in parallel. 153 .toList().stream().flatMap(s -> s).toList().parallelStream() 154 .forEach(t -> collect(contents, t)); 155 contentProviders().stream() 156 .map(p -> p.resources(of(JavaResourceTreeType).using(Supply))) 157 // Terminate to trigger all future stream evaluations before 158 // starting to process the results. Then collect in parallel. 159 .toList().stream().flatMap(s -> s).toList().parallelStream() 160 .forEach(t -> collect(contents, t)); 161 } 162 163 @Override 164 @SuppressWarnings({ "PMD.CollapsibleIfStatements", "unchecked", 165 "PMD.CyclomaticComplexity" }) 166 protected <T extends Resource> Collection<T> 167 doProvide(ResourceRequest<T> request) { 168 if (!request.accepts(LibraryJarFileType) 169 && !request.accepts(CleanlinessType)) { 170 return Collections.emptyList(); 171 } 172 173 // Maybe only delete 174 if (request.accepts(CleanlinessType)) { 175 destination().resolve(jarName()).toFile().delete(); 176 return Collections.emptyList(); 177 } 178 179 // Upgrade to most specific type to avoid duplicate generation 180 if (mainClass() != null && !request.type().equals(AppJarFileType)) { 181 return (Collection<T>) context() 182 .resources(this, project().of(AppJarFileType)).toList(); 183 } 184 if (mainClass() == null && !request.type().equals(LibraryJarFileType)) { 185 return (Collection<T>) context() 186 .resources(this, project().of(LibraryJarFileType)).toList(); 187 } 188 189 // Make sure mainClass is set for app jar 190 if (request.isFor(AppJarFileType) && mainClass() == null) { 191 throw new ConfigurationException().from(this).message( 192 "Main class must be set for %s", name()); 193 } 194 195 // Prepare jar file 196 var destDir = destination(); 197 if (!destDir.toFile().exists()) { 198 if (!destDir.toFile().mkdirs()) { 199 throw new BuildException().from(this) 200 .message("Cannot create directory " + destDir); 201 } 202 } 203 var jarResource = request.isFor(AppJarFileType) 204 ? AppJarFile.of(destDir.resolve(jarName())) 205 : LibraryJarFile.of(destDir.resolve(jarName())); 206 207 buildJar(jarResource); 208 return List.of((T) jarResource); 209 } 210}