001/*
002 * JDrupes Builder
003 * Copyright (C) 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.bnd;
020
021import aQute.bnd.osgi.Analyzer;
022import aQute.bnd.version.Version;
023import com.google.common.flogger.FluentLogger;
024import static com.google.common.flogger.LazyArgs.lazy;
025import io.vavr.control.Try;
026import java.io.File;
027import java.nio.file.Path;
028import java.util.Map;
029import java.util.Optional;
030import java.util.jar.Attributes;
031import java.util.jar.Manifest;
032import java.util.stream.Collectors;
033import java.util.stream.Stream;
034import org.jdrupes.builder.api.BuildException;
035import org.jdrupes.builder.api.ConfigurationException;
036import org.jdrupes.builder.api.Generator;
037import static org.jdrupes.builder.api.Intent.Consume;
038import static org.jdrupes.builder.api.Intent.Expose;
039import static org.jdrupes.builder.api.Intent.Reveal;
040import static org.jdrupes.builder.api.Intent.Supply;
041import org.jdrupes.builder.api.Project;
042import org.jdrupes.builder.api.Resource;
043import org.jdrupes.builder.api.ResourceProvider;
044import org.jdrupes.builder.api.ResourceRequest;
045import org.jdrupes.builder.api.ResourceType;
046import org.jdrupes.builder.api.Resources;
047import org.jdrupes.builder.api.RootProject;
048import org.jdrupes.builder.java.ClassTree;
049import org.jdrupes.builder.java.JavaCompiler;
050import static org.jdrupes.builder.java.JavaTypes.*;
051import org.jdrupes.builder.java.LibraryJarFile;
052import org.jdrupes.builder.java.ManifestAttributes;
053
054/// A [Generator] that computes OSGi metadata in response to requests for
055/// [ManifestAttributes].
056///
057/// This implementation uses the `bndlib` library from the
058/// [bnd](https://github.com/bndtools/bnd) project to analyze bundle
059/// contents and compute manifest attributes.
060///
061/// When invoked, the analyzer first obtains resources of type [ClassTree]
062/// supplied to the project (typically by a [JavaCompiler]). These class
063/// trees are treated as the content of the bundle.
064///
065/// It then obtains resources of type [LibraryJarFile] from the project's
066/// dependencies with intents `Consume`, `Reveal` and `Expose` (the same
067/// intents as used by the [JavaCompiler] when assembling the compilation
068/// classpath). These library resources are registered as bundle
069/// dependencies.
070///
071/// The collected class tree and library resources are analyzed by `bndlib`
072/// to produce the manifest attributes requested.
073///
074/// Contrary to most [ResourceProvider]s, the [BndAnalyzer] needs project
075/// specific informations (supplied as instructions). This can be handled
076/// in multiple ways. One approach is to add the [BndAnalyzer] with the
077/// instructions in the project’s constructor rather than in 
078/// [RootProject#prepareProject]. Alternatively, put project-specific
079/// instructions in a `bnd.bnd` file in the project's directory, then
080/// register the analyzer in [RootProject#prepareProject] and add the
081/// instructions via [#instructions(Path)], where `Path` refers to the
082/// `bnd.bnd` file.
083///
084@SuppressWarnings("PMD.TooManyStaticImports")
085public class BndAnalyzer extends AbstractBndGenerator {
086
087    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
088
089    /// Initializes a new osgi analyzer.
090    ///
091    /// @param project the project
092    ///
093    public BndAnalyzer(Project project) {
094        super(project);
095    }
096
097    /// Add the instruction specified by key and value.
098    ///
099    /// @param key the key
100    /// @param value the value
101    /// @return the bnd analyzer
102    ///
103    @Override
104    public BndAnalyzer instruction(String key, String value) {
105        super.instruction(key, value);
106        return this;
107    }
108
109    /// Add the given instructions for the analyzer.
110    ///
111    /// @param instructions the instructions
112    /// @return the bnd analyzer
113    ///
114    @Override
115    public BndAnalyzer instructions(Map<String, String> instructions) {
116        super.instructions(instructions);
117        return this;
118    }
119
120    /// Add the instructions from the given bnd (properties) file.
121    ///
122    /// @param bndFile the bnd file
123    /// @return the bnd analyzer
124    ///
125    @Override
126    public BndAnalyzer instructions(Path bndFile) {
127        super.instructions(bndFile);
128        return this;
129    }
130
131    @Override
132    @SuppressWarnings("PMD.AvoidCatchingGenericException")
133    protected <T extends Resource> Stream<T>
134            doProvide(ResourceRequest<T> requested) {
135        if (!requested.accepts(ManifestAttributesType)) {
136            return Stream.empty();
137        }
138        try (var analyzer = new Analyzer();
139                var jar = new aQute.bnd.osgi.Jar("dot")) {
140            // Assemble bundle content
141            var content = Resources.of(ClassTreesType).addAll(project()
142                .providers().resources(of(ClassTree.class).using(Supply)));
143            // A bnd ("better never document") Jar can actually be a
144            // classfile tree, and several such "Jar"s can be merged.
145            // IOException will be throw (.get()) and handled in the outer try
146            vavrStream(content).find(_ -> true).peek(t -> Try.of(() -> jar
147                .addAll(new aQute.bnd.osgi.Jar(t.root().toFile()))).get());
148            analyzer.setJar(jar);
149            applyInstructions(analyzer);
150
151            // Add classpath dependencies
152            var bundleDeps = Resources.of(
153                new ResourceType<Resources<LibraryJarFile>>() {}).addAll(
154                    project().providers(Consume, Reveal, Expose)
155                        .resources(project().of(LibraryJarFileType)));
156            logger.atFiner().log("BndAnalyzer in"
157                + " %s uses dependencies %s", project(),
158                lazy(() -> bundleDeps.stream().map(e -> e.path().toString())
159                    .collect(Collectors.joining(File.pathSeparator))));
160            // IOException will be throw (.get()) and handled in the outer try
161            vavrStream(bundleDeps).forEach(dep -> Try
162                .run(() -> analyzer.addClasspath(dep.path().toFile())).get());
163
164            // Evaluate and convert to result type
165            var manifest = analyzer.calcManifest();
166            verifyManifest(manifest);
167            var asResource = ManifestAttributes.create();
168            asResource.putAll(manifest.getMainAttributes());
169            @SuppressWarnings("unchecked")
170            var result = (T) asResource;
171            return Stream.of(result);
172        } catch (Exception e) {
173            throw new BuildException().from(this).cause(e);
174        }
175    }
176
177    private void verifyManifest(Manifest manifest) {
178        Optional.ofNullable((String) manifest.getMainAttributes()
179            .get(new Attributes.Name("Bundle-Version"))).ifPresent(v -> {
180                try {
181                    new Version(v);
182                } catch (IllegalArgumentException e) {
183                    throw new ConfigurationException().message(
184                        "Attempt to specify invalid OSGi version %s", v)
185                        .from(this).cause(e);
186                }
187            });
188
189    }
190}