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