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