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