001/* 002 * JDrupes Builder 003 * Copyright (C) 2025, 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.File; 022import java.io.IOException; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashSet; 028import java.util.List; 029import java.util.Optional; 030import java.util.Properties; 031import java.util.Set; 032import java.util.function.BiConsumer; 033import java.util.function.Consumer; 034import java.util.function.Function; 035import java.util.function.Supplier; 036import java.util.stream.Collectors; 037import javax.xml.parsers.DocumentBuilderFactory; 038import javax.xml.parsers.ParserConfigurationException; 039import javax.xml.transform.OutputKeys; 040import javax.xml.transform.TransformerException; 041import javax.xml.transform.TransformerFactory; 042import javax.xml.transform.TransformerFactoryConfigurationError; 043import javax.xml.transform.dom.DOMSource; 044import javax.xml.transform.stream.StreamResult; 045import org.jdrupes.builder.api.BuildException; 046import org.jdrupes.builder.api.FileTree; 047import static org.jdrupes.builder.api.Intent.*; 048import org.jdrupes.builder.api.MergedTestProject; 049import org.jdrupes.builder.api.Project; 050import org.jdrupes.builder.api.Resource; 051import org.jdrupes.builder.api.ResourceRequest; 052import org.jdrupes.builder.api.ResourceType; 053import org.jdrupes.builder.core.AbstractGenerator; 054import static org.jdrupes.builder.eclipse.EclipseTypes.*; 055import org.jdrupes.builder.java.ClassTree; 056import org.jdrupes.builder.java.CodeContribution; 057import org.jdrupes.builder.java.JavaCompiler; 058import org.jdrupes.builder.java.JavaProject; 059import static org.jdrupes.builder.java.JavaTypes.*; 060import org.jdrupes.builder.java.LibraryJarFile; 061import org.w3c.dom.Document; 062import org.w3c.dom.Element; 063import org.w3c.dom.Node; 064 065/// The [EclipseConfigurator] provides the resource [EclipseConfiguration] 066/// and [EclipseLauncher]. 067/// 068/// ## Eclipse configuration 069/// 070/// "The configuration" consists of the Eclipse configuration files 071/// for a given project. The configurator generates the following 072/// files as W3C DOM documents (for XML files) or as [Properties] 073/// for a given project: 074/// 075/// * `.project`, 076/// * `.classpath`, 077/// * `.settings/org.eclipse.core.resources.prefs`, 078/// * `.settings/org.eclipse.core.runtime.prefs` and 079/// * `.settings/org.eclipse.jdt.core.prefs`. 080/// 081/// Each generated data structure can be post processed by a corresponding 082/// `adapt` method before being written to disk. Additional resources can 083/// be generated by the method [#adaptConfiguration]. 084/// 085/// Eclipse provides project nesting, but the outer project does not 086/// define a namespace. This can lead to problems if you have multiple 087/// (sub)projects with the same name in the workspace. The configurator 088/// allows you to define an alias for the project name to avoid this 089/// problem. This alias is used as Eclipse project name in all generated 090/// files. 091/// 092/// If a project is a [MergedTestProject], the configurator merges the 093/// information from this test project into the configuration files of 094/// its parent project. Resources that the test project depends 095/// on will be added as "test only" class path resources and the folder 096/// with the sources for the java compiler will be added as "test sources". 097/// 098/// ### JPMS support 099/// 100/// Referenced projects are added to the module-path if they provide 101/// a `module-info.class`. Referenced libraries are added to the module-path 102/// if [CodeContribution#isModular()] returns `true`. 103/// 104/// Resources from [MergedTestProject]s are always added to the classpath. 105/// This ensures maximum visibility of the classes under test. Making the 106/// test project modular, i.e. adding a `module-info.java` is not supported 107/// by Eclipse. It effectively merges the sources from `src/` and `test/` 108/// and complains about a duplicate `module-info.java` if such a file 109/// exists in the `test` folder. 110/// 111/// ## Eclipse launcher 112/// 113/// The [EclipseConfigurator] also generates Eclipse launch configuration 114/// files (`.launch`) as [EclipseLauncher] resources. A launch configuration 115/// is an XML document of type 116/// `org.eclipse.jdt.launching.localJavaApplication` that contains 117/// everything needed to run a Java application from within Eclipse. 118/// 119/// A request is made via [EclipseLauncherRequest] and must specify a name 120/// (the base name of the file name) and a fully qualified main class. 121/// Program arguments and VM arguments are optional. 122/// 123/// The launch configuration includes a fully resolved classpath with the 124/// JRE container, the project itself, all dependent projects, and library 125/// JARs (including those resolved from Maven repositories). For modular 126/// projects, the module path is populated with modular project dependencies 127/// and the module name is set accordingly. 128/// 129/// The working directory is set to the Eclipse workspace location of the 130/// project. Launch configurations are not generated for [MergedTestProject]s. 131/// 132/// If the launch configuration already exists, the classpath, the module path 133/// the arguments and the VM arguments are updated, everything else is left 134/// unchanged. 135/// 136@SuppressWarnings({ "PMD.TooManyMethods", "PMD.GodClass" }) 137public class EclipseConfigurator extends AbstractGenerator { 138 139 /// The Constant GENERATED_BY. 140 public static final String GENERATED_BY = "Generated by JDrupes Builder"; 141 private static DocumentBuilderFactory dbf 142 = DocumentBuilderFactory.newInstance(); 143 private Path outputDirectory = Path.of("bin"); 144 private Path testOutputDirectory = Path.of("test-bin"); 145 private Supplier<String> eclipseAlias = () -> project().name(); 146 private BiConsumer<Document, Node> classpathAdaptor = (_, _) -> { 147 }; 148 private Runnable configurationAdaptor = () -> { 149 }; 150 private Consumer<Properties> jdtCorePrefsAdaptor = _ -> { 151 }; 152 private Consumer<Properties> resourcesPrefsAdaptor = _ -> { 153 }; 154 private Consumer<Properties> runtimePrefsAdaptor = _ -> { 155 }; 156 private ProjectConfigurationAdaptor prjConfigAdaptor = (_, _, _) -> { 157 }; 158 159 /// Instantiates a new eclipse configurator. 160 /// 161 /// @param project the project 162 /// 163 public EclipseConfigurator(Project project) { 164 super(project); 165 } 166 167 /// return the document builder factory. 168 /// 169 /// @return the document builder factory 170 /// 171 protected DocumentBuilderFactory documentBuilderFactory() { 172 return dbf; 173 } 174 175 /// Define the eclipse (alias) project name. 176 /// 177 /// @param eclipseAlias the eclipse alias 178 /// @return the eclipse configurator 179 /// 180 public EclipseConfigurator eclipseAlias(Supplier<String> eclipseAlias) { 181 this.eclipseAlias = eclipseAlias; 182 return this; 183 } 184 185 /// Define the eclipse (alias) project name. 186 /// 187 /// @param eclipseAlias the eclipse alias 188 /// @return the eclipse configurator 189 /// 190 public EclipseConfigurator eclipseAlias(String eclipseAlias) { 191 this.eclipseAlias = () -> eclipseAlias; 192 return this; 193 } 194 195 /// Returns the eclipse alias. 196 /// 197 /// @return the string 198 /// 199 public String eclipseAlias() { 200 return eclipseAlias.get(); 201 } 202 203 /// Sets the output directory (a.k.a. folder) for classes. Defaults 204 /// to "`bin`". If set to `null` the configurator makes an attempt 205 /// to derive the directory from the [JavaCompiler] in the same 206 /// project. 207 /// 208 /// @param outputDirectory the output directory 209 /// @return the eclipse configurator 210 /// 211 public EclipseConfigurator outputDirectory(Path outputDirectory) { 212 this.outputDirectory = outputDirectory; 213 return this; 214 } 215 216 /// Sets the output directory (a.k.a. folder) for test classes. Defaults 217 /// to "`test-bin`". If set to `null` the configurator makes an attempt 218 /// to derive the directory from the [JavaCompiler] in the test 219 /// project. 220 /// 221 /// @param outputDirectory the output directory 222 /// @return the eclipse configurator 223 /// 224 public EclipseConfigurator testOutputDirectory(Path outputDirectory) { 225 testOutputDirectory = outputDirectory; 226 return this; 227 } 228 229 /// Provides an [EclipseConfiguration] or [EclipseLauncher]. 230 /// 231 /// @param <T> the generic type 232 /// @param requested the requested 233 /// @return the stream 234 /// 235 @Override 236 protected <T extends Resource> Collection<T> 237 doProvide(ResourceRequest<T> requested) { 238 // Check if provided and evaluate for most special type 239 if (!requested.accepts(EclipseConfigurationType) 240 && !requested.accepts(EclipseLauncherType)) { 241 return Collections.emptyList(); 242 } 243 244 // Handle EclipseLauncher requests 245 if (requested.accepts(EclipseLauncherType)) { 246 return new LaunchGenerator(this).provideLauncher(requested); 247 } 248 249 if (!requested.isFor(EclipseConfigurationType)) { 250 @SuppressWarnings("unchecked") 251 var result = (Collection<T>) resources(of(EclipseConfigurationType)) 252 .toList(); 253 return result; 254 } 255 256 // Generate nothing for test projects. 257 if (project() instanceof MergedTestProject) { 258 return Collections.emptyList(); 259 } 260 261 // Make sure that the directories exist. 262 project().directory().resolve(".settings").toFile().mkdirs(); 263 264 // generate .project 265 generateXmlFile(this::generateProjectConfiguration, ".project"); 266 267 // generate .classpath 268 if (project() instanceof JavaProject) { 269 generateXmlFile(this::generateClasspathConfiguration, ".classpath"); 270 } 271 272 // Generate preferences 273 generateResourcesPrefs(); 274 generateRuntimePrefs(); 275 if (project() instanceof JavaProject) { 276 generateJdtCorePrefs(); 277 } 278 279 // General overrides 280 configurationAdaptor.run(); 281 282 // Create result 283 @SuppressWarnings({ "unchecked" }) 284 var result = (Collection<T>) List.of( 285 EclipseConfiguration.of(project(), eclipseAlias())); 286 return result; 287 } 288 289 /// Generate an XML file from the supplied document. 290 /// 291 /// @param generator the generator 292 /// @param name the name 293 /// 294 protected void generateXmlFile(Function<Document, Document> generator, 295 String name) { 296 try { 297 var doc = dbf.newDocumentBuilder().newDocument(); 298 doc = generator.apply(doc); 299 var transformer = TransformerFactory.newInstance().newTransformer(); 300 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 301 transformer.setOutputProperty( 302 "{http://xml.apache.org/xslt}indent-amount", "4"); 303 try (var out = Files 304 .newBufferedWriter(project().directory().resolve(name))) { 305 transformer.transform(new DOMSource(doc), 306 new StreamResult(out)); 307 } 308 } catch (ParserConfigurationException | TransformerException 309 | TransformerFactoryConfigurationError | IOException e) { 310 throw new BuildException().from(this).cause(e); 311 } 312 } 313 314 /// Generates the content of the `.project` file into the given document. 315 /// 316 /// @param doc an empty document as starting point 317 /// @return the generated document 318 /// 319 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 320 protected Document generateProjectConfiguration(Document doc) { 321 var prjDescr = doc.appendChild(doc.createElement("projectDescription")); 322 prjDescr.appendChild(doc.createElement("name")) 323 .appendChild(doc.createTextNode(eclipseAlias())); 324 prjDescr.appendChild(doc.createElement("comment")).appendChild( 325 doc.createTextNode(GENERATED_BY)); 326 prjDescr.appendChild(doc.createElement("projects")); 327 var buildSpec = prjDescr.appendChild(doc.createElement("buildSpec")); 328 var natures = prjDescr.appendChild(doc.createElement("natures")); 329 if (project() instanceof JavaProject) { 330 var cmd = buildSpec.appendChild(doc.createElement("buildCommand")); 331 cmd.appendChild(doc.createElement("name")).appendChild( 332 doc.createTextNode("org.eclipse.jdt.core.javabuilder")); 333 cmd.appendChild(doc.createElement("arguments")); 334 natures.appendChild(doc.createElement("nature")).appendChild( 335 doc.createTextNode("org.eclipse.jdt.core.javanature")); 336 } 337 338 // Allow derived class to adapt the project configuration 339 prjConfigAdaptor.accept(doc, buildSpec, natures); 340 341 // Return updated document 342 return doc; 343 } 344 345 /// Allow derived classes to post process the project configuration. 346 /// 347 @FunctionalInterface 348 public interface ProjectConfigurationAdaptor { 349 /// Execute the adaptor. 350 /// 351 /// @param doc the document 352 /// @param buildSpec shortcut to the `buildSpec` element 353 /// @param natures shortcut to the `natures` element 354 /// 355 void accept(Document doc, Node buildSpec, 356 Node natures); 357 } 358 359 /// Adapt project configuration. 360 /// 361 /// @param adaptor the adaptor 362 /// @return the eclipse configurator 363 /// 364 public EclipseConfigurator adaptProjectConfiguration( 365 ProjectConfigurationAdaptor adaptor) { 366 prjConfigAdaptor = adaptor; 367 return this; 368 } 369 370 /// Generates the content of the `.classpath` file into the given 371 /// document. 372 /// 373 /// @param doc an empty document as starting point 374 /// @return the generated document 375 /// 376 protected Document generateClasspathConfiguration(Document doc) { 377 var isModular = project() 378 .resources(of(CodeContributionType).using(Supply)) 379 .filter(cc -> cc instanceof ClassTree).map(cc -> (ClassTree) cc) 380 .filter(cc -> cc.isModular()).findAny().isPresent(); 381 var classpath = doc.appendChild(doc.createElement("classpath")); 382 addCompilationResources(doc, classpath, isModular, project()); 383 addJavaResources(doc, classpath, project()); 384 385 // Add projects 386 final Set<CodeContribution> providedByProjects = new HashSet<>(); 387 final Set<Project> exposed = project().providers().select(Expose) 388 .filter(p -> p instanceof Project).map(Project.class::cast) 389 .collect(Collectors.toSet()); 390 project().providers().filter(p -> p instanceof Project) 391 .select(Consume, Reveal, Expose, Forward) 392 .toList().stream().map(Project.class::cast).forEach(p -> { 393 addProject( 394 doc, classpath, providedByProjects, exposed, isModular, p); 395 }); 396 397 // Add jars 398 final Set<CodeContribution> exposedByProject = new HashSet<>(); 399 exposedByProject.addAll(project() 400 .resources(of(CodeContributionType).using(Expose)) 401 .toList()); 402 project().resources(of(LibraryJarFileType) 403 .using(Consume, Reveal, Expose)) 404 .filter(jf -> !providedByProjects.contains(jf)) 405 .collect(Collectors.toSet()).stream().forEach(jf -> { 406 addJarFileEntry(doc, classpath, isModular, jf, 407 exposedByProject.contains(jf), false); 408 }); 409 410 // Allow derived class to override 411 classpathAdaptor.accept(doc, classpath); 412 413 // Return updated document 414 return doc; 415 } 416 417 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 418 private void addProject(Document doc, Node classpath, 419 final Set<CodeContribution> providedByProject, 420 final Set<Project> exposed, boolean useModules, Project project) { 421 if (project instanceof MergedTestProject) { 422 if (project.parentProject().get().equals(project())) { 423 // Test projects contribute their resources to the 424 // parent. They are always put on the classpath for 425 // maximum visibility. 426 addCompilationResources(doc, classpath, false, project); 427 addJavaResources(doc, classpath, project); 428 } 429 return; 430 } 431 var entry = (Element) classpath 432 .appendChild(doc.createElement("classpathentry")); 433 entry.setAttribute("kind", "src"); 434 entry.setAttribute("path", "/" + refName(project)); 435 if (exposed.contains(project)) { 436 entry.setAttribute("exported", "true"); 437 } 438 var attributes 439 = entry.appendChild(doc.createElement("attributes")); 440 var attribute = (Element) attributes 441 .appendChild(doc.createElement("attribute")); 442 attribute.setAttribute("without_test_code", "true"); 443 444 // Check if the project is a modular project 445 if (useModules 446 && project.resources(of(CodeContributionType).using(Supply)) 447 .filter(cc -> cc instanceof ClassTree).map(cc -> (ClassTree) cc) 448 .filter(cc -> cc.isModular()).findAny().isPresent()) { 449 attribute = (Element) attributes 450 .appendChild(doc.createElement("attribute")); 451 attribute.setAttribute("name", "module"); 452 attribute.setAttribute("value", "true"); 453 } 454 455 // Remember what we already have due to the dependency on the 456 // project to avoid duplicate entries for libraries 457 providedByProject.addAll(project.resources(of(CodeContributionType) 458 .using(Supply, Expose)).toList()); 459 } 460 461 /// Returns another project's Eclipse name for referencing it 462 /// in the configuration. 463 /// 464 /// @param project the project 465 /// @return the string 466 /// 467 protected String refName(Project project) { 468 return project.resources( 469 of(new ResourceType<EclipseConfiguration>() {}) 470 .using(Supply, Expose)) 471 .filter(c -> c.projectName().equals(project.name())).findFirst() 472 .map(EclipseConfiguration::eclipseAlias).orElse(project.name()); 473 } 474 475 private void addJarFileEntry(Document doc, Node classpath, 476 boolean useModules, LibraryJarFile jarFile, boolean exported, 477 boolean test) { 478 var entry = (Element) classpath 479 .appendChild(doc.createElement("classpathentry")); 480 entry.setAttribute("kind", "lib"); 481 var jarPathName = jarFile.path().toString(); 482 entry.setAttribute("path", jarPathName); 483 if (exported) { 484 entry.setAttribute("exported", "true"); 485 } 486 487 // Add attributes 488 var attributes 489 = (Element) entry.appendChild(doc.createElement("attributes")); 490 if (useModules && jarFile.isModular()) { 491 var attr = (Element) attributes 492 .appendChild(doc.createElement("attribute")); 493 attr.setAttribute("name", "module"); 494 attr.setAttribute("value", "true"); 495 } 496 if (test) { 497 var attr = (Element) attributes 498 .appendChild(doc.createElement("attribute")); 499 attr.setAttribute("name", "test"); 500 attr.setAttribute("value", "true"); 501 } 502 503 // Educated guesses 504 var sourcesJar 505 = new File(jarPathName.replaceFirst("\\.jar$", "-sources.jar")); 506 if (sourcesJar.canRead()) { 507 entry.setAttribute("sourcepath", sourcesJar.getAbsolutePath()); 508 } 509 var javadocJar = new File( 510 jarPathName.replaceFirst("\\.jar$", "-javadoc.jar")); 511 if (javadocJar.canRead()) { 512 var attr = (Element) attributes 513 .appendChild(doc.createElement("attribute")); 514 attr.setAttribute("name", "javadoc_location"); 515 attr.setAttribute("value", 516 "jar:file:" + javadocJar.getAbsolutePath() + "!/"); 517 } 518 } 519 520 private void addJavaResources(Document doc, Node classpath, 521 Project project) { 522 // TODO Generalize. Currently we assume a Java compiler exists 523 // and use it to obtain the output directory for all generators 524 var javaCompiler = project.providers().select(Consume, Reveal, Supply) 525 .filter(p -> p instanceof JavaCompiler) 526 .map(JavaCompiler.class::cast).findFirst(); 527 var outputDirectory = Optional.ofNullable( 528 (project instanceof MergedTestProject) ? testOutputDirectory 529 : this.outputDirectory) 530 .or(() -> javaCompiler 531 .map(jc -> project.relativize(jc.destination()))); 532 533 // Add resources 534 project.providers().without(Project.class).resources( 535 of(JavaResourceTreeType).using(Consume, Reveal, Supply)) 536 .map(FileTree::root).filter(p -> p.toFile().canRead()) 537 .collect(Collectors.toSet()).stream() 538 .map(project::relativize).forEach(p -> { 539 var entry = (Element) classpath 540 .appendChild(doc.createElement("classpathentry")); 541 entry.appendChild(doc.createComment("From " + project)); 542 entry.setAttribute("kind", "src"); 543 entry.setAttribute("path", p.toString()); 544 if (project instanceof MergedTestProject) { 545 outputDirectory.ifPresent(o -> { 546 entry.setAttribute("output", o.toString()); 547 }); 548 var attr = (Element) entry 549 .appendChild(doc.createElement("attributes")) 550 .appendChild(doc.createElement("attribute")); 551 attr.setAttribute("name", "test"); 552 attr.setAttribute("value", "true"); 553 } 554 }); 555 } 556 557 private void addCompilationResources(Document doc, Node classpath, 558 boolean useModules, Project project) { 559 // TODO Generalize. Currently we assume a Java compiler exists 560 // and use it to obtain the output directory for all generators 561 var javaCompiler = project.providers().select(Consume, Reveal, Supply) 562 .filter(p -> p instanceof JavaCompiler) 563 .map(JavaCompiler.class::cast).findFirst(); 564 var outputDirectory = Optional.ofNullable( 565 (project instanceof MergedTestProject) ? testOutputDirectory 566 : this.outputDirectory) 567 .or(() -> javaCompiler 568 .map(jc -> project.relativize(jc.destination()))); 569 570 // Add source trees 571 project.providers().without(Project.class).resources( 572 of(JavaSourceTreeType).using(Consume, Reveal, Supply)) 573 .map(FileTree::root).filter(p -> p.toFile().canRead()) 574 .map(project::relativize).forEach(p -> { 575 var entry = (Element) classpath 576 .appendChild(doc.createElement("classpathentry")); 577 entry.appendChild(doc.createComment("From " + project)); 578 entry.setAttribute("kind", "src"); 579 entry.setAttribute("path", p.toString()); 580 if (project instanceof MergedTestProject) { 581 outputDirectory.ifPresent(o -> { 582 entry.setAttribute("output", o.toString()); 583 }); 584 var attr = (Element) entry 585 .appendChild(doc.createElement("attributes")) 586 .appendChild(doc.createElement("attribute")); 587 attr.setAttribute("name", "test"); 588 attr.setAttribute("value", "true"); 589 } 590 }); 591 592 // For merged test project also add compile path resources 593 if (project instanceof MergedTestProject) { 594 project.providers().without(project.parentProject().get()).filter( 595 p -> javaCompiler.map(jc -> !p.equals(jc)).orElse(true)) 596 .resources( 597 of(LibraryJarFileType).using(Consume, Reveal, Expose)) 598 .forEach(jf -> { 599 addJarFileEntry(doc, classpath, useModules, jf, false, 600 true); 601 }); 602 return; 603 } 604 605 // For "normal projects" configure default output directory 606 outputDirectory.ifPresent(o -> { 607 var entry = (Element) classpath 608 .appendChild(doc.createElement("classpathentry")); 609 entry.setAttribute("kind", "output"); 610 entry.setAttribute("path", o.toString()); 611 }); 612 613 // Finally Add JRE 614 javaCompiler.ifPresent(jc -> { 615 jc.optionArgument("-target", "--target", "--release") 616 .ifPresentOrElse(v -> addSpecificJre(doc, classpath, v), 617 () -> addInheritedJre(doc, classpath)); 618 }); 619 } 620 621 private void addSpecificJre(Document doc, Node classpath, 622 String version) { 623 var entry = (Element) classpath 624 .appendChild(doc.createElement("classpathentry")); 625 entry.setAttribute("kind", "con"); 626 entry.setAttribute("path", 627 "org.eclipse.jdt.launching.JRE_CONTAINER" 628 + "/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType" 629 + "/JavaSE-" + version); 630 var attributes = entry.appendChild(doc.createElement("attributes")); 631 var attribute 632 = (Element) attributes.appendChild(doc.createElement("attribute")); 633 attribute.setAttribute("name", "module"); 634 attribute.setAttribute("value", "true"); 635 } 636 637 private void addInheritedJre(Document doc, Node classpath) { 638 var entry = (Element) classpath 639 .appendChild(doc.createElement("classpathentry")); 640 entry.setAttribute("kind", "con"); 641 entry.setAttribute("path", 642 "org.eclipse.jdt.launching.JRE_CONTAINER"); 643 var attributes = entry.appendChild(doc.createElement("attributes")); 644 var attribute 645 = (Element) attributes.appendChild(doc.createElement("attribute")); 646 attribute.setAttribute("name", "module"); 647 attribute.setAttribute("value", "true"); 648 } 649 650 /// Allow the user to post process the classpath configuration. 651 /// The node passed to the consumer is the `classpath` element. 652 /// 653 /// @param adaptor the adaptor 654 /// @return the eclipse configurator 655 /// 656 public EclipseConfigurator 657 adaptClasspathConfiguration(BiConsumer<Document, Node> adaptor) { 658 classpathAdaptor = adaptor; 659 return this; 660 } 661 662 /// Generate the properties for the 663 /// `.settings/org.eclipse.core.resources.prefs` file. 664 /// 665 protected void generateResourcesPrefs() { 666 var props = new Properties(); 667 props.setProperty("eclipse.preferences.version", "1"); 668 props.setProperty("encoding/<project>", "UTF-8"); 669 resourcesPrefsAdaptor.accept(props); 670 try (var out = new FixCommentsFilter(Files.newBufferedWriter( 671 project().directory().resolve( 672 ".settings/org.eclipse.core.resources.prefs")), 673 GENERATED_BY)) { 674 props.store(out, ""); 675 } catch (IOException e) { 676 throw new BuildException().from(this).cause(e); 677 } 678 } 679 680 /// Allow the user to adapt the properties for the 681 /// `.settings/org.eclipse.core.resources.prefs` file. 682 /// 683 /// @param adaptor the adaptor 684 /// @return the eclipse configurator 685 /// 686 public EclipseConfigurator 687 adaptResourcePrefs(Consumer<Properties> adaptor) { 688 resourcesPrefsAdaptor = adaptor; 689 return this; 690 } 691 692 /// Generate the properties for the 693 /// `.settings/org.eclipse.core.runtime.prefs` file. 694 /// 695 protected void generateRuntimePrefs() { 696 var props = new Properties(); 697 props.setProperty("eclipse.preferences.version", "1"); 698 props.setProperty("line.separator", "\n"); 699 runtimePrefsAdaptor.accept(props); 700 try (var out = new FixCommentsFilter(Files.newBufferedWriter( 701 project().directory().resolve( 702 ".settings/org.eclipse.core.runtime.prefs")), 703 GENERATED_BY)) { 704 props.store(out, ""); 705 } catch (IOException e) { 706 throw new BuildException().from(this).cause(e); 707 } 708 } 709 710 /// Allow the user to adapt the properties for the 711 /// `.settings/org.eclipse.core.runtime.prefs` file. 712 /// 713 /// @param adaptor the adaptor 714 /// @return the eclipse configurator 715 /// 716 public EclipseConfigurator adaptRuntimePrefs(Consumer<Properties> adaptor) { 717 runtimePrefsAdaptor = adaptor; 718 return this; 719 } 720 721 /// Generate the properties for the 722 /// `.settings/org.eclipse.jdt.core.prefs` file. 723 /// 724 protected void generateJdtCorePrefs() { 725 var props = new Properties(); 726 props.setProperty("eclipse.preferences.version", "1"); 727 project().providers().select(Supply) 728 .filter(p -> p instanceof JavaCompiler).map(p -> (JavaCompiler) p) 729 .findFirst().ifPresent(jc -> { 730 jc.optionArgument("-target", "--target", "--release") 731 .ifPresent(v -> { 732 props.setProperty("org.eclipse.jdt.core.compiler" 733 + ".codegen.targetPlatform", v); 734 }); 735 jc.optionArgument("-source", "--source", "--release") 736 .ifPresent(v -> { 737 props.setProperty("org.eclipse.jdt.core.compiler" 738 + ".source", v); 739 props.setProperty("org.eclipse.jdt.core.compiler" 740 + ".compliance", v); 741 }); 742 }); 743 jdtCorePrefsAdaptor.accept(props); 744 try (var out = new FixCommentsFilter(Files.newBufferedWriter( 745 project().directory() 746 .resolve(".settings/org.eclipse.jdt.core.prefs")), 747 GENERATED_BY)) { 748 props.store(out, ""); 749 } catch (IOException e) { 750 throw new BuildException().from(this).cause(e); 751 } 752 } 753 754 /// Allow the user to adapt the properties for the 755 /// `.settings/org.eclipse.jdt.core.prefs` file. 756 /// 757 /// @param adaptor the adaptor 758 /// @return the eclipse configurator 759 /// 760 public EclipseConfigurator adaptJdtCorePrefs(Consumer<Properties> adaptor) { 761 jdtCorePrefsAdaptor = adaptor; 762 return this; 763 } 764 765 /// Allow the user to add additional resources. 766 /// 767 /// @param adaptor the adaptor 768 /// @return the eclipse configurator 769 /// 770 public EclipseConfigurator adaptConfiguration(Runnable adaptor) { 771 configurationAdaptor = adaptor; 772 return this; 773 } 774 775}