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