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}