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