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.ext.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/// This provider is made available as an extension.
085/// [![org.jdrupes:jdbld-ext-bnd:](
086/// https://img.shields.io/maven-central/v/org.jdrupes/jdbld-ext-bnd?label=org.jdrupes:jdbld-ext-bnd%3A)
087/// ](https://mvnrepository.com/artifact/org.jdrupes/jdbld-ext-bnd)
088///
089@SuppressWarnings("PMD.TooManyStaticImports")
090public class BndAnalyzer extends AbstractBndGenerator {
091
092    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
093
094    /// Initializes a new osgi analyzer.
095    ///
096    /// @param project the project
097    ///
098    public BndAnalyzer(Project project) {
099        super(project);
100    }
101
102    /// Add the instruction specified by key and value.
103    ///
104    /// @param key the key
105    /// @param value the value
106    /// @return the bnd analyzer
107    ///
108    @Override
109    public BndAnalyzer instruction(String key, String value) {
110        super.instruction(key, value);
111        return this;
112    }
113
114    /// Add the given instructions for the analyzer.
115    ///
116    /// @param instructions the instructions
117    /// @return the bnd analyzer
118    ///
119    @Override
120    public BndAnalyzer instructions(Map<String, String> instructions) {
121        super.instructions(instructions);
122        return this;
123    }
124
125    /// Add the instructions from the given bnd (properties) file.
126    ///
127    /// @param bndFile the bnd file
128    /// @return the bnd analyzer
129    ///
130    @Override
131    public BndAnalyzer instructions(Path bndFile) {
132        super.instructions(bndFile);
133        return this;
134    }
135
136    @Override
137    @SuppressWarnings("PMD.AvoidCatchingGenericException")
138    protected <T extends Resource> Stream<T>
139            doProvide(ResourceRequest<T> requested) {
140        if (!requested.accepts(ManifestAttributesType)) {
141            return Stream.empty();
142        }
143        try (var analyzer = new Analyzer();
144                var jar = new aQute.bnd.osgi.Jar("dot")) {
145            // Assemble bundle content
146            var content = Resources.of(ClassTreesType).addAll(project()
147                .providers().resources(of(ClassTree.class).using(Supply)));
148            // A bnd ("better never document") Jar can actually be a
149            // classfile tree, and several such "Jar"s can be merged.
150            // IOException will be throw (.get()) and handled in the outer try
151            vavrStream(content).find(_ -> true).peek(t -> Try.of(() -> jar
152                .addAll(new aQute.bnd.osgi.Jar(t.root().toFile()))).get());
153            analyzer.setJar(jar);
154            applyInstructions(analyzer);
155
156            // Add classpath dependencies
157            var bundleDeps = Resources.of(
158                new ResourceType<Resources<LibraryJarFile>>() {}).addAll(
159                    project().providers(Consume, Reveal, Expose)
160                        .resources(project().of(LibraryJarFileType)));
161            logger.atFiner().log("BndAnalyzer in"
162                + " %s uses dependencies %s", project(),
163                lazy(() -> bundleDeps.stream().map(e -> e.path().toString())
164                    .collect(Collectors.joining(File.pathSeparator))));
165            // IOException will be throw (.get()) and handled in the outer try
166            vavrStream(bundleDeps).forEach(dep -> Try
167                .run(() -> analyzer.addClasspath(dep.path().toFile())).get());
168
169            // Evaluate and convert to result type
170            var manifest = analyzer.calcManifest();
171            verifyManifest(manifest);
172            var asResource = ManifestAttributes.create();
173            asResource.putAll(manifest.getMainAttributes());
174            @SuppressWarnings("unchecked")
175            var result = (T) asResource;
176            return Stream.of(result);
177        } catch (Exception e) {
178            throw new BuildException().from(this).cause(e);
179        }
180    }
181
182    private void verifyManifest(Manifest manifest) {
183        Optional.ofNullable((String) manifest.getMainAttributes()
184            .get(new Attributes.Name("Bundle-Version"))).ifPresent(v -> {
185                try {
186                    new Version(v);
187                } catch (IllegalArgumentException e) {
188                    throw new ConfigurationException().message(
189                        "Attempt to specify invalid OSGi version %s", v)
190                        .from(this).cause(e);
191                }
192            });
193
194    }
195}