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 java.nio.file.Path;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.List;
026import java.util.Locale;
027import java.util.Optional;
028import java.util.stream.Stream;
029import javax.tools.Diagnostic;
030import javax.tools.DiagnosticCollector;
031import javax.tools.JavaFileObject;
032import org.jdrupes.builder.api.Project;
033import org.jdrupes.builder.core.AbstractGenerator;
034
035/// A base class for generators that invoke java tools.
036///
037public abstract class JavaTool extends AbstractGenerator {
038
039    /// Controls how Java compilation handles JPMS modules.
040    ///
041    public enum ModuleMode {
042        /// Always use classpath compilation.
043        CLASSPATH,
044        /// Always use module-path compilation for modular dependencies.
045        MODULE,
046        /// Use module-path compilation when sources contain
047        /// `module-info.java`, otherwise fall back to classpath.
048        AUTO
049    }
050
051    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
052    private JavaTool.ModuleMode moduleMode = JavaTool.ModuleMode.AUTO;
053    private final List<String> options = new ArrayList<>();
054
055    /// Instantiates a new java tool.
056    ///
057    /// @param project the project
058    ///
059    public JavaTool(Project project) {
060        super(project);
061    }
062
063    /// Returns the current module compilation mode. Defaults to
064    /// `ModuleMode.AUTO`, which activates module-path compilation when
065    /// a `module-info.java` is found among the sources.
066    ///
067    /// @return the module mode
068    ///
069    public JavaTool.ModuleMode moduleMode() {
070        return moduleMode;
071    }
072
073    /// Sets the module compilation mode.
074    ///
075    /// @param moduleMode the new mode
076    /// @return the java compiler
077    ///
078    public JavaTool moduleMode(JavaTool.ModuleMode moduleMode) {
079        this.moduleMode = moduleMode;
080        return this;
081    }
082
083    /// Determines the effective module mode, resolving `ModuleMode.AUTO`
084    /// to either `ModuleMode.MODULE` or `ModuleMode.CLASSPATH` based on
085    /// whether a `module-info.java` exists among the sources.
086    ///
087    /// @return the effective mode
088    ///
089    protected ModuleMode effectiveModuleMode() {
090        if (moduleMode == JavaTool.ModuleMode.AUTO) {
091            return hasModuleInfo() ? JavaTool.ModuleMode.MODULE
092                : JavaTool.ModuleMode.CLASSPATH;
093        }
094        return moduleMode;
095    }
096
097    /// Checks whether a `module-info.java` exists among the configured
098    /// sources.
099    ///
100    /// @return true, if module-info.java is present
101    ///
102    protected abstract boolean hasModuleInfo();
103
104    /// Adds the given options.
105    ///
106    /// @param options the options
107    /// @return the java tool
108    ///
109    public JavaTool options(Stream<String> options) {
110        this.options.addAll(options.toList());
111        return this;
112    }
113
114    /// Adds the given options.
115    ///
116    /// @param options the options
117    /// @return the java tool
118    ///
119    public JavaTool options(String... options) {
120        this.options.addAll(Arrays.asList(options));
121        return this;
122    }
123
124    /// Return the options.
125    ///
126    /// @return the stream
127    ///
128    public List<String> options() {
129        return options;
130    }
131
132    /// Find the argument for the given option. As some options are
133    /// allows in different styles, several names can be specified. 
134    ///
135    /// @param names the names
136    /// @return the optional
137    ///
138    public Optional<String> optionArgument(String... names) {
139        var itr = options.iterator();
140        if (itr.hasNext()) {
141            String opt = itr.next();
142            if (Arrays.stream(names).anyMatch(opt::equals) && itr.hasNext()) {
143                return Optional.of(itr.next());
144            }
145        }
146        return Optional.empty();
147    }
148
149    /// Log diagnostic.
150    ///
151    /// @param diagnostic the diagnostic
152    ///
153    protected void logDiagnostic(
154            Diagnostic<? extends JavaFileObject> diagnostic) {
155        String level = switch (diagnostic.getKind()) {
156        case ERROR -> "error";
157        case WARNING -> "warning";
158        case MANDATORY_WARNING -> "warning";
159        default -> "info";
160        };
161        String msg;
162        if (diagnostic.getSource() == null) {
163            msg = level + ": " + diagnostic.getMessage(Locale.ENGLISH);
164        } else {
165            msg = String.format("%s:%d:%d: %s: %s",
166                project().rootProject().directory().relativize(
167                    Path.of(diagnostic.getSource().toUri().getPath())),
168                diagnostic.getLineNumber(), diagnostic.getColumnNumber(),
169                level, diagnostic.getMessage(null));
170        }
171        switch (diagnostic.getKind()) {
172        case ERROR -> logger.atSevere().log(msg);
173        case WARNING -> logger.atWarning().log(msg);
174        case MANDATORY_WARNING -> logger.atWarning().log(msg);
175        default -> logger.atInfo().log(msg);
176        }
177        if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
178            context().error().println(msg);
179        } else {
180            context().out().println(msg);
181        }
182    }
183
184    /// Log diagnostics.
185    ///
186    /// @param diagnostics the diagnostics
187    ///
188    protected void
189            logDiagnostics(DiagnosticCollector<JavaFileObject> diagnostics) {
190        if (diagnostics.getDiagnostics().isEmpty()) {
191            return;
192        }
193        context().out().println("Problems found by " + this + ":");
194        for (var diagnostic : diagnostics.getDiagnostics()) {
195            logDiagnostic(diagnostic);
196        }
197    }
198}