001/* 002 * JDrupes Builder 003 * Copyright (C) 2026 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.eclipse; 020 021import java.io.IOException; 022import java.nio.file.Files; 023import java.nio.file.Path; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Set; 029import javax.xml.parsers.ParserConfigurationException; 030import org.eclipse.aether.repository.RemoteRepository; 031import org.jdrupes.builder.api.BuildException; 032import org.jdrupes.builder.api.ConfigurationException; 033import static org.jdrupes.builder.api.Intent.Consume; 034import static org.jdrupes.builder.api.Intent.Expose; 035import static org.jdrupes.builder.api.Intent.Forward; 036import static org.jdrupes.builder.api.Intent.Reveal; 037import static org.jdrupes.builder.api.Intent.Supply; 038import org.jdrupes.builder.api.MergedTestProject; 039import org.jdrupes.builder.api.Project; 040import org.jdrupes.builder.api.Resource; 041import org.jdrupes.builder.api.ResourceRequest; 042import org.jdrupes.builder.api.ResourceType; 043import org.jdrupes.builder.api.Resources; 044import org.jdrupes.builder.java.ClassTree; 045import org.jdrupes.builder.java.JavaCompiler; 046import static org.jdrupes.builder.java.JavaTypes.CodeContributionType; 047import static org.jdrupes.builder.java.JavaTypes.LibraryJarFileType; 048import org.jdrupes.builder.mvnrepo.MvnRepoJarFile; 049import org.jdrupes.builder.mvnrepo.MvnRepoLibraryJarFile; 050import org.jdrupes.builder.mvnrepo.MvnRepoLookup; 051import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.MvnRepoResourceType; 052import org.w3c.dom.Document; 053import org.w3c.dom.Element; 054import org.w3c.dom.Node; 055import org.xml.sax.SAXException; 056 057/// The Class LaunchGenerator. 058/// 059public class LaunchGenerator { 060 061 private final EclipseConfigurator configurator; 062 063 /// Initializes a new launch generator. 064 /// 065 /// @param configurator the eclipse configurator 066 /// 067 public LaunchGenerator(EclipseConfigurator configurator) { 068 this.configurator = configurator; 069 } 070 071 @SuppressWarnings("PMD.ShortMethodName") 072 private <T extends Resource> ResourceRequest<T> 073 of(ResourceType<? extends T> type) { 074 return configurator.of(type); 075 } 076 077 private Project project() { 078 return configurator.project(); 079 } 080 081 /// Provide the launch configuration. 082 /// 083 /// @param <T> the generic type 084 /// @param requested the requested 085 /// @return the collection 086 /// 087 @SuppressWarnings({ "unchecked", "PMD.ConfusingTernary" }) 088 public <T extends Resource> Collection<T> provideLauncher( 089 ResourceRequest<T> requested) { 090 // Generate nothing for test projects. 091 if (project() instanceof MergedTestProject 092 || !(requested instanceof EclipseLauncherRequest)) { 093 return Collections.emptyList(); 094 } 095 096 var launcherName = requested.name().orElseThrow( 097 () -> new ConfigurationException().message( 098 "EclipseLauncherRequest must have a name")); 099 100 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 101 var launchFile 102 = project().directory().resolve(launcherName + ".launch"); 103 var request = (EclipseLauncherRequest) requested; 104 105 if (!Files.exists(launchFile)) { 106 configurator.generateXmlFile( 107 doc -> generateLaunchConfiguration(doc, request), 108 launcherName + ".launch"); 109 } else { 110 configurator.generateXmlFile(doc -> { 111 var existing = parseExisting(launchFile); 112 generateLaunchConfiguration(doc, request); 113 mergeSettings(existing, doc); 114 return existing; 115 }, launcherName + ".launch"); 116 } 117 118 // Create result 119 return (Collection<T>) List.of(EclipseLauncher.of( 120 project().directory().resolve(launcherName + ".launch"))); 121 } 122 123 private Document parseExisting(Path path) { 124 try { 125 return configurator.documentBuilderFactory().newDocumentBuilder() 126 .parse(path.toFile()); 127 } catch (ParserConfigurationException | SAXException | IOException e) { 128 throw new BuildException().from(configurator).cause(e); 129 } 130 } 131 132 /// Generates the launch configuration XML into the given document. 133 /// 134 /// @param doc an empty document as starting point 135 /// @param request the request 136 /// @return the generated document 137 /// 138 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 139 protected Document generateLaunchConfiguration(Document doc, 140 EclipseLauncherRequest request) { 141 var mainClass = request.mainClass(); 142 if (mainClass == null) { 143 throw new ConfigurationException().from(configurator) 144 .message("mainClass is required for %s", request.name().get()); 145 } 146 var launchConfig = (Element) doc.appendChild( 147 doc.createElement("launchConfiguration")); 148 launchConfig.setAttribute("type", 149 "org.eclipse.jdt.launching.localJavaApplication"); 150 151 // Debug core attributes 152 addStringAttribute(doc, launchConfig, 153 "org.eclipse.debug.core.source_locator_id", 154 "org.eclipse.jdt.launching" 155 + ".sourceLocator.JavaAdvancedSourceLookupDirector"); 156 addBooleanAttribute(doc, launchConfig, 157 "org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING", 158 "false"); 159 160 // JDT launching attributes 161 addBooleanAttribute(doc, launchConfig, 162 "org.eclipse.jdt.launching.ATTR_ATTR_USE_ARGFILE", "false"); 163 addBooleanAttribute(doc, launchConfig, 164 "org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE", "true"); 165 addBooleanAttribute(doc, launchConfig, 166 "org.eclipse.jdt.launching.ATTR_SHOW_CODEDETAILS" 167 + "_IN_EXCEPTION_MESSAGES", 168 "true"); 169 addBooleanAttribute(doc, launchConfig, 170 "org.eclipse.jdt.launching.ATTR_USE_CLASSPATH_ONLY_JAR", "false"); 171 172 // Classpath 173 generateLaunchClasspath(doc, launchConfig); 174 addBooleanAttribute(doc, launchConfig, 175 "org.eclipse.jdt.launching.DEFAULT_CLASSPATH", "false"); 176 177 // JRE 178 addJreToLaunch(doc, launchConfig); 179 180 // Main class 181 addStringAttribute(doc, launchConfig, 182 "org.eclipse.jdt.launching.MAIN_TYPE", mainClass); 183 184 // Module path 185 addModulePath(doc, launchConfig); 186 187 // Program arguments 188 if (request.arguments() != null && !request.arguments().isBlank()) { 189 addStringAttribute(doc, launchConfig, 190 "org.eclipse.jdt.launching.PROGRAM_ARGUMENTS", 191 request.arguments()); 192 } 193 194 // Project attribute 195 addStringAttribute(doc, launchConfig, 196 "org.eclipse.jdt.launching.PROJECT_ATTR", 197 configurator.eclipseAlias()); 198 199 // VM arguments 200 if (request.vmArguments() != null 201 && !request.vmArguments().isBlank()) { 202 addStringAttribute(doc, launchConfig, 203 "org.eclipse.jdt.launching.VM_ARGUMENTS", 204 request.vmArguments()); 205 } 206 207 // Working directory 208 addStringAttribute(doc, launchConfig, 209 "org.eclipse.jdt.launching.WORKING_DIRECTORY", 210 "${workspace_loc:" + configurator.eclipseAlias() + "}"); 211 212 // Return updated document 213 return doc; 214 } 215 216 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 217 private void generateLaunchClasspath(Document doc, Node parent) { 218 var classpathAttr = (Element) parent 219 .appendChild(doc.createElement("listAttribute")); 220 classpathAttr.setAttribute("key", 221 "org.eclipse.jdt.launching.CLASSPATH"); 222 223 // Add JRE container (type 4) 224 var javaCompiler = project().providers() 225 .select(Consume, Reveal, Supply) 226 .filter(p -> p instanceof JavaCompiler) 227 .map(JavaCompiler.class::cast).findFirst(); 228 var version = javaCompiler 229 .map(jc -> jc.optionArgument("-target", "--target", "--release")) 230 .map(opt -> opt.orElse("25")) 231 .orElse("25"); 232 addListEntry(doc, classpathAttr, 233 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" 234 + "<runtimeClasspathEntry " 235 + "containerPath=\"org.eclipse.jdt.launching.JRE_CONTAINER" 236 + "/org.eclipse.jdt.internal.debug.ui.launcher" 237 + ".StandardVMType/JavaSE-" + version + "\"" 238 + " path=\"5\" type=\"4\"/>"); 239 240 // Add project dependencies 241 addListEntry(doc, classpathAttr, "<?xml version=\"1.0\"" 242 + " encoding=\"UTF-8\" standalone=\"no\"?>\n" 243 + "<runtimeClasspathEntry projectName=\"" 244 + configurator.refName(project()) + "\" " 245 + "path=\"5\" type=\"1\"/>"); 246 project().providers().filter(p -> p instanceof Project) 247 .select(Consume, Reveal, Expose, Forward) 248 .toList().stream().map(Project.class::cast).forEach(p -> { 249 addListEntry(doc, classpathAttr, "<?xml version=\"1.0\"" 250 + " encoding=\"UTF-8\" standalone=\"no\"?>\n" 251 + "<runtimeClasspathEntry " 252 + "projectName=\"" + configurator.refName(p) + "\" " 253 + "path=\"5\" type=\"1\"/>"); 254 }); 255 256 // Add library jars 257 var cpes = Resources.with(LibraryJarFileType); 258 Set<RemoteRepository> repos = new HashSet<>(); 259 var repoRefs = Resources.with(MvnRepoResourceType); 260 project().resources(of(LibraryJarFileType) 261 .usingAll()).forEach(r -> { 262 if (r instanceof MvnRepoJarFile repoJar) { 263 repoRefs.add(repoJar.reference()); 264 repos.addAll(repoJar.repositories()); 265 } else { 266 cpes.add(r); 267 } 268 }); 269 // Jar files from maven repositories must be resolved before 270 // they can be added to the application to avoid duplicates. 271 var lookup = new MvnRepoLookup().name(configurator.name()) 272 .addRepositories(repos.toArray(RemoteRepository[]::new)); 273 lookup.resolve(repoRefs.stream()); 274 project().context() 275 .resources(lookup, of(CodeContributionType) 276 .using(Consume, Reveal, Supply, Expose)) 277 .forEach(cpe -> { 278 if (cpe instanceof MvnRepoLibraryJarFile jarFile) { 279 cpes.add(jarFile); 280 } 281 }); 282 283 cpes.stream().forEach(jf -> { 284 var jarPath = jf.path().toString(); 285 addListEntry(doc, classpathAttr, "<?xml version=\"1.0\"" 286 + " encoding=\"UTF-8\" standalone=\"no\"?>\n" 287 + "<runtimeClasspathEntry " 288 + "externalArchive=\"" + escapeXml(jarPath) + "\" " 289 + "path=\"5\" type=\"2\"/>"); 290 }); 291 } 292 293 private void addJreToLaunch(Document doc, Node parent) { 294 var javaCompiler = project().providers() 295 .select(Consume, Reveal, Supply) 296 .filter(p -> p instanceof JavaCompiler) 297 .map(JavaCompiler.class::cast).findFirst(); 298 var version = javaCompiler 299 .map(jc -> jc.optionArgument("-target", "--target", "--release")) 300 .map(opt -> opt.orElse("25")) 301 .orElse("25"); 302 addStringAttribute(doc, parent, 303 "org.eclipse.jdt.launching.JRE_CONTAINER", 304 "org.eclipse.jdt.launching.JRE_CONTAINER" 305 + "/org.eclipse.jdt.internal.debug.ui.launcher" 306 + ".StandardVMType/JavaSE-" + version); 307 } 308 309 private void addModulePath(Document doc, Node parent) { 310 var modulePath = (Element) parent 311 .appendChild(doc.createElement("listAttribute")); 312 modulePath.setAttribute("key", 313 "org.eclipse.jdt.launching.MODULEPATH"); 314 315 var isModular = project() 316 .resources(of(CodeContributionType).using(Supply)) 317 .filter(cc -> cc instanceof ClassTree).map(cc -> (ClassTree) cc) 318 .filter(cc -> cc.isModular()).findAny().isPresent(); 319 320 if (isModular) { 321 // Check for modular project dependencies 322 project().providers().filter(p -> p instanceof Project) 323 .select(Consume, Reveal, Expose, Forward) 324 .toList().stream().map(Project.class::cast).forEach(p -> { 325 if (p 326 .resources( 327 of(CodeContributionType).using(Supply)) 328 .filter(cc -> cc instanceof ClassTree) 329 .map(cc -> (ClassTree) cc) 330 .filter(cc -> cc.isModular()).findAny().isPresent()) { 331 addListEntry(doc, modulePath, 332 "<runtimeClasspathEntry " 333 + "path=\"5\" projectName=\"" 334 + configurator.refName(p) 335 + "\" type=\"1\"/>"); 336 } 337 }); 338 339 // Set module name 340 var moduleName = project() 341 .resources(of(CodeContributionType) 342 .using(Supply)) 343 .filter(cc -> cc instanceof ClassTree) 344 .map(cc -> (ClassTree) cc) 345 .filter(cc -> cc.isModular()).findAny() 346 .flatMap(ClassTree::moduleName).orElse(null); 347 if (moduleName != null) { 348 addStringAttribute(doc, parent, 349 "org.eclipse.jdt.launching.MODULE_NAME", moduleName); 350 } 351 } 352 } 353 354 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 355 private void addStringAttribute(Document doc, Node parent, 356 String key, String value) { 357 var attr = (Element) parent 358 .appendChild(doc.createElement("stringAttribute")); 359 attr.setAttribute("key", key); 360 attr.setAttribute("value", value); 361 } 362 363 private void addBooleanAttribute(Document doc, Node parent, 364 String key, String value) { 365 var attr = (Element) parent 366 .appendChild(doc.createElement("booleanAttribute")); 367 attr.setAttribute("key", key); 368 attr.setAttribute("value", value); 369 } 370 371 private void addListEntry(Document doc, Node parent, String value) { 372 var entry = (Element) parent 373 .appendChild(doc.createElement("listEntry")); 374 entry.setAttribute("value", value); 375 } 376 377 private String escapeXml(String str) { 378 return str.replace("&", "&") 379 .replace("<", "<") 380 .replace(">", ">") 381 .replace("\"", """) 382 .replace("'", "'"); 383 } 384 385 /// Updates the classpath and modulepath in the target document 386 /// from a freshly generated source document. 387 /// 388 /// @param target the existing document to update 389 /// @param source the freshly generated document 390 /// 391 protected void mergeSettings(Document target, Document source) { 392 replaceListAttribute(target, source, 393 "org.eclipse.jdt.launching.CLASSPATH"); 394 replaceListAttribute(target, source, 395 "org.eclipse.jdt.launching.MODULEPATH"); 396 replaceStringAttribute(target, source, 397 "org.eclipse.jdt.launching.PROGRAM_ARGUMENTS"); 398 replaceStringAttribute(target, source, 399 "org.eclipse.jdt.launching.VM_ARGUMENTS"); 400 } 401 402 private void replaceListAttribute(Document target, Document source, 403 String key) { 404 var targetNode = findListAttribute(target, key); 405 var sourceNode = findListAttribute(source, key); 406 if (sourceNode == null) { 407 return; 408 } 409 if (targetNode != null) { 410 var imported = target.importNode(sourceNode, true); 411 targetNode.getParentNode().replaceChild(imported, targetNode); 412 } else { 413 target.getDocumentElement().appendChild( 414 target.importNode(sourceNode, true)); 415 } 416 } 417 418 private void replaceStringAttribute(Document target, Document source, 419 String key) { 420 var targetNode = findStringAttribute(target, key); 421 var sourceNode = findStringAttribute(source, key); 422 if (sourceNode == null) { 423 return; 424 } 425 if (targetNode != null) { 426 ((Element) targetNode).setAttribute("value", 427 ((Element) sourceNode).getAttribute("value")); 428 } else { 429 target.getDocumentElement().appendChild( 430 target.importNode(sourceNode, true)); 431 } 432 } 433 434 private Node findListAttribute(Document doc, String key) { 435 var root = doc.getDocumentElement(); 436 var children = root.getChildNodes(); 437 for (int i = 0; i < children.getLength(); i++) { 438 var child = children.item(i); 439 if (child instanceof Element 440 && "listAttribute".equals(child.getNodeName()) 441 && key.equals(((Element) child).getAttribute("key"))) { 442 return child; 443 } 444 } 445 return null; 446 } 447 448 private Node findStringAttribute(Document doc, String key) { 449 var root = doc.getDocumentElement(); 450 var children = root.getChildNodes(); 451 for (int i = 0; i < children.getLength(); i++) { 452 var child = children.item(i); 453 if (child instanceof Element 454 && "stringAttribute".equals(child.getNodeName()) 455 && key.equals(((Element) child).getAttribute("key"))) { 456 return child; 457 } 458 } 459 return null; 460 } 461 462}