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.Intent.*; 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.accepts(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().from(this).cause(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().select(Supply) 130 .filter(p -> p instanceof JavaCompiler) 131 .map(p -> (JavaCompiler) p).findFirst().flatMap( 132 jc -> jc.optionArgument("--release", "--target", "-target")) 133 .filter(jdkLocations::containsKey) 134 .ifPresent(version -> { 135 @SuppressWarnings("PMD.UseConcurrentHashMap") 136 Map<String, Object> runtime = new HashMap<>(); 137 runtime.put("name", "JavaSE-" + version); 138 runtime.put("path", jdkLocations.get(version).toString()); 139 runtime.put("default", true); 140 settings.put("java.configuration.runtimes", List.of(runtime)); 141 }); 142 143 // Add output directories of contributing projects 144 var referenced = new java.util.ArrayList<String>(); 145 collectContributing(project()).collect(Collectors.toSet()).stream() 146 .forEach(proj -> { 147 referenced 148 .add(proj.buildDirectory().resolve("classes").toString()); 149 }); 150 151 // Add JARs to classpath 152 referenced.addAll(project().resources(of(ClasspathElementType) 153 .using(Consume, Reveal, Expose)).filter(p -> p instanceof JarFile) 154 .map(jf -> ((JarFile) jf).path().toString()) 155 .collect(Collectors.toList())); 156 if (!referenced.isEmpty()) { 157 settings.put("java.project.referencedLibraries", referenced); 158 } 159 160 // Allow user to adapt settings 161 settingsAdaptor.accept(settings); 162 ObjectMapper mapper = new ObjectMapper(); 163 String json = mapper.writerWithDefaultPrettyPrinter() 164 .writeValueAsString(settings); 165 Files.writeString(file, json); 166 } 167 168 private Stream<Project> collectContributing(Project project) { 169 return project.providers().select(Consume, Reveal, Forward, Expose) 170 .filter(p -> p instanceof Project).map(p -> (Project) p) 171 .map(p -> Stream.concat(Stream.of(p), collectContributing(p))) 172 .flatMap(s -> s); 173 } 174 175 private void generateLaunchJson(Path file) throws IOException { 176 @SuppressWarnings("PMD.UseConcurrentHashMap") 177 Map<String, Object> launch = new HashMap<>(); 178 launch.put("version", "0.2.0"); 179 launch.put("configurations", new java.util.ArrayList<>()); 180 launchAdaptor.accept(launch); 181 ObjectMapper mapper = new ObjectMapper(); 182 String json = mapper.writerWithDefaultPrettyPrinter() 183 .writeValueAsString(launch); 184 Files.writeString(file, json); 185 } 186 187 /** 188 * Allow the user to adapt the launch data structure before writing. 189 * The version and an empty list for "configurations" has already be 190 * added. 191 * 192 * @param adaptor the adaptor 193 * @return the vscode configurator 194 */ 195 public VscodeConfigurator 196 adaptLaunch(Consumer<Map<String, Object>> adaptor) { 197 launchAdaptor = adaptor; 198 return this; 199 } 200 201 private void generateTasksJson(Path file) throws IOException { 202 @SuppressWarnings("PMD.UseConcurrentHashMap") 203 Map<String, Object> tasks = new HashMap<>(); 204 tasks.put("version", "2.0.0"); 205 tasks.put("tasks", new java.util.ArrayList<>()); 206 tasksAdaptor.accept(tasks); 207 ObjectMapper mapper = new ObjectMapper(); 208 String json 209 = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(tasks); 210 Files.writeString(file, json); 211 } 212 213 /** 214 * Allow the user to adapt the tasks data structure before writing. 215 * 216 * @param adaptor the adaptor 217 * @return the vscode configurator 218 */ 219 public VscodeConfigurator 220 adaptTasks(Consumer<Map<String, Object>> adaptor) { 221 tasksAdaptor = adaptor; 222 return this; 223 } 224 225 /// Allow the user to add additional resources. 226 /// 227 /// @param adaptor the adaptor 228 /// @return the eclipse configurator 229 /// 230 public VscodeConfigurator adaptConfiguration(Runnable adaptor) { 231 configurationAdaptor = adaptor; 232 return this; 233 } 234}