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