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.ArrayList;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.function.Consumer;
030import java.util.stream.Stream;
031import org.jdrupes.builder.api.BuildException;
032import org.jdrupes.builder.api.Project;
033import org.jdrupes.builder.api.Resource;
034import org.jdrupes.builder.api.ResourceRequest;
035import static org.jdrupes.builder.api.ResourceType.resourceType;
036import org.jdrupes.builder.core.AbstractGenerator;
037
038/// The [VscodeConfigurator] provides the resource [VscodeConfiguration].
039/// The configuration consists of the configuration files:
040///   * .vscode/settings.json
041///   * .vscode/launch.json
042///   * .vscode/tasks.json
043///   
044/// Each generated data structure can be post processed by a corresponding
045/// `adapt` method before being written to disk.
046/// 
047/// VS Code relies on the `.project` and `.classpath` files as used by
048/// eclipse for its java support. Currently, the configurator does not
049/// generate these files. Use the eclipse configurator in addition.
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
103        Path vscodeDir = project().directory().resolve(".vscode");
104        vscodeDir.toFile().mkdirs();
105        try {
106            generateSettings(vscodeDir.resolve("settings.json"));
107            generateLaunch(vscodeDir.resolve("launch.json"));
108            generateTasks(vscodeDir.resolve("tasks.json"));
109        } catch (IOException e) {
110            throw new BuildException().from(this).cause(e);
111        }
112
113        // General overrides
114        configurationAdaptor.run();
115
116        // Return a result
117        @SuppressWarnings({ "unchecked" })
118        var result = (Stream<T>) Stream.of(VscodeConfiguration.of(project()));
119        return result;
120    }
121
122    private void generateSettings(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        // Allow user to adapt settings
129        settingsAdaptor.accept(settings);
130        ObjectMapper mapper = new ObjectMapper();
131        String json = mapper.writerWithDefaultPrettyPrinter()
132            .writeValueAsString(settings);
133        Files.writeString(file, json);
134    }
135
136    private void generateLaunch(Path file) throws IOException {
137        @SuppressWarnings("PMD.UseConcurrentHashMap")
138        Map<String, Object> launch = new HashMap<>();
139        launch.put("version", "0.2.0");
140        launch.put("configurations", new ArrayList<>());
141        launchAdaptor.accept(launch);
142        if (!((List<?>) launch.get("configurations")).isEmpty()) {
143            ObjectMapper mapper = new ObjectMapper();
144            String json = mapper.writerWithDefaultPrettyPrinter()
145                .writeValueAsString(launch);
146            Files.writeString(file, json);
147        }
148    }
149
150    /**
151     * Allow the user to adapt the launch data structure before writing.
152     * The version and an empty list for "configurations" has already be
153     * added.
154     *
155     * @param adaptor the adaptor
156     * @return the vscode configurator
157     */
158    public VscodeConfigurator
159            adaptLaunch(Consumer<Map<String, Object>> adaptor) {
160        launchAdaptor = adaptor;
161        return this;
162    }
163
164    private void generateTasks(Path file) throws IOException {
165        @SuppressWarnings("PMD.UseConcurrentHashMap")
166        Map<String, Object> tasks = new HashMap<>();
167        tasks.put("version", "2.0.0");
168        tasks.put("tasks", new ArrayList<>());
169        tasksAdaptor.accept(tasks);
170        if (!((List<?>) tasks.get("tasks")).isEmpty()) {
171            ObjectMapper mapper = new ObjectMapper();
172            String json
173                = mapper.writerWithDefaultPrettyPrinter()
174                    .writeValueAsString(tasks);
175            Files.writeString(file, json);
176        }
177    }
178
179    /**
180     * Allow the user to adapt the tasks data structure before writing.
181     *
182     * @param adaptor the adaptor
183     * @return the vscode configurator
184     */
185    public VscodeConfigurator
186            adaptTasks(Consumer<Map<String, Object>> adaptor) {
187        tasksAdaptor = adaptor;
188        return this;
189    }
190
191    /// Allow the user to add additional resources.
192    ///
193    /// @param adaptor the adaptor
194    /// @return the eclipse configurator
195    ///
196    public VscodeConfigurator adaptConfiguration(Runnable adaptor) {
197        configurationAdaptor = adaptor;
198        return this;
199    }
200}