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        if (!requested.accepts(resourceType(VscodeConfiguration.class))) {
101            return Collections.emptyList();
102        }
103
104        Path vscodeDir = project().directory().resolve(".vscode");
105        vscodeDir.toFile().mkdirs();
106        try {
107            generateSettings(vscodeDir.resolve("settings.json"));
108            generateLaunch(vscodeDir.resolve("launch.json"));
109            generateTasks(vscodeDir.resolve("tasks.json"));
110        } catch (IOException e) {
111            throw new BuildException().from(this).cause(e);
112        }
113
114        // General overrides
115        configurationAdaptor.run();
116
117        // Return a result
118        @SuppressWarnings({ "unchecked" })
119        var result = (Collection<T>) List.of(VscodeConfiguration.of(project()));
120        return result;
121    }
122
123    private void generateSettings(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        // Allow user to adapt settings
130        settingsAdaptor.accept(settings);
131        ObjectMapper mapper = new ObjectMapper();
132        String json = mapper.writerWithDefaultPrettyPrinter()
133            .writeValueAsString(settings);
134        Files.writeString(file, json);
135    }
136
137    private void generateLaunch(Path file) throws IOException {
138        @SuppressWarnings("PMD.UseConcurrentHashMap")
139        Map<String, Object> launch = new HashMap<>();
140        launch.put("version", "0.2.0");
141        launch.put("configurations", new ArrayList<>());
142        launchAdaptor.accept(launch);
143        if (!((List<?>) launch.get("configurations")).isEmpty()) {
144            ObjectMapper mapper = new ObjectMapper();
145            String json = mapper.writerWithDefaultPrettyPrinter()
146                .writeValueAsString(launch);
147            Files.writeString(file, json);
148        }
149    }
150
151    /**
152     * Allow the user to adapt the launch data structure before writing.
153     * The version and an empty list for "configurations" has already be
154     * added.
155     *
156     * @param adaptor the adaptor
157     * @return the vscode configurator
158     */
159    public VscodeConfigurator
160            adaptLaunch(Consumer<Map<String, Object>> adaptor) {
161        launchAdaptor = adaptor;
162        return this;
163    }
164
165    private void generateTasks(Path file) throws IOException {
166        @SuppressWarnings("PMD.UseConcurrentHashMap")
167        Map<String, Object> tasks = new HashMap<>();
168        tasks.put("version", "2.0.0");
169        tasks.put("tasks", new ArrayList<>());
170        tasksAdaptor.accept(tasks);
171        if (!((List<?>) tasks.get("tasks")).isEmpty()) {
172            ObjectMapper mapper = new ObjectMapper();
173            String json
174                = mapper.writerWithDefaultPrettyPrinter()
175                    .writeValueAsString(tasks);
176            Files.writeString(file, json);
177        }
178    }
179
180    /**
181     * Allow the user to adapt the tasks data structure before writing.
182     *
183     * @param adaptor the adaptor
184     * @return the vscode configurator
185     */
186    public VscodeConfigurator
187            adaptTasks(Consumer<Map<String, Object>> adaptor) {
188        tasksAdaptor = adaptor;
189        return this;
190    }
191
192    /// Allow the user to add additional resources.
193    ///
194    /// @param adaptor the adaptor
195    /// @return the eclipse configurator
196    ///
197    public VscodeConfigurator adaptConfiguration(Runnable adaptor) {
198        configurationAdaptor = adaptor;
199        return this;
200    }
201}