001/*
002 * Extra Bnd Repository Plugins
003 * Copyright (C) 2019-2021 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package de.mnl.osgi.bnd.maven;
020
021import static aQute.bnd.osgi.repository.BridgeRepository.addInformationCapability;
022import aQute.bnd.osgi.resource.CapabilityBuilder;
023import aQute.bnd.osgi.resource.ResourceBuilder;
024import aQute.bnd.osgi.resource.ResourceUtils;
025import aQute.bnd.version.Version;
026import aQute.maven.api.Archive;
027import aQute.maven.api.Program;
028import aQute.maven.provider.MavenBackingRepository;
029import aQute.service.reporter.Reporter;
030import java.io.File;
031import java.io.IOException;
032import java.util.ArrayList;
033import java.util.Collection;
034import java.util.Collections;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.List;
038import java.util.Map;
039import java.util.Optional;
040import java.util.Set;
041import java.util.concurrent.ConcurrentHashMap;
042import java.util.concurrent.Executor;
043import java.util.function.Function;
044import java.util.stream.Collectors;
045import org.apache.maven.model.Dependency;
046import org.apache.maven.model.Model;
047import org.apache.maven.model.building.ModelBuildingException;
048import org.apache.maven.model.resolution.UnresolvableModelException;
049import org.osgi.framework.namespace.IdentityNamespace;
050import org.osgi.resource.Capability;
051import org.osgi.resource.Requirement;
052import org.osgi.resource.Resource;
053
054/**
055 * Wraps the artifacts from a maven repository as {@link Resource}s.
056 */
057@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
058public class MavenResourceRepository extends CompositeMavenRepository {
059
060    /** The namespace used to store the maven dependencies information. */
061    public static final String MAVEN_DEPENDENCIES_NS
062        = "maven.dependencies.info";
063
064    private Function<Archive, Optional<Resource>> resourceSupplier
065        = resource -> Optional.empty();
066    private final Map<Archive, MavenResource> resourceCache
067        = new ConcurrentHashMap<>();
068
069    /**
070     * Instantiates a new maven resource repository.
071     *
072     * @param base the base
073     * @param repoId the repo id
074     * @param releaseRepos the release repos
075     * @param snapshotRepos the snapshot repos
076     * @param executor the executor
077     * @param reporter the reporter
078     * @throws Exception the exception
079     */
080    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
081    public MavenResourceRepository(File base, String repoId,
082            List<MavenBackingRepository> releaseRepos,
083            List<MavenBackingRepository> snapshotRepos, Executor executor,
084            Reporter reporter) throws Exception {
085        super(base, repoId, releaseRepos, snapshotRepos, executor, reporter);
086    }
087
088    @Override
089    public void reset() {
090        super.reset();
091        resourceCache.clear();
092    }
093
094    /**
095     * Sets a function that can provide resource information more
096     * efficiently (e.g. from some local persistent cache) than
097     * the remote maven repository.
098     * <P>
099     * Any resource information provided by the function must be
100     * complete, i.e. must hold the information from the "bnd.info"
101     * namespace and from the "maven.dependencies.info" namespace.
102     *
103     * @param resourceSupplier the resource supplier
104     * @return the composite maven repository
105     */
106    public MavenResourceRepository setResourceSupplier(
107            Function<Archive, Optional<Resource>> resourceSupplier) {
108        this.resourceSupplier = resourceSupplier;
109        return this;
110    }
111
112    /**
113     * Creates a {@link MavenResource} for the given program and version. 
114     *
115     * @param program the program
116     * @param version the version
117     * @param extension the extension (or {@code null} for "jar")
118     * @param classifier the classifier (or {@code null} for "")
119     * @param location which URL to use for the binary in the {@link Resource}
120     * @return the resource
121     */
122    public Optional<MavenResource> resource(Program program,
123            MavenVersionSpecification version, String extension,
124            String classifier, BinaryLocation location) {
125        return find(program, version)
126            .map(revision -> resource(revision.archive(extension, classifier),
127                location));
128    }
129
130    /**
131     * Creates a {@link MavenResource} for the given archive. 
132     *
133     * @param archive the archive
134     * @param location which URL to use for the binary in the {@link Resource}
135     * @return the resource
136     */
137    public MavenResource resource(BoundArchive archive,
138            BinaryLocation location) {
139        return resourceCache.computeIfAbsent(archive,
140            a -> resourceSupplier.apply(a)
141                .map(resource -> new MavenResourceImpl(archive, resource))
142                .orElseGet(() -> new MavenResourceImpl(archive, location)));
143    }
144
145    /**
146     * Retrieves the dependency information from the provided
147     * resource. Assumes that the resource was created by this
148     * repository, i.e. with capabilities in the
149     * "maven.dependencies.info" name space.
150     *
151     * @param resource the resource
152     * @param dependencies the dependencies
153     */
154    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
155    private static void retrieveDependencies(Resource resource,
156            Collection<Dependency> dependencies) {
157        // Actually, there should be only one such capability per resource.
158        for (Capability capability : resource
159            .getCapabilities(MAVEN_DEPENDENCIES_NS)) {
160            Map<String, Object> depAttrs = capability.getAttributes();
161            depAttrs.values().stream()
162                .flatMap(val -> COORDS_SPLITTER.splitAsStream((String) val))
163                .map(rev -> {
164                    String[] parts = rev.split(":");
165                    Dependency dep = new Dependency();
166                    dep.setGroupId(parts[0]);
167                    dep.setArtifactId(parts[1]);
168                    dep.setVersion(parts[2]);
169                    return dep;
170                }).forEach(dependencies::add);
171        }
172    }
173
174    /**
175     * A maven resource that obtains its information
176     * lazily from a {@link CompositeMavenRepository}.
177     */
178    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
179    public class MavenResourceImpl implements MavenResource {
180
181        private final Archive archive;
182        private BoundArchive cachedArchive;
183        private Resource cachedDelegee;
184        private List<Dependency> cachedDependencies;
185        private final BinaryLocation location;
186
187        /**
188         * Instantiates a new maven resource from the given data.
189         *
190         * @param revision the archive
191         * @param location the location
192         */
193        private MavenResourceImpl(Archive archive, BinaryLocation location) {
194            this.archive = archive;
195            this.location = location;
196        }
197
198        /**
199         * Instantiates a new maven resource from the given data.
200         *
201         * @param revision the archive
202         * @param location the location
203         */
204        private MavenResourceImpl(BoundArchive archive,
205                BinaryLocation location) {
206            this.archive = archive;
207            this.cachedArchive = archive;
208            this.location = location;
209        }
210
211        /**
212         * Instantiates a new maven resource from the given data.
213         *
214         * @param revision the archive
215         * @param resource the resource information associated with the 
216         * archive.
217         */
218        private MavenResourceImpl(Archive archive, Resource resource) {
219            this.archive = archive;
220            this.cachedDelegee = resource;
221            // Doesn't matter, resource won't be created (already there).
222            this.location = BinaryLocation.REMOTE;
223        }
224
225        /**
226         * Instantiates a new maven resource from the given data.
227         *
228         * @param revision the revision
229         * @param resource the resource information associated with the 
230         * archive.
231         */
232        private MavenResourceImpl(BoundArchive archive, Resource resource) {
233            this.archive = archive;
234            this.cachedArchive = archive;
235            this.cachedDelegee = resource;
236            // Doesn't matter, resource won't be created (already there).
237            this.location = BinaryLocation.REMOTE;
238        }
239
240        @Override
241        public Archive archive() {
242            return archive;
243        }
244
245        @Override
246        public BoundArchive boundArchive() throws MavenResourceException {
247            try {
248                if (cachedArchive == null) {
249                    cachedArchive = find(archive).get();
250                }
251                return cachedArchive;
252            } catch (IOException e) {
253                throw new MavenResourceException(e);
254            }
255        }
256
257        @Override
258        public Resource asResource() throws MavenResourceException {
259            if (cachedDelegee == null) {
260                createResource();
261            }
262            return cachedDelegee;
263        }
264
265        /**
266         * Creates a {@link Resource} representation from the manifest
267         * of the artifact.  
268         *
269         * @throws IOException Signals that an I/O exception has occurred.
270         * @throws ModelBuildingException the model building exception
271         * @throws UnresolvableModelException the unresolvable model exception
272         */
273        @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
274            "PMD.AvoidInstanceofChecksInCatchClause",
275            "PMD.CyclomaticComplexity", "PMD.NcssCount",
276            "PMD.AvoidInstantiatingObjectsInLoops",
277            "PMD.AvoidLiteralsInIfCondition", "PMD.CognitiveComplexity" })
278        private void createResource() throws MavenResourceException {
279            Model model = model(archive.revision);
280            String extension = model.getPackaging();
281            if ("bundle".equals(extension)
282                || "eclipse-plugin".equals(extension)) {
283                extension = Archive.JAR_EXTENSION;
284            }
285            ResourceBuilder builder = new ResourceBuilder();
286            if (extension.equals(Archive.JAR_EXTENSION)) {
287                File binary;
288                try {
289                    binary = get(archive);
290                } catch (IOException e) {
291                    throw new MavenResourceException(e);
292                }
293                try {
294                    if (location == BinaryLocation.LOCAL) {
295                        builder.addFile(binary, binary.toURI());
296                    } else {
297                        builder.addFile(binary,
298                            boundArchive().mavenBackingRepository()
299                                .toURI(archive.remotePath));
300                    }
301                } catch (Exception e) {
302                    // That's what the exceptions thrown here come down to.
303                    throw new MavenResourceException(e);
304                }
305            }
306            List<Capability> caps = builder.getCapabilities();
307            Map<String, Object> idAttrs = caps.stream()
308                .filter(cap -> IdentityNamespace.IDENTITY_NAMESPACE
309                    .equals(cap.getNamespace()))
310                .findFirst().map(Capability::getAttributes)
311                .orElse(Collections.emptyMap());
312            String bsn = (String) idAttrs.getOrDefault(
313                IdentityNamespace.IDENTITY_NAMESPACE,
314                archive.getWithoutVersion());
315            Version version = ResourceUtils.toVersion(idAttrs.getOrDefault(
316                IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE,
317                archive.revision.version.getOSGiVersion()));
318            addInformationCapability(builder, bsn, version, archive.toString(),
319                null);
320
321            // Add dependency infos
322            if (!dependencies().isEmpty()) {
323                CapabilityBuilder cap
324                    = new CapabilityBuilder(MAVEN_DEPENDENCIES_NS);
325                @SuppressWarnings("PMD.UseConcurrentHashMap")
326                Map<String, Set<Dependency>> depsByScope = new HashMap<>();
327                for (Dependency dep : dependencies()) {
328                    String scope = Optional.ofNullable(dep.getScope())
329                        .orElse("compile").toLowerCase();
330                    depsByScope.computeIfAbsent(scope, key -> new HashSet<>())
331                        .add(dep);
332                }
333                try {
334                    for (var deps : depsByScope.entrySet()) {
335                        cap.addAttribute(deps.getKey(),
336                            toVersionList(deps.getValue()));
337                    }
338                } catch (Exception e) {
339                    throw new IllegalArgumentException(e);
340                }
341                builder.addCapability(cap);
342            }
343            cachedDelegee = builder.build();
344        }
345
346        private String toVersionList(Collection<Dependency> deps) {
347            StringBuilder depsList = new StringBuilder("");
348            for (Dependency dep : deps) {
349                if (depsList.length() > 0) {
350                    depsList.append(';');
351                }
352                depsList.append(dep.getGroupId());
353                depsList.append(':');
354                depsList.append(dep.getArtifactId());
355                depsList.append(':');
356                depsList.append(dep.getVersion());
357            }
358            return depsList.toString();
359        }
360
361        @Override
362        public List<Capability> getCapabilities(String namespace)
363                throws MavenResourceException {
364            return asResource().getCapabilities(namespace);
365        }
366
367        @Override
368        public List<Requirement> getRequirements(String namespace)
369                throws MavenResourceException {
370            return asResource().getRequirements(namespace);
371        }
372
373        @Override
374        public boolean equals(Object obj) {
375            if (obj == null) {
376                return false;
377            }
378            if (obj instanceof MavenResource) {
379                return archive.equals(((MavenResource) obj).archive());
380            }
381            if (obj instanceof Resource) {
382                try {
383                    return asResource().equals(obj);
384                } catch (MavenResourceException e) {
385                    return false;
386                }
387            }
388            return false;
389        }
390
391        @Override
392        public int hashCode() {
393            return archive.hashCode();
394        }
395
396        @Override
397        public String toString() {
398            return archive.toString();
399        }
400
401        /*
402         * (non-Javadoc)
403         * 
404         * @see de.mnl.osgi.bnd.maven.MavenResource#dependencies()
405         */
406        @Override
407        @SuppressWarnings({ "PMD.ConfusingTernary",
408            "PMD.AvoidSynchronizedAtMethodLevel" })
409        public final synchronized List<Dependency> dependencies()
410                throws MavenResourceException {
411            if (cachedDependencies == null) {
412                if (cachedDelegee != null) {
413                    cachedDependencies = new ArrayList<>();
414                    retrieveDependencies(cachedDelegee, cachedDependencies);
415                } else {
416                    cachedDependencies = MavenResourceRepository.this
417                        .model(archive.revision).getDependencies().stream()
418                        .filter(dep -> !dep.getGroupId().contains("$")
419                            && !dep.getArtifactId().contains("$")
420                            && !dep.isOptional()
421                            && (dep.getScope() == null
422                                || dep.getScope().equals("compile")
423                                || dep.getScope().equals("runtime")
424                                || dep.getScope().equals("provided")))
425                        .collect(Collectors.toList());
426                }
427            }
428            return cachedDependencies;
429        }
430
431    }
432}