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