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