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