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.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 [ClasspathElement] 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/// 070public class LibraryBuilder extends JarBuilder implements ResourceRetriever { 071 072 @SuppressWarnings({ "unused" }) 073 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 074 private final StreamCollector<ResourceProvider> providers 075 = StreamCollector.cached(); 076 private String mainClass; 077 078 /// Instantiates a new library generator. 079 /// 080 /// @param project the project 081 /// 082 public LibraryBuilder(Project project) { 083 super(project, LibraryJarFileType); 084 } 085 086 @Override 087 public LibraryBuilder name(String name) { 088 rename(name); 089 return this; 090 } 091 092 /// Returns the main class. 093 /// 094 /// @return the main class 095 /// 096 public String mainClass() { 097 return mainClass; 098 } 099 100 /// Sets the main class. 101 /// 102 /// @param mainClass the new main class 103 /// @return the library builder for method chaining 104 /// 105 public LibraryBuilder mainClass(String mainClass) { 106 this.mainClass = Objects.requireNonNull(mainClass); 107 return this; 108 } 109 110 @Override 111 public LibraryBuilder addFrom(ResourceProvider... providers) { 112 addFrom(Stream.of(providers)); 113 return this; 114 } 115 116 @Override 117 public LibraryBuilder addFrom(Stream<ResourceProvider> providers) { 118 this.providers.add(providers.filter(p -> !p.equals(this))); 119 return this; 120 } 121 122 /// return the cached providers. 123 /// 124 /// @return the content providers 125 /// 126 protected StreamCollector<ResourceProvider> contentProviders() { 127 return providers; 128 } 129 130 @Override 131 protected void 132 collectContents(Map<Path, Resources<InputResource>> contents) { 133 super.collectContents(contents); 134 // Add main class if defined 135 if (mainClass() != null) { 136 attributes(Map.entry(Attributes.Name.MAIN_CLASS, mainClass())); 137 } 138 collectFromProviders(contents); 139 } 140 141 /// Collects the contents from the providers. This implementation 142 /// requests [ClassTree]s and [JavaResourceTree]s. 143 /// 144 /// @param contents the contents 145 /// 146 protected void collectFromProviders( 147 Map<Path, Resources<InputResource>> contents) { 148 contentProviders().stream() 149 .map(p -> p.resources(of(ClassTreeType).using(Supply))) 150 // Terminate to trigger all future stream evaluations before 151 // starting to process the results. Then collect in parallel. 152 .toList().stream().flatMap(s -> s).toList().parallelStream() 153 .forEach(t -> collect(contents, t)); 154 contentProviders().stream() 155 .map(p -> p.resources(of(JavaResourceTreeType).using(Supply))) 156 // Terminate to trigger all future stream evaluations before 157 // starting to process the results. Then collect in parallel. 158 .toList().stream().flatMap(s -> s).toList().parallelStream() 159 .forEach(t -> collect(contents, t)); 160 } 161 162 @Override 163 @SuppressWarnings({ "PMD.CollapsibleIfStatements", "unchecked", 164 "PMD.CyclomaticComplexity" }) 165 protected <T extends Resource> Collection<T> 166 doProvide(ResourceRequest<T> request) { 167 if (!request.accepts(LibraryJarFileType) 168 && !request.accepts(CleanlinessType)) { 169 return Collections.emptyList(); 170 } 171 172 // Maybe only delete 173 if (request.accepts(CleanlinessType)) { 174 destination().resolve(jarName()).toFile().delete(); 175 return Collections.emptyList(); 176 } 177 178 // Upgrade to most specific type to avoid duplicate generation 179 if (mainClass() != null && !request.type().equals(AppJarFileType)) { 180 return (Collection<T>) context() 181 .resources(this, project().of(AppJarFileType)).toList(); 182 } 183 if (mainClass() == null && !request.type().equals(LibraryJarFileType)) { 184 return (Collection<T>) context() 185 .resources(this, project().of(LibraryJarFileType)).toList(); 186 } 187 188 // Make sure mainClass is set for app jar 189 if (request.isFor(AppJarFileType) && mainClass() == null) { 190 throw new ConfigurationException().from(this).message( 191 "Main class must be set for %s", name()); 192 } 193 194 // Prepare jar file 195 var destDir = destination(); 196 if (!destDir.toFile().exists()) { 197 if (!destDir.toFile().mkdirs()) { 198 throw new BuildException().from(this) 199 .message("Cannot create directory " + destDir); 200 } 201 } 202 var jarResource = request.isFor(AppJarFileType) 203 ? AppJarFile.of(destDir.resolve(jarName())) 204 : LibraryJarFile.of(destDir.resolve(jarName())); 205 206 buildJar(jarResource); 207 return List.of((T) jarResource); 208 } 209}