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.Intend.*; 033import org.jdrupes.builder.api.Project; 034import org.jdrupes.builder.api.Resource; 035import org.jdrupes.builder.api.ResourceRequest; 036import static org.jdrupes.builder.api.ResourceRequest.*; 037import static org.jdrupes.builder.api.ResourceType.resourceType; 038import org.jdrupes.builder.core.AbstractGenerator; 039import org.jdrupes.builder.java.JarFile; 040import org.jdrupes.builder.java.JavaCompiler; 041import static org.jdrupes.builder.java.JavaTypes.*; 042 043/// The [VscodeConfigurator] provides the resource [VscodeConfiguration]. 044/// The configuration consists of the configuration files: 045/// * .vscode/settings.json 046/// * .vscode/launch.json 047/// * .vscode/tasks.json 048/// 049/// Each generated data structure can be post processed by a corresponding 050/// `adapt` method before being written to disk. 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> Stream<T> 099 doProvide(ResourceRequest<T> requested) { 100 if (!requested.collects(resourceType(VscodeConfiguration.class))) { 101 return Stream.empty(); 102 } 103 Path vscodeDir = project().directory().resolve(".vscode"); 104 vscodeDir.toFile().mkdirs(); 105 try { 106 generateSettingsJson(vscodeDir.resolve("settings.json")); 107 generateLaunchJson(vscodeDir.resolve("launch.json")); 108 generateTasksJson(vscodeDir.resolve("tasks.json")); 109 } catch (IOException e) { 110 throw new BuildException(e); 111 } 112 113 // General overrides 114 configurationAdaptor.run(); 115 116 // Return a result 117 @SuppressWarnings({ "unchecked" }) 118 var result = (Stream<T>) Stream.of(project().newResource( 119 resourceType(VscodeConfiguration.class), project().directory())); 120 return result; 121 } 122 123 private void generateSettingsJson(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 // Set java compiler target 130 project().providers(Supply).filter(p -> p instanceof JavaCompiler) 131 .map(p -> (JavaCompiler) p) 132 .findFirst() 133 .flatMap( 134 jc -> jc.optionArgument("--release", "--target", "-target")) 135 .filter(jdkLocations::containsKey) 136 .ifPresent(version -> { 137 @SuppressWarnings("PMD.UseConcurrentHashMap") 138 Map<String, Object> runtime = new HashMap<>(); 139 runtime.put("name", "JavaSE-" + version); 140 runtime.put("path", jdkLocations.get(version).toString()); 141 runtime.put("default", true); 142 settings.put("java.configuration.runtimes", List.of(runtime)); 143 }); 144 145 // Add output directories of contributing projects 146 var referenced = new java.util.ArrayList<String>(); 147 collectContributing(project()).collect(Collectors.toSet()).stream() 148 .forEach(proj -> { 149 referenced 150 .add(proj.buildDirectory().resolve("classes").toString()); 151 }); 152 153 // Add JARs to classpath 154 referenced.addAll(project().provided( 155 requestFor(CompilationClasspathType)) 156 .filter(p -> p instanceof JarFile) 157 .map(jf -> ((JarFile) jf).path().toString()) 158 .collect(Collectors.toList())); 159 if (!referenced.isEmpty()) { 160 settings.put("java.project.referencedLibraries", referenced); 161 } 162 163 // Allow user to adapt settings 164 settingsAdaptor.accept(settings); 165 ObjectMapper mapper = new ObjectMapper(); 166 String json = mapper.writerWithDefaultPrettyPrinter() 167 .writeValueAsString(settings); 168 Files.writeString(file, json); 169 } 170 171 private Stream<Project> collectContributing(Project project) { 172 return project.providers(Consume, Forward, Expose) 173 .filter(p -> p instanceof Project).map(p -> (Project) p) 174 .map(p -> Stream.concat(Stream.of(p), collectContributing(p))) 175 .flatMap(s -> s); 176 } 177 178 private void generateLaunchJson(Path file) throws IOException { 179 @SuppressWarnings("PMD.UseConcurrentHashMap") 180 Map<String, Object> launch = new HashMap<>(); 181 launch.put("version", "0.2.0"); 182 launch.put("configurations", new java.util.ArrayList<>()); 183 launchAdaptor.accept(launch); 184 ObjectMapper mapper = new ObjectMapper(); 185 String json = mapper.writerWithDefaultPrettyPrinter() 186 .writeValueAsString(launch); 187 Files.writeString(file, json); 188 } 189 190 /** 191 * Allow the user to adapt the launch data structure before writing. 192 * The version and an empty list for "configurations" has already be 193 * added. 194 * 195 * @param adaptor the adaptor 196 * @return the vscode configurator 197 */ 198 public VscodeConfigurator 199 adaptLaunch(Consumer<Map<String, Object>> adaptor) { 200 launchAdaptor = adaptor; 201 return this; 202 } 203 204 private void generateTasksJson(Path file) throws IOException { 205 @SuppressWarnings("PMD.UseConcurrentHashMap") 206 Map<String, Object> tasks = new HashMap<>(); 207 tasks.put("version", "2.0.0"); 208 tasks.put("tasks", new java.util.ArrayList<>()); 209 tasksAdaptor.accept(tasks); 210 ObjectMapper mapper = new ObjectMapper(); 211 String json 212 = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(tasks); 213 Files.writeString(file, json); 214 } 215 216 /** 217 * Allow the user to adapt the tasks data structure before writing. 218 * 219 * @param adaptor the adaptor 220 * @return the vscode configurator 221 */ 222 public VscodeConfigurator 223 adaptTasks(Consumer<Map<String, Object>> adaptor) { 224 tasksAdaptor = adaptor; 225 return this; 226 } 227 228 /// Allow the user to add additional resources. 229 /// 230 /// @param adaptor the adaptor 231 /// @return the eclipse configurator 232 /// 233 public VscodeConfigurator adaptConfiguration(Runnable adaptor) { 234 configurationAdaptor = adaptor; 235 return this; 236 } 237}