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