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