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 static com.google.common.flogger.LazyArgs.*; 023import java.io.File; 024import java.nio.file.Path; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.List; 028import java.util.function.Function; 029import java.util.stream.Collectors; 030import java.util.stream.Stream; 031import javax.tools.DiagnosticCollector; 032import javax.tools.JavaFileObject; 033import javax.tools.ToolProvider; 034import org.jdrupes.builder.api.BuildException; 035import org.jdrupes.builder.api.FileResource; 036import org.jdrupes.builder.api.FileTree; 037import static org.jdrupes.builder.api.Intent.*; 038import org.jdrupes.builder.api.MergedTestProject; 039import org.jdrupes.builder.api.Project; 040import static org.jdrupes.builder.api.Project.Properties.*; 041import org.jdrupes.builder.api.Resource; 042import org.jdrupes.builder.api.ResourceRequest; 043import org.jdrupes.builder.api.ResourceType; 044import static org.jdrupes.builder.api.ResourceType.*; 045import org.jdrupes.builder.api.Resources; 046import org.jdrupes.builder.api.UnavailableException; 047import static org.jdrupes.builder.java.JavaTypes.*; 048 049/// The [JavaCompiler] generator provides two types of resources. 050/// 051/// 1. The [JavaSourceFile]s of the project as configured with 052/// [addSources(FileTree<JavaSourceFile>)][addSources] 053/// in response to a [ResourceRequest] with [ResourceType] 054/// [JavaTypes#JavaSourceTreeType] (or a more general type). 055/// 056/// 2. The [ClassFile]s that result from compiling the sources in response 057/// to a [ResourceRequest] with [ResourceType] 058/// [JavaTypes#ClassTreeType] (or a more general type such as 059/// [JavaTypes#ClasspathElementType]). 060/// 061/// No attempt has been made to define types for the options of 062/// the java compiler. Rather, the options are passed as strings 063/// as the [ToolProvider] API suggests. There are some noteworthy 064/// exceptions for options that are directly related to resource 065/// types (files, directory trees, paths) from the builder context. 066/// 067/// If no "`-g...`" option is specified, the generator adds "`-g`" and 068/// thus generates full debug information. If you want to restore the 069/// default behavior of the java compiler, you have to specify 070/// "`-g:[lines, source]`" explicitly. 071/// 072@SuppressWarnings("PMD.TooManyStaticImports") 073public class JavaCompiler extends JavaTool { 074 075 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 076 private final Resources<FileTree<JavaSourceFile>> sources 077 = Resources.of(new ResourceType<>() {}); 078 private Path destination; 079 080 /// Initializes a new java compiler. 081 /// 082 /// @param project the project 083 /// 084 public JavaCompiler(Project project) { 085 super(project); 086 if (project instanceof MergedTestProject) { 087 destination = Path.of("test-classes"); 088 } else { 089 destination = Path.of("classes"); 090 } 091 } 092 093 /// Returns the destination directory. Defaults to "`classes`" for 094 /// "normal" projects and to "`test-classes`" for projects that 095 /// implement the [MergedTestProject] interface. 096 /// 097 /// @return the destination 098 /// 099 public Path destination() { 100 return project().buildDirectory().resolve(destination); 101 } 102 103 /// Sets the destination directory. The [Path] is resolved against 104 /// the project's build directory (see [Project#buildDirectory]). 105 /// 106 /// @param destination the new destination 107 /// @return the java compiler 108 /// 109 public JavaCompiler destination(Path destination) { 110 this.destination = destination; 111 return this; 112 } 113 114 /// Adds the source tree. 115 /// 116 /// @param sources the sources 117 /// @return the java compiler 118 /// 119 public final JavaCompiler addSources(FileTree<JavaSourceFile> sources) { 120 this.sources.add(sources); 121 return this; 122 } 123 124 /// Adds the files from the given directory matching the given pattern. 125 /// Short for 126 /// `addSources(project().newFileTree(directory, pattern, JavaSourceFile.class))`. 127 /// 128 /// @param directory the directory 129 /// @param pattern the pattern 130 /// @return the resources collector 131 /// 132 public final JavaCompiler addSources(Path directory, String pattern) { 133 addSources(FileTree.of( 134 project(), directory, JavaSourceFile.class, pattern)); 135 return this; 136 } 137 138 /// Adds the sources. 139 /// 140 /// @param sources the sources 141 /// @return the java compiler 142 /// 143 public final JavaCompiler 144 addSources(Stream<FileTree<JavaSourceFile>> sources) { 145 this.sources.addAll(sources); 146 return this; 147 } 148 149 /// Return the source trees configured for the compiler. 150 /// 151 /// @return the resources 152 /// 153 public Resources<FileTree<JavaSourceFile>> sources() { 154 return sources; 155 } 156 157 /// Source paths. 158 /// 159 /// @return the collection 160 /// 161 private Collection<Path> sourcePaths() { 162 return sources.stream().map(Resources::stream) 163 .flatMap(Function.identity()).map(FileResource::path) 164 .collect(Collectors.toList()); 165 } 166 167 @Override 168 protected <T extends Resource> Stream<T> 169 doProvide(ResourceRequest<T> requested) { 170 if (requested.accepts(JavaSourceTreeType)) { 171 @SuppressWarnings({ "unchecked" }) 172 var result = (Stream<T>) sources.stream(); 173 return result; 174 } 175 176 if (!requested.accepts(ClassTreeType) 177 && !requested.accepts(CleanlinessType)) { 178 return Stream.empty(); 179 } 180 181 // Get this project's previously generated classes for checking 182 // or deleting. 183 var destDir = project().buildDirectory().resolve(destination); 184 final var classSet = ClassTree.of(project(), destDir); 185 if (requested.accepts(CleanlinessType)) { 186 classSet.cleanup(); 187 return Stream.empty(); 188 } 189 190 // Evaluate for most special type 191 if (requested.accepts(ClassTreeType) 192 && !requested.type().equals(ClassTreeType)) { 193 @SuppressWarnings("unchecked") 194 var result = (Stream<T>) resources(of(ClassTreeType)); 195 return result; 196 } 197 198 // Get classpath for compilation. Filter myself, in case the 199 // compilation result is consumed by the project. 200 var cpResources = Resources.of(ClasspathType).addAll( 201 project().providers(Consume, Reveal, Expose).without(this) 202 .resources(of(ClasspathElementType))); 203 logger.atFiner().log("Compiling in %s with classpath %s", project(), 204 lazy(() -> cpResources.stream().map(e -> e.toPath().toString()) 205 .collect(Collectors.joining(File.pathSeparator)))); 206 207 // (Re-)compile only if necessary 208 if (sources.isNewerThan(classSet) 209 || cpResources.isNewerThan(classSet) 210 || classSet.stream().count() < sources.stream() 211 .flatMap(Resources::stream).map(FileResource::path) 212 .filter(p -> p.toString().endsWith(".java") 213 && !p.endsWith("package-info.java") 214 && !p.endsWith("module-info.java")) 215 .count()) { 216 classSet.cleanup(); 217 compile(cpResources, destDir); 218 } else { 219 logger.atFine().log("Classes in %s are up to date", project()); 220 } 221 classSet.clear(); 222 @SuppressWarnings("unchecked") 223 var result = (Stream<T>) Stream.of(classSet); 224 return result; 225 } 226 227 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 228 "PMD.ExceptionAsFlowControl" }) 229 private void compile(Resources<ClasspathElement> cpResources, 230 Path destDir) { 231 logger.atInfo().log("Compiling Java in %s", project().name()); 232 var classpath = cpResources.stream().map(e -> e.toPath().toString()) 233 .collect(Collectors.joining(File.pathSeparator)); 234 var javac = ToolProvider.getSystemJavaCompiler(); 235 var diagnostics = new DiagnosticCollector<JavaFileObject>(); 236 try (var fileManager 237 = javac.getStandardFileManager(diagnostics, null, null)) { 238 var compilationUnits 239 = fileManager.getJavaFileObjectsFromPaths(sourcePaths()); 240 List<String> allOptions = new ArrayList<>(options()); 241 242 // If no -g... option is given, add -g (full debug info) 243 if (allOptions.stream() 244 .filter(o -> o.startsWith("-g")).findAny().isEmpty()) { 245 allOptions.add("-g"); 246 } 247 248 // Add options from specific properties 249 allOptions.addAll(List.of( 250 "-d", destDir.toString(), 251 "-cp", classpath, 252 "-encoding", project().get(Encoding).toString())); 253 if (!javac.getTask(null, fileManager, diagnostics, allOptions, 254 null, compilationUnits).call()) { 255 throw new UnavailableException().from(this); 256 } 257 } catch (Exception e) { 258 logger.atSevere().withCause(e) 259 .log("Project %s: Problem compiling Java: %s", project().name(), 260 e.getMessage()); 261 throw new BuildException().from(this).cause(e); 262 } finally { 263 logDiagnostics(diagnostics); 264 } 265 } 266}