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