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