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