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 062 implements ResourceRetriever { 063 064 private final StreamCollector<ResourceProvider> providers 065 = StreamCollector.cached(); 066 private String mainClass; 067 068 /// Instantiates a new library generator. 069 /// 070 /// @param project the project 071 /// 072 public LibraryGenerator(Project project) { 073 super(project, LibraryJarFileType); 074 } 075 076 /// Returns the main class. 077 /// 078 /// @return the main class 079 /// 080 public String mainClass() { 081 return mainClass; 082 } 083 084 /// Sets the main class. 085 /// 086 /// @param mainClass the new main class 087 /// @return the jar generator for method chaining 088 /// 089 public LibraryGenerator mainClass(String mainClass) { 090 this.mainClass = mainClass; 091 return this; 092 } 093 094 /// Additionally uses the given providers for obtaining contents for the 095 /// jar. 096 /// 097 /// @param providers the providers 098 /// @return the jar generator 099 /// 100 @Override 101 public LibraryGenerator from(ResourceProvider... providers) { 102 from(Stream.of(providers)); 103 return this; 104 } 105 106 /// Additionally uses the given providers for obtaining contents for the 107 /// jar. 108 /// 109 /// @param providers the providers 110 /// @return the jar generator 111 /// 112 @Override 113 public LibraryGenerator from(Stream<ResourceProvider> providers) { 114 this.providers.add(providers.filter(p -> !p.equals(this))); 115 return this; 116 } 117 118 /// return the cached providers. 119 /// 120 /// @return the cached stream 121 /// 122 protected StreamCollector<ResourceProvider> providers() { 123 return providers; 124 } 125 126 @Override 127 protected void collectContents(Map<Path, Resources<IOResource>> contents) { 128 super.collectContents(contents); 129 // Add main class if defined 130 if (mainClass() != null) { 131 attributes(Map.entry(Attributes.Name.MAIN_CLASS, mainClass())); 132 } 133 collectFromProviders(contents); 134 } 135 136 /// Collects the contents from the providers. This implementation 137 /// requests [ClassTree]s and [JavaResourceTree]s. 138 /// 139 /// @param contents the contents 140 /// 141 protected void 142 collectFromProviders(Map<Path, Resources<IOResource>> contents) { 143 project().from(providers().stream()) 144 .get(new ResourceRequest<ClassTree>(new ResourceType<>() {})) 145 .parallel().forEach(t -> collect(contents, t)); 146 project().from(providers().stream()) 147 .get(new ResourceRequest<JavaResourceTree>(new ResourceType<>() {})) 148 .parallel().forEach(t -> collect(contents, t)); 149 } 150 151 @Override 152 @SuppressWarnings({ "PMD.CollapsibleIfStatements", "unchecked" }) 153 protected <T extends Resource> Stream<T> 154 doProvide(ResourceRequest<T> requested) { 155 if (!requested.collects(LibraryJarFileType) 156 && !requested.collects(CleanlinessType)) { 157 return Stream.empty(); 158 } 159 160 // Make sure mainClass is set for app jar 161 if (AppJarFileType.isAssignableFrom(requested.type().containedType()) 162 && mainClass() == null) { 163 throw new BuildException("Main class must be set for " 164 + name() + " in " + project()); 165 } 166 167 // Prepare jar file 168 var destDir = destination(); 169 if (!destDir.toFile().exists()) { 170 if (!destDir.toFile().mkdirs()) { 171 throw new BuildException("Cannot create directory " + destDir); 172 } 173 } 174 var jarResource 175 = AppJarFileType.isAssignableFrom(requested.type().containedType()) 176 ? project().newResource(AppJarFileType, 177 destDir.resolve(jarName())) 178 : project().newResource(LibraryJarFileType, 179 destDir.resolve(jarName())); 180 181 // Maybe only delete 182 if (requested.collects(CleanlinessType)) { 183 jarResource.delete(); 184 return Stream.empty(); 185 } 186 187 buildJar(jarResource); 188 return Stream.of((T) jarResource); 189 } 190}