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}