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 org.jdrupes.builder.api.Intend; 043import org.jdrupes.builder.api.Project; 044import org.jdrupes.builder.api.Resource; 045import org.jdrupes.builder.api.ResourceRequest; 046import static 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.CompilationResources; 051import org.jdrupes.builder.java.JavaCompiler; 052import org.jdrupes.builder.java.JavaProject; 053import org.jdrupes.builder.java.JavaResourceCollector; 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 in the workspace with the same name. The configurator 078/// allows you to define an alias for the project name to avoid this 079/// problem. The alias is used as Eclipse project name in all generated 080/// files. 081/// 082public class EclipseConfigurator extends AbstractGenerator { 083 084 /// The Constant GENERATED_BY. 085 public static final String GENERATED_BY = "Generated by JDrupes Builder"; 086 private static DocumentBuilderFactory dbf 087 = DocumentBuilderFactory.newInstance(); 088 private Supplier<String> eclipseAlias = () -> project().name(); 089 private BiConsumer<Document, Node> classpathAdaptor = (_, _) -> { 090 }; 091 private Runnable configurationAdaptor = () -> { 092 }; 093 private Consumer<Properties> jdtCorePrefsAdaptor = _ -> { 094 }; 095 private Consumer<Properties> resourcesPrefsAdaptor = _ -> { 096 }; 097 private Consumer<Properties> runtimePrefsAdaptor = _ -> { 098 }; 099 private ProjectConfigurationAdaptor prjConfigAdaptor = (_, _, _) -> { 100 }; 101 102 /// Instantiates a new eclipse configurator. 103 /// 104 /// @param project the project 105 /// 106 public EclipseConfigurator(Project project) { 107 super(project); 108 } 109 110 /// Define the eclipse (alias) project name. 111 /// 112 /// @param eclipseAlias the eclipse alias 113 /// @return the eclipse configurator 114 /// 115 public EclipseConfigurator eclipseAlias(Supplier<String> eclipseAlias) { 116 this.eclipseAlias = eclipseAlias; 117 return this; 118 } 119 120 /// Define the eclipse (alias) project name. 121 /// 122 /// @param eclipseAlias the eclipse alias 123 /// @return the eclipse configurator 124 /// 125 public EclipseConfigurator eclipseAlias(String eclipseAlias) { 126 this.eclipseAlias = () -> eclipseAlias; 127 return this; 128 } 129 130 /// Returns the eclipse alias. 131 /// 132 /// @return the string 133 /// 134 public String eclipseAlias() { 135 return eclipseAlias.get(); 136 } 137 138 /// Provides an [EclipseConfiguration]. 139 /// 140 /// @param <T> the generic type 141 /// @param requested the requested 142 /// @return the stream 143 /// 144 @Override 145 protected <T extends Resource> Stream<T> 146 doProvide(ResourceRequest<T> requested) { 147 if (!requested.collects(new ResourceType<EclipseConfiguration>() {})) { 148 return Stream.empty(); 149 } 150 151 // Make sure that the directories exist. 152 project().directory().resolve(".settings").toFile().mkdirs(); 153 154 // generate .project 155 generateXmlFile(this::generateProjectConfiguration, ".project"); 156 157 // generate .classpath 158 if (project() instanceof JavaProject) { 159 generateXmlFile(this::generateClasspathConfiguration, ".classpath"); 160 } 161 162 // Generate preferences 163 generateResourcesPrefs(); 164 generateRuntimePrefs(); 165 if (project() instanceof JavaProject) { 166 generateJdtCorePrefs(); 167 } 168 169 // General overrides 170 configurationAdaptor.run(); 171 172 // Create result 173 @SuppressWarnings({ "unchecked", "PMD.UseDiamondOperator" }) 174 var result = (Stream<T>) Stream.of(project().newResource( 175 new ResourceType<EclipseConfiguration>() {}, 176 project().name(), eclipseAlias())); 177 return result; 178 } 179 180 private void generateXmlFile(Consumer<Document> generator, String name) { 181 try { 182 var doc = dbf.newDocumentBuilder().newDocument(); 183 generator.accept(doc); 184 var transformer = TransformerFactory.newInstance().newTransformer(); 185 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 186 transformer.setOutputProperty( 187 "{http://xml.apache.org/xslt}indent-amount", "4"); 188 try (var out = Files 189 .newBufferedWriter(project().directory().resolve(name))) { 190 transformer.transform(new DOMSource(doc), 191 new StreamResult(out)); 192 } 193 } catch (ParserConfigurationException | TransformerException 194 | TransformerFactoryConfigurationError | IOException e) { 195 throw new BuildException(e); 196 } 197 } 198 199 /// Generates the content of the `.project` file into the given document. 200 /// 201 /// @param doc the document 202 /// 203 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 204 protected void generateProjectConfiguration(Document doc) { 205 var prjDescr = doc.appendChild(doc.createElement("projectDescription")); 206 prjDescr.appendChild(doc.createElement("name")) 207 .appendChild(doc.createTextNode(eclipseAlias())); 208 prjDescr.appendChild(doc.createElement("comment")).appendChild( 209 doc.createTextNode(GENERATED_BY)); 210 prjDescr.appendChild(doc.createElement("projects")); 211 var buildSpec = prjDescr.appendChild(doc.createElement("buildSpec")); 212 var natures = prjDescr.appendChild(doc.createElement("natures")); 213 if (project() instanceof JavaProject) { 214 var cmd = buildSpec.appendChild(doc.createElement("buildCommand")); 215 cmd.appendChild(doc.createElement("name")).appendChild( 216 doc.createTextNode("org.eclipse.jdt.core.javabuilder")); 217 cmd.appendChild(doc.createElement("arguments")); 218 natures.appendChild(doc.createElement("nature")).appendChild( 219 doc.createTextNode("org.eclipse.jdt.core.javanature")); 220 } 221 222 // Allow derived class to adapt the project configuration 223 prjConfigAdaptor.accept(doc, buildSpec, natures); 224 } 225 226 /// Allow derived classes to post process the project configuration. 227 /// 228 @FunctionalInterface 229 public interface ProjectConfigurationAdaptor { 230 /// Execute the adaptor. 231 /// 232 /// @param doc the document 233 /// @param buildSpec shortcut to the `buildSpec` element 234 /// @param natures shortcut to the `natures` element 235 /// 236 void accept(Document doc, Node buildSpec, 237 Node natures); 238 } 239 240 /// Adapt project configuration. 241 /// 242 /// @param adaptor the adaptor 243 /// @return the eclipse configurator 244 /// 245 public EclipseConfigurator adaptProjectConfiguration( 246 ProjectConfigurationAdaptor adaptor) { 247 prjConfigAdaptor = adaptor; 248 return this; 249 } 250 251 /// Generates the content of the `.classpath` file into the given 252 /// document. 253 /// 254 /// @param doc the doc 255 /// 256 @SuppressWarnings({ "PMD.AvoidDuplicateLiterals" }) 257 protected void generateClasspathConfiguration(Document doc) { 258 var classpath = doc.appendChild(doc.createElement("classpath")); 259 project().providers(Intend.Supply) 260 .filter(p -> p instanceof JavaCompiler).map(p -> (JavaCompiler) p) 261 .findFirst().ifPresent(jc -> { 262 jc.sources().stream().map(FileTree::root) 263 .map(p -> project().relativize(p)).forEach(p -> { 264 var entry = (Element) classpath 265 .appendChild(doc.createElement("classpathentry")); 266 entry.setAttribute("kind", "src"); 267 entry.setAttribute("path", p.toString()); 268 }); 269 var entry = (Element) classpath 270 .appendChild(doc.createElement("classpathentry")); 271 entry.setAttribute("kind", "output"); 272 entry.setAttribute("path", 273 project().relativize(jc.destination()).toString()); 274 jc.optionArgument("-target", "--target", "--release") 275 .ifPresentOrElse(v -> addSpecificJre(doc, classpath, v), 276 () -> addInheritedJre(doc, classpath)); 277 }); 278 279 // Add resources 280 project().providers(Intend.Supply) 281 .filter(p -> p instanceof JavaResourceCollector) 282 .map(p -> (JavaResourceCollector) p) 283 .findFirst().ifPresent(rc -> { 284 rc.resources().stream().map(FileTree::root) 285 .filter(p -> p.toFile().canRead()) 286 .map(p -> project().relativize(p)).forEach(p -> { 287 var entry = (Element) classpath 288 .appendChild(doc.createElement("classpathentry")); 289 entry.setAttribute("kind", "src"); 290 entry.setAttribute("path", p.toString()); 291 }); 292 }); 293 294 // Add projects 295 final Set<ClasspathElement> addedByProject = new HashSet<>(); 296 collectContributing(project()).collect(Collectors.toSet()).stream() 297 .forEach(p -> { 298 var entry = (Element) classpath 299 .appendChild(doc.createElement("classpathentry")); 300 entry.setAttribute("kind", "src"); 301 var referenced = p.get(requestFor(EclipseConfiguration.class)) 302 .filter(c -> c.projectName().equals(p.name())).findFirst() 303 .map(EclipseConfiguration::eclipseAlias).orElse(p.name()); 304 entry.setAttribute("path", "/" + referenced); 305 var attributes 306 = entry.appendChild(doc.createElement("attributes")); 307 var attribute = (Element) attributes 308 .appendChild(doc.createElement("attribute")); 309 attribute.setAttribute("without_test_code", "true"); 310 addedByProject.addAll(p.from(Intend.Supply) 311 .get(requestFor(ClasspathElement.class)).toList()); 312 }); 313 314 // Add jars 315 project().provided(requestFor( 316 new ResourceType<CompilationResources<LibraryJarFile>>() {})) 317 .filter(jf -> !addedByProject.contains(jf)) 318 .forEach(jf -> { 319 var entry = (Element) classpath 320 .appendChild(doc.createElement("classpathentry")); 321 entry.setAttribute("kind", "lib"); 322 var jarPathName = jf.path().toString(); 323 entry.setAttribute("path", jarPathName); 324 325 // Educated guesses 326 var sourcesJar = new File( 327 jarPathName.replaceFirst("\\.jar$", "-sources.jar")); 328 if (sourcesJar.canRead()) { 329 entry.setAttribute("sourcepath", 330 sourcesJar.getAbsolutePath()); 331 } 332 var javadocJar = new File( 333 jarPathName.replaceFirst("\\.jar$", "-javadoc.jar")); 334 if (javadocJar.canRead()) { 335 var attr = (Element) entry 336 .appendChild(doc.createElement("attributes")) 337 .appendChild(doc.createElement("attribute")); 338 attr.setAttribute("name", "javadoc_location"); 339 attr.setAttribute("value", 340 "jar:file:" + javadocJar.getAbsolutePath() + "!/"); 341 } 342 }); 343 344 // Allow derived class to override 345 classpathAdaptor.accept(doc, classpath); 346 } 347 348 private Stream<Project> collectContributing(Project project) { 349 return project.providers(Intend.Consume, Intend.Forward, Intend.Expose) 350 .filter(p -> p instanceof Project).map(p -> (Project) p) 351 .map(p -> Stream.concat(Stream.of(p), collectContributing(p))) 352 .flatMap(s -> s); 353 } 354 355 private void addSpecificJre(Document doc, Node classpath, 356 String version) { 357 var entry = (Element) classpath 358 .appendChild(doc.createElement("classpathentry")); 359 entry.setAttribute("kind", "con"); 360 entry.setAttribute("path", 361 "org.eclipse.jdt.launching.JRE_CONTAINER" 362 + "/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType" 363 + "/JavaSE-" + version); 364 var attributes = entry.appendChild(doc.createElement("attributes")); 365 var attribute 366 = (Element) attributes.appendChild(doc.createElement("attribute")); 367 attribute.setAttribute("name", "module"); 368 attribute.setAttribute("value", "true"); 369 } 370 371 private void addInheritedJre(Document doc, Node classpath) { 372 var entry = (Element) classpath 373 .appendChild(doc.createElement("classpathentry")); 374 entry.setAttribute("kind", "con"); 375 entry.setAttribute("path", 376 "org.eclipse.jdt.launching.JRE_CONTAINER"); 377 var attributes = entry.appendChild(doc.createElement("attributes")); 378 var attribute 379 = (Element) attributes.appendChild(doc.createElement("attribute")); 380 attribute.setAttribute("name", "module"); 381 attribute.setAttribute("value", "true"); 382 } 383 384 /// Allow the user to post process the classpath configuration. 385 /// The node passed to the consumer is the `classpath` element. 386 /// 387 /// @param adaptor the adaptor 388 /// @return the eclipse configurator 389 /// 390 public EclipseConfigurator 391 adaptClasspathConfiguration(BiConsumer<Document, Node> adaptor) { 392 classpathAdaptor = adaptor; 393 return this; 394 } 395 396 /// Generate the properties for the 397 /// `.settings/org.eclipse.core.resources.prefs` file. 398 /// 399 @SuppressWarnings("PMD.PreserveStackTrace") 400 protected void generateResourcesPrefs() { 401 var props = new Properties(); 402 props.setProperty("eclipse.preferences.version", "1"); 403 props.setProperty("encoding/<project>", "UTF-8"); 404 resourcesPrefsAdaptor.accept(props); 405 try (var out = new FixCommentsFilter(Files.newBufferedWriter( 406 project().directory().resolve( 407 ".settings/org.eclipse.core.resources.prefs")), 408 GENERATED_BY)) { 409 props.store(out, ""); 410 } catch (IOException e) { 411 throw new BuildException( 412 "Cannot write eclipse settings: " + e.getMessage()); 413 } 414 } 415 416 /// Allow the user to adapt the properties for the 417 /// `.settings/org.eclipse.core.resources.prefs` file. 418 /// 419 /// @param adaptor the adaptor 420 /// @return the eclipse configurator 421 /// 422 public EclipseConfigurator 423 adaptResourcePrefs(Consumer<Properties> adaptor) { 424 resourcesPrefsAdaptor = adaptor; 425 return this; 426 } 427 428 /// Generate the properties for the 429 /// `.settings/org.eclipse.core.runtime.prefs` file. 430 /// 431 @SuppressWarnings("PMD.PreserveStackTrace") 432 protected void generateRuntimePrefs() { 433 var props = new Properties(); 434 props.setProperty("eclipse.preferences.version", "1"); 435 props.setProperty("line.separator", "\n"); 436 runtimePrefsAdaptor.accept(props); 437 try (var out = new FixCommentsFilter(Files.newBufferedWriter( 438 project().directory().resolve( 439 ".settings/org.eclipse.core.runtime.prefs")), 440 GENERATED_BY)) { 441 props.store(out, ""); 442 } catch (IOException e) { 443 throw new BuildException( 444 "Cannot write eclipse settings: " + e.getMessage()); 445 } 446 } 447 448 /// Allow the user to adapt the properties for the 449 /// `.settings/org.eclipse.core.runtime.prefs` file. 450 /// 451 /// @param adaptor the adaptor 452 /// @return the eclipse configurator 453 /// 454 public EclipseConfigurator adaptRuntimePrefs(Consumer<Properties> adaptor) { 455 runtimePrefsAdaptor = adaptor; 456 return this; 457 } 458 459 /// Generate the properties for the 460 /// `.settings/org.eclipse.jdt.core.prefs` file. 461 /// 462 @SuppressWarnings("PMD.PreserveStackTrace") 463 protected void generateJdtCorePrefs() { 464 var props = new Properties(); 465 props.setProperty("eclipse.preferences.version", "1"); 466 project().providers(Intend.Supply) 467 .filter(p -> p instanceof JavaCompiler).map(p -> (JavaCompiler) p) 468 .findFirst().ifPresent(jc -> { 469 jc.optionArgument("-target", "--target", "--release") 470 .ifPresent(v -> { 471 props.setProperty("org.eclipse.jdt.core.compiler" 472 + ".codegen.targetPlatform", v); 473 }); 474 jc.optionArgument("-source", "--source", "--release") 475 .ifPresent(v -> { 476 props.setProperty("org.eclipse.jdt.core.compiler" 477 + ".source", v); 478 props.setProperty("org.eclipse.jdt.core.compiler" 479 + ".compliance", v); 480 }); 481 }); 482 jdtCorePrefsAdaptor.accept(props); 483 try (var out = new FixCommentsFilter(Files.newBufferedWriter( 484 project().directory() 485 .resolve(".settings/org.eclipse.jdt.core.prefs")), 486 GENERATED_BY)) { 487 props.store(out, ""); 488 } catch (IOException e) { 489 throw new BuildException( 490 "Cannot write eclipse settings: " + e.getMessage()); 491 } 492 } 493 494 /// Allow the user to adapt the properties for the 495 /// `.settings/org.eclipse.jdt.core.prefs` file. 496 /// 497 /// @param adaptor the adaptor 498 /// @return the eclipse configurator 499 /// 500 public EclipseConfigurator adaptJdtCorePrefs(Consumer<Properties> adaptor) { 501 jdtCorePrefsAdaptor = adaptor; 502 return this; 503 } 504 505 /// Allow the user to add additional resources. 506 /// 507 /// @param adaptor the adaptor 508 /// @return the eclipse configurator 509 /// 510 public EclipseConfigurator adaptConfiguration(Runnable adaptor) { 511 configurationAdaptor = adaptor; 512 return this; 513 } 514 515}