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}