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