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