Rework SingleFeatureTest
[odlparent.git] / features-test-plugin / src / main / java / org / opendaylight / odlparent / features / test / plugin / DependencyResolver.java
1 /*
2  * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.odlparent.features.test.plugin;
9
10 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.FEATURES;
11 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.KARAF_VERSION;
12 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.PAX_EXAM_VERSION;
13 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.RELEASE_VERSION;
14 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.TEST;
15 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.XML;
16 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.extractDependencies;
17 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.identifierOf;
18 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.isFeature;
19 import static org.opendaylight.odlparent.features.test.plugin.DependencyUtils.toAetherArtifact;
20
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.IOException;
24 import java.net.MalformedURLException;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.LinkedList;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.stream.Collectors;
32 import org.apache.karaf.features.Feature;
33 import org.apache.karaf.features.internal.model.Features;
34 import org.apache.karaf.features.internal.model.JaxbUtil;
35 import org.apache.maven.plugin.MojoExecutionException;
36 import org.eclipse.aether.RepositorySystem;
37 import org.eclipse.aether.RepositorySystemSession;
38 import org.eclipse.aether.artifact.Artifact;
39 import org.eclipse.aether.artifact.DefaultArtifact;
40 import org.eclipse.aether.repository.RemoteRepository;
41 import org.eclipse.aether.resolution.ArtifactRequest;
42 import org.eclipse.aether.resolution.ArtifactResolutionException;
43 import org.eclipse.aether.resolution.ArtifactResult;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * Maven dependencies resolver for feature artifacts. The main goal of dependencies resolution
49  * is moving artifacts to local repositories so these artifacts became available for karaf deployer
50  * when test feature is being installed.
51  */
52 final class DependencyResolver {
53     private static final Logger LOG = LoggerFactory.getLogger(DependencyResolver.class);
54
55     private final Map<String, Artifact> resolvedArtifacts = new HashMap<>();
56     private final Map<String, Set<String>> resolvedFeatures = new HashMap<>();
57     private final RepositorySystemSession repoSession;
58     private final List<RemoteRepository> repositories;
59     private final RepositorySystem repoSystem;
60
61     DependencyResolver(final RepositorySystem repoSystem, final RepositorySystemSession repoSession,
62             final List<RemoteRepository> repositories) {
63         this.repoSession = repoSession;
64         this.repositories = repositories;
65         this.repoSystem = repoSystem;
66     }
67
68     /**
69      * Iterates over artifacts, detects features, resolves dependencies on other artifacts.
70      *
71      * @param artifacts list of maven artifacts to check
72      * @return collection of feature descriptors for features from test scope, expected to be pre-installed
73      * @throws MojoExecutionException  if any dependency resolution fails
74      */
75     Set<FeatureDependency> resolveFeatures(final Collection<org.apache.maven.artifact.Artifact> artifacts)
76             throws MojoExecutionException {
77         final var featureDependencies = new LinkedList<FeatureDependency>();
78         for (var mvnArtifact : artifacts) {
79             final var artifact = toAetherArtifact(mvnArtifact);
80             if (isFeature(artifact)) {
81                 final var resolved = resolve(artifact);
82                 final var featureNames = resolveFeatureFile(resolved.getFile());
83                 if (TEST.equals(mvnArtifact.getScope())) {
84                     featureDependencies.add(new FeatureDependency(resolved, featureNames));
85                 }
86             }
87         }
88         return Set.copyOf(featureDependencies);
89     }
90
91     /**
92      * Resolves dependencies on features required for proper plugin functionality.
93      *
94      * @return collection of feature descriptors for the features expected to be pre-installed
95      * @throws MojoExecutionException if any dependency resolution fails
96      */
97     Set<FeatureDependency> resolvePluginFeatures() throws MojoExecutionException {
98
99         // pax-exam features, installed by karaf container, no need explicit installation
100         final var paxExamFeaturesResolved = resolve(
101             new DefaultArtifact("org.ops4j.pax.exam", "pax-exam-features", null, XML, PAX_EXAM_VERSION));
102         resolveFeatureFile(paxExamFeaturesResolved.getFile());
103
104         // karaf scr feature
105         final var karafFeatureResolved = resolve(
106             new DefaultArtifact("org.apache.karaf.features", "standard", FEATURES, XML, KARAF_VERSION));
107         resolveFeatureFile(karafFeatureResolved.getFile());
108
109         return Set.of(new FeatureDependency(karafFeatureResolved, Set.of("scr")));
110     }
111
112     String resolveKarafDistroUrl(final String groupId, final String artifactId, final String type)
113             throws MojoExecutionException {
114         final var resolved = resolve(new DefaultArtifact(groupId, artifactId, null, type, RELEASE_VERSION));
115         if (!resolved.getFile().exists()) {
116             throw new MojoExecutionException("No file for karaf distribution resolved " + resolved);
117         }
118         try {
119             return resolved.getFile().toURI().toURL().toString();
120         } catch (MalformedURLException e) {
121             throw new MojoExecutionException("Could not get karaf distribution URL", e);
122         }
123     }
124
125     /**
126      * Extracts features dependencies from feature file and resolves dependencies on maven artifacts including
127      * other features.
128      *
129      * @param featureFile the xml file describing features
130      * @return Collection of feature names extracted from the file
131      * @throws MojoExecutionException if file cannot be parsed, or any dependency resolved
132      */
133     Set<String> resolveFeatureFile(final File featureFile) throws MojoExecutionException {
134         final var identifier = featureFile.getAbsolutePath();
135         LOG.debug("Resolving dependencies for feature file: {}", identifier);
136         final var cached = resolvedFeatures.get(identifier);
137         if (cached != null) {
138             return cached;
139         }
140         if (!featureFile.exists()) {
141             LOG.debug("Feature file {} does not exist. Dependency resolution omitted.", identifier);
142             return Set.of();
143         }
144         final Features features;
145         try (var inputStream = new FileInputStream(featureFile)) {
146             features = JaxbUtil.unmarshal(featureFile.toURI().toString(), inputStream, false);
147         } catch (IOException e) {
148             throw new MojoExecutionException("Could not read feature file " + featureFile, e);
149         }
150         for (var unresolved : extractDependencies(features)) {
151             final var resolved = resolve(unresolved);
152             if (isFeature(resolved)) {
153                 resolveFeatureFile(resolved.getFile());
154             }
155         }
156         final var featureNames = features.getFeature().stream().map(Feature::getName).collect(Collectors.toSet());
157         resolvedFeatures.put(identifier, featureNames);
158         return featureNames;
159     }
160
161     private Artifact resolve(final Artifact unresolved) throws MojoExecutionException {
162         final var identifier = identifierOf(unresolved);
163         final var cached = resolvedArtifacts.get(identifier);
164         if (cached != null) {
165             return cached;
166         }
167         final var request = new ArtifactRequest().setRepositories(repositories).setArtifact(unresolved);
168         final ArtifactResult resolutionResult;
169         try {
170             resolutionResult = repoSystem.resolveArtifact(repoSession, request);
171         } catch (ArtifactResolutionException e) {
172             throw new MojoExecutionException("Could not resolve artifact " + identifier, e);
173         }
174         final var resolved = resolutionResult.getArtifact();
175         LOG.debug("Dependency resolved for {}", identifier);
176         final var file = resolved.getFile();
177         if (file == null || !file.exists()) {
178             LOG.warn("Dependency artifact {} is resolved but has no attached file.", identifier);
179         }
180         resolvedArtifacts.put(identifier, resolved);
181         return resolved;
182     }
183 }