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.vscode;
020
021import com.fasterxml.jackson.databind.ObjectMapper;
022import java.io.IOException;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.function.Consumer;
029import java.util.stream.Collectors;
030import java.util.stream.Stream;
031import org.jdrupes.builder.api.BuildException;
032import static org.jdrupes.builder.api.Intend.*;
033import org.jdrupes.builder.api.Project;
034import org.jdrupes.builder.api.Resource;
035import org.jdrupes.builder.api.ResourceRequest;
036import static org.jdrupes.builder.api.ResourceRequest.*;
037import static org.jdrupes.builder.api.ResourceType.resourceType;
038import org.jdrupes.builder.core.AbstractGenerator;
039import org.jdrupes.builder.java.JarFile;
040import org.jdrupes.builder.java.JavaCompiler;
041import static org.jdrupes.builder.java.JavaTypes.*;
042
043/// The [VscodeConfigurator] provides the resource [VscodeConfiguration].
044/// The configuration consists of the configuration files:
045///   * .vscode/settings.json
046///   * .vscode/launch.json
047///   * .vscode/tasks.json
048///   
049/// Each generated data structure can be post processed by a corresponding
050/// `adapt` method before being written to disk.
051/// 
052public class VscodeConfigurator extends AbstractGenerator {
053    @SuppressWarnings({ "PMD.UseConcurrentHashMap",
054        "PMD.AvoidDuplicateLiterals" })
055    private final Map<String, Path> jdkLocations = new HashMap<>();
056    private Consumer<Map<String, Object>> settingsAdaptor = _ -> {
057    };
058    private Consumer<Map<String, Object>> launchAdaptor = _ -> {
059    };
060    private Consumer<Map<String, Object>> tasksAdaptor = _ -> {
061    };
062    private Runnable configurationAdaptor = () -> {
063    };
064
065    /// Initializes a new vscode configurator.
066    ///
067    /// @param project the project
068    ///
069    public VscodeConfigurator(Project project) {
070        super(project);
071    }
072
073    /**
074     * Allow the user to adapt the settings data structure before writing.
075     *
076     * @param adaptor the adaptor
077     * @return the vscode configurator
078     */
079    public VscodeConfigurator
080            adaptSettings(Consumer<Map<String, Object>> adaptor) {
081        settingsAdaptor = adaptor;
082        return this;
083    }
084
085    /// VSCode does not have a central JDK registry. JDKs can therefore
086    /// be configured with this method. 
087    ///
088    /// @param version the version
089    /// @param location the location
090    /// @return the vscode configurator
091    ///
092    public VscodeConfigurator jdk(String version, Path location) {
093        jdkLocations.put(version, location);
094        return this;
095    }
096
097    @Override
098    protected <T extends Resource> Stream<T>
099            doProvide(ResourceRequest<T> requested) {
100        if (!requested.collects(resourceType(VscodeConfiguration.class))) {
101            return Stream.empty();
102        }
103        Path vscodeDir = project().directory().resolve(".vscode");
104        vscodeDir.toFile().mkdirs();
105        try {
106            generateSettingsJson(vscodeDir.resolve("settings.json"));
107            generateLaunchJson(vscodeDir.resolve("launch.json"));
108            generateTasksJson(vscodeDir.resolve("tasks.json"));
109        } catch (IOException e) {
110            throw new BuildException(e);
111        }
112
113        // General overrides
114        configurationAdaptor.run();
115
116        // Return a result
117        @SuppressWarnings({ "unchecked" })
118        var result = (Stream<T>) Stream.of(project().newResource(
119            resourceType(VscodeConfiguration.class), project().directory()));
120        return result;
121    }
122
123    private void generateSettingsJson(Path file) throws IOException {
124        @SuppressWarnings({ "PMD.UseConcurrentHashMap" })
125        Map<String, Object> settings = new HashMap<>();
126        settings.put("java.configuration.updateBuildConfiguration",
127            "automatic");
128
129        // Set java compiler target
130        project().providers(Supply).filter(p -> p instanceof JavaCompiler)
131            .map(p -> (JavaCompiler) p)
132            .findFirst()
133            .flatMap(
134                jc -> jc.optionArgument("--release", "--target", "-target"))
135            .filter(jdkLocations::containsKey)
136            .ifPresent(version -> {
137                @SuppressWarnings("PMD.UseConcurrentHashMap")
138                Map<String, Object> runtime = new HashMap<>();
139                runtime.put("name", "JavaSE-" + version);
140                runtime.put("path", jdkLocations.get(version).toString());
141                runtime.put("default", true);
142                settings.put("java.configuration.runtimes", List.of(runtime));
143            });
144
145        // Add output directories of contributing projects
146        var referenced = new java.util.ArrayList<String>();
147        collectContributing(project()).collect(Collectors.toSet()).stream()
148            .forEach(proj -> {
149                referenced
150                    .add(proj.buildDirectory().resolve("classes").toString());
151            });
152
153        // Add JARs to classpath
154        referenced.addAll(project().provided(
155            requestFor(CompilationClasspathType))
156            .filter(p -> p instanceof JarFile)
157            .map(jf -> ((JarFile) jf).path().toString())
158            .collect(Collectors.toList()));
159        if (!referenced.isEmpty()) {
160            settings.put("java.project.referencedLibraries", referenced);
161        }
162
163        // Allow user to adapt settings
164        settingsAdaptor.accept(settings);
165        ObjectMapper mapper = new ObjectMapper();
166        String json = mapper.writerWithDefaultPrettyPrinter()
167            .writeValueAsString(settings);
168        Files.writeString(file, json);
169    }
170
171    private Stream<Project> collectContributing(Project project) {
172        return project.providers(Consume, Forward, Expose)
173            .filter(p -> p instanceof Project).map(p -> (Project) p)
174            .map(p -> Stream.concat(Stream.of(p), collectContributing(p)))
175            .flatMap(s -> s);
176    }
177
178    private void generateLaunchJson(Path file) throws IOException {
179        @SuppressWarnings("PMD.UseConcurrentHashMap")
180        Map<String, Object> launch = new HashMap<>();
181        launch.put("version", "0.2.0");
182        launch.put("configurations", new java.util.ArrayList<>());
183        launchAdaptor.accept(launch);
184        ObjectMapper mapper = new ObjectMapper();
185        String json = mapper.writerWithDefaultPrettyPrinter()
186            .writeValueAsString(launch);
187        Files.writeString(file, json);
188    }
189
190    /**
191     * Allow the user to adapt the launch data structure before writing.
192     * The version and an empty list for "configurations" has already be
193     * added.
194     *
195     * @param adaptor the adaptor
196     * @return the vscode configurator
197     */
198    public VscodeConfigurator
199            adaptLaunch(Consumer<Map<String, Object>> adaptor) {
200        launchAdaptor = adaptor;
201        return this;
202    }
203
204    private void generateTasksJson(Path file) throws IOException {
205        @SuppressWarnings("PMD.UseConcurrentHashMap")
206        Map<String, Object> tasks = new HashMap<>();
207        tasks.put("version", "2.0.0");
208        tasks.put("tasks", new java.util.ArrayList<>());
209        tasksAdaptor.accept(tasks);
210        ObjectMapper mapper = new ObjectMapper();
211        String json
212            = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(tasks);
213        Files.writeString(file, json);
214    }
215
216    /**
217     * Allow the user to adapt the tasks data structure before writing.
218     *
219     * @param adaptor the adaptor
220     * @return the vscode configurator
221     */
222    public VscodeConfigurator
223            adaptTasks(Consumer<Map<String, Object>> adaptor) {
224        tasksAdaptor = adaptor;
225        return this;
226    }
227
228    /// Allow the user to add additional resources.
229    ///
230    /// @param adaptor the adaptor
231    /// @return the eclipse configurator
232    ///
233    public VscodeConfigurator adaptConfiguration(Runnable adaptor) {
234        configurationAdaptor = adaptor;
235        return this;
236    }
237}