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}