Skip to content

Commit 7586ac0

Browse files
authored
Merge pull request #954 from ascopes/bugfix/GH-951
GH-951: Reimplement AetherResolver to handle exceptions more thoroughly
2 parents 7fee286 + b46a9dd commit 7586ac0

File tree

9 files changed

+663
-119
lines changed

9 files changed

+663
-119
lines changed

protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/dependencies/MavenArtifactPathResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public interface MavenArtifactPathResolver {
4242
* @return the path to the resolved artifact.
4343
* @throws ResolutionException if resolution fails in the backend.
4444
*/
45-
Path resolveExecutable(MavenArtifact artifact) throws ResolutionException;
45+
Path resolveArtifact(MavenArtifact artifact) throws ResolutionException;
4646

4747
/**
4848
* Resolve all given dependencies based on their resolution depth semantics.

protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/dependencies/aether/AetherMavenArtifactPathResolver.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ final class AetherMavenArtifactPathResolver implements MavenArtifactPathResolver
7474
}
7575

7676
@Override
77-
public Path resolveExecutable(MavenArtifact artifact) throws ResolutionException {
77+
public Path resolveArtifact(MavenArtifact artifact) throws ResolutionException {
7878
log.debug("Resolving artifact \"{}\"", artifact);
7979
var unresolvedArtifact = aetherArtifactMapper.mapPmpArtifactToEclipseArtifact(artifact);
80-
var resolvedArtifact = aetherResolver.resolveRequiredArtifact(unresolvedArtifact);
80+
var resolvedArtifact = aetherResolver.resolveArtifact(unresolvedArtifact);
8181
var originalPath = aetherArtifactMapper.mapEclipseArtifactToPath(resolvedArtifact);
8282

8383
// GH-792: make a copy and set that as executable rather than changing what is in the
@@ -103,7 +103,7 @@ public List<Path> resolveDependencies(
103103
DependencyResolutionDepth depth,
104104
Set<String> dependencyScopes,
105105
boolean includeProjectArtifacts
106-
) {
106+
) throws ResolutionException {
107107
var unresolvedDependencies = new ArrayList<org.eclipse.aether.graph.Dependency>();
108108

109109
if (includeProjectArtifacts) {

protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/dependencies/aether/AetherResolver.java

Lines changed: 94 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,21 @@
1515
*/
1616
package io.github.ascopes.protobufmavenplugin.dependencies.aether;
1717

18-
import static java.util.Objects.requireNonNull;
18+
import static java.util.function.Predicate.not;
1919

2020
import io.github.ascopes.protobufmavenplugin.utils.ResolutionException;
2121
import io.github.ascopes.protobufmavenplugin.utils.StringUtils;
22-
import java.util.ArrayList;
2322
import java.util.Collection;
24-
import java.util.Collections;
2523
import java.util.List;
24+
import java.util.Objects;
2625
import java.util.Set;
26+
import java.util.stream.Collectors;
2727
import javax.inject.Inject;
2828
import javax.inject.Named;
29-
import org.apache.maven.execution.MavenSession;
3029
import org.apache.maven.execution.scope.MojoExecutionScoped;
30+
import org.apache.maven.project.MavenProject;
3131
import org.eclipse.aether.RepositorySystem;
32+
import org.eclipse.aether.RepositorySystemSession;
3233
import org.eclipse.aether.artifact.Artifact;
3334
import org.eclipse.aether.collection.CollectRequest;
3435
import org.eclipse.aether.graph.Dependency;
@@ -39,7 +40,6 @@
3940
import org.eclipse.aether.resolution.DependencyRequest;
4041
import org.eclipse.aether.resolution.DependencyResolutionException;
4142
import org.eclipse.aether.resolution.DependencyResult;
42-
import org.eclipse.aether.util.filter.ScopeDependencyFilter;
4343
import org.eclipse.sisu.Description;
4444
import org.slf4j.Logger;
4545
import org.slf4j.LoggerFactory;
@@ -53,7 +53,8 @@
5353
*
5454
* <p>Warning: the code in this class is very fragile and changing it can easily result in the
5555
* introduction of regressions for users. If you need to alter it, be very sure that you know what
56-
* you are doing!
56+
* you are doing and that <strong>all</strong> possible error cases are handled in a remotely
57+
* sensible way to avoid bug reports due to ambiguous handling!
5758
*
5859
* @author Ashley Scopes
5960
* @since 2.4.4
@@ -66,79 +67,61 @@ final class AetherResolver {
6667
private static final Logger log = LoggerFactory.getLogger(AetherResolver.class);
6768

6869
private final RepositorySystem repositorySystem;
69-
private final ProtobufMavenPluginRepositorySession repositorySystemSession;
70-
private final List<RemoteRepository> remoteRepositories;
70+
private final RepositorySystemSession repositorySystemSession;
71+
private final MavenProject mavenProject;
7172

7273
@Inject
7374
AetherResolver(
7475
RepositorySystem repositorySystem,
75-
MavenSession mavenSession,
76-
ProtobufMavenPluginRepositorySession repositorySystemSession
76+
RepositorySystemSession repositorySystemSession,
77+
MavenProject mavenProject
7778
) {
7879
this.repositorySystem = repositorySystem;
7980
this.repositorySystemSession = repositorySystemSession;
80-
81-
// Prior to v2.12.0, we used the ProjectBuildingRequest on the MavenSession
82-
// and used RepositoryUtils.toRepos to create the repository list. GH-579
83-
// was raised to report that the <repositories> block in the POM was being
84-
// ignored. This appears to be due to the project building request only
85-
// looking at what is in ~/.m2/settings.xml. The current project remote
86-
// repositories seems to be what we need to use instead here.
87-
remoteRepositories = mavenSession.getCurrentProject().getRemoteProjectRepositories();
81+
this.mavenProject = mavenProject;
8882
}
8983

90-
Artifact resolveRequiredArtifact(Artifact artifact) throws ResolutionException {
91-
log.info("Resolving artifact \"{}\"", artifact);
84+
Artifact resolveArtifact(Artifact artifact) throws ResolutionException {
85+
log.debug("Resolving artifact \"{}\"", artifact);
9286

93-
var request = new ArtifactRequest();
94-
request.setArtifact(artifact);
95-
request.setRepositories(remoteRepositories);
87+
var request = new ArtifactRequest()
88+
.setArtifact(artifact)
89+
.setRepositories(computeRemoteRepositories());
9690

97-
ArtifactResult result;
91+
ArtifactResult artifactResult;
92+
Exception cause = null;
9893

9994
try {
100-
result = repositorySystem.resolveArtifact(repositorySystemSession, request);
95+
artifactResult = repositorySystem.resolveArtifact(repositorySystemSession, request);
10196
} catch (ArtifactResolutionException ex) {
102-
log.debug("Discarding internal exception while resolving \"{}\"", artifact, ex);
103-
result = ex.getResult();
97+
log.debug("Handling resolution exception", ex);
98+
artifactResult = ex.getResult();
99+
cause = ex;
104100
}
105101

106-
// Looks like we can get resolution exceptions even if things resolve correctly, as it appears
107-
// to raise for the local repository first.
108-
// If this happens, don't bother raising or reporting it as it is normal behaviour. If anything
109-
// else goes wrong, then we can panic about it.
110-
if (result.isMissing()) {
111-
throw mapExceptions(
112-
"Failed to resolve artifact \"" + artifact + "\"",
113-
result.getExceptions()
102+
if (cause != null || !artifactResult.isResolved()) {
103+
var ex = new ResolutionException(
104+
"Failed to resolve artifact "
105+
+ artifact
106+
+ ". Check the coordinates and connection, and try again. Cause was: "
107+
+ cause,
108+
cause
114109
);
115-
}
116110

117-
reportWarnings(result.getExceptions());
111+
artifactResult.getExceptions().forEach(ex::addSuppressed);
112+
throw ex;
113+
}
118114

119-
// This shouldn't happen, but I do not trust that Aether isn't hiding some wild edge cases
120-
// for me here.
121-
return requireNonNull(
122-
result.getArtifact(),
123-
() -> "No result was returned by Aether while resolving artifact \"" + artifact + "\""
124-
);
115+
return Objects.requireNonNull(artifactResult.getArtifact());
125116
}
126117

127118
Collection<Artifact> resolveDependencies(
128119
List<Dependency> dependencies,
129120
Set<String> allowedDependencyScopes
130-
) {
131-
var collectRequest = new CollectRequest();
132-
collectRequest.setDependencies(dependencies);
133-
collectRequest.setRepositories(remoteRepositories);
134-
135-
var dependencyRequest = new DependencyRequest();
136-
dependencyRequest.setCollectRequest(collectRequest);
137-
var filter = new ScopeDependencyFilter(
138-
/* included: */ allowedDependencyScopes,
139-
/* excluded */ List.of()
140-
);
141-
dependencyRequest.setFilter(filter);
121+
) throws ResolutionException {
122+
if (dependencies.isEmpty()) {
123+
return List.of();
124+
}
142125

143126
log.debug(
144127
"Resolving {} with {} {} - {}",
@@ -148,67 +131,78 @@ Collection<Artifact> resolveDependencies(
148131
dependencies
149132
);
150133

134+
var dependencyRequest = new DependencyRequest()
135+
.setCollectRequest(new CollectRequest()
136+
.setDependencies(dependencies)
137+
.setRepositories(computeRemoteRepositories()))
138+
.setFilter(new InclusiveScopeDependencyFilter(allowedDependencyScopes));
139+
151140
DependencyResult dependencyResult;
141+
Exception cause = null;
152142

153143
try {
154144
dependencyResult = repositorySystem
155145
.resolveDependencies(repositorySystemSession, dependencyRequest);
156146
} catch (DependencyResolutionException ex) {
157-
log.debug("Discarding internal exception while resolving {}", dependencies, ex);
147+
log.debug("Handling resolution exception", ex);
158148
dependencyResult = ex.getResult();
149+
cause = ex;
159150
}
160151

161-
var artifacts = new ArrayList<Artifact>();
162-
var exceptions = new ArrayList<>(dependencyResult.getCollectExceptions());
163-
164-
for (var artifactResult : dependencyResult.getArtifactResults()) {
165-
var artifact = artifactResult.getArtifact();
166-
if (artifactResult.isResolved() && artifact != null) {
167-
log.debug("Resolution of dependencies returned artifact \"{}\"", artifact);
168-
artifacts.add(artifact);
152+
// Handle the multiple cases where we might have failed.
153+
if (cause != null || !dependencyResult.getCollectExceptions().isEmpty()) {
154+
// TODO(ascopes): should we limit the number of things output here?
155+
var failedGavs = dependencyResult.getArtifactResults().stream()
156+
.filter(not(ArtifactResult::isResolved))
157+
.map(ArtifactResult::getArtifact)
158+
.map(Artifact::toString)
159+
.collect(Collectors.joining(", "));
160+
161+
String errorMessage;
162+
163+
if (failedGavs.isEmpty()) {
164+
errorMessage = "Failed to resolve dependencies. Cause was: " + cause;
165+
} else {
166+
errorMessage = "Failed to resolve dependencies: " + failedGavs
167+
+ ". Check the direct and transitive coordinates, and network connection, "
168+
+ "then try again. Cause was: "
169+
+ cause;
169170
}
170171

171-
if (artifactResult.isMissing()) {
172-
log.debug("Could not find artifact \"{}\" during dependency resolution", artifact);
173-
}
172+
var ex = new ResolutionException(errorMessage, cause);
173+
dependencyResult.getCollectExceptions().forEach(ex::addSuppressed);
174+
dependencyResult.getArtifactResults().stream()
175+
.map(ArtifactResult::getExceptions)
176+
.flatMap(List::stream)
177+
.forEach(ex::addSuppressed);
174178

175-
exceptions.addAll(artifactResult.getExceptions());
179+
throw ex;
176180
}
177181

178-
reportWarnings(exceptions);
179-
180-
return Collections.unmodifiableList(artifacts);
182+
return dependencyResult.getArtifactResults()
183+
.stream()
184+
.map(ArtifactResult::getArtifact)
185+
.map(Objects::requireNonNull)
186+
.toList();
181187
}
182188

183-
private ResolutionException mapExceptions(String message, Collection<Exception> causes) {
184-
var causeIterator = causes.iterator();
185-
186-
// Assumption: this is always possible. If it isn't, we screwed up somewhere.
187-
var cause = causeIterator.next();
188-
var exception = new ResolutionException(
189-
message
190-
+ " - resolution failed with "
191-
+ StringUtils.pluralize(causes.size(), "error: ", "errors - first was: ")
192-
+ cause.getMessage(),
193-
cause
189+
// Some historical context of why we do this in a very specific way:
190+
//
191+
// Prior to v2.12.0, we used the ProjectBuildingRequest on the MavenSession
192+
// and used RepositoryUtils.toRepos to create the repository list. GH-579
193+
// was raised to report that the <repositories> block in the POM was being
194+
// ignored. This appears to be due to the project building request only
195+
// looking at what is in ~/.m2/settings.xml. The current project remote
196+
// repositories seems to be what we need to use instead here.
197+
//
198+
// As of 5.0.2, we use .newResolutionRepositories to do this as it ensures
199+
// certain networking and authentication configurations are propagated
200+
// correctly without relying on Maven exposing the final configuration to
201+
// us immediately.
202+
private List<RemoteRepository> computeRemoteRepositories() {
203+
return repositorySystem.newResolutionRepositories(
204+
repositorySystemSession,
205+
mavenProject.getRemoteProjectRepositories()
194206
);
195-
causeIterator.forEachRemaining(exception::addSuppressed);
196-
return exception;
197-
}
198-
199-
private void reportWarnings(Iterable<? extends Exception> exceptions) {
200-
exceptions.forEach(exception -> {
201-
// We purposely log the warning class and message twice, since exception tracebacks are
202-
// hidden unless Maven was invoked with --errors.
203-
//noinspection LoggingPlaceholderCountMatchesArgumentCount
204-
log.debug(
205-
"Dependency resolution warning was reported. "
206-
+ "This might be okay, or it could indicate a further issue - {}: {}",
207-
exception.getClass().getName(),
208-
exception.getMessage(),
209-
exception
210-
);
211-
});
212207
}
213208
}
214-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (C) 2023 Ashley Scopes
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.ascopes.protobufmavenplugin.dependencies.aether;
17+
18+
import java.util.List;
19+
import java.util.Set;
20+
import org.eclipse.aether.graph.DependencyFilter;
21+
import org.eclipse.aether.graph.DependencyNode;
22+
import org.jspecify.annotations.Nullable;
23+
24+
/**
25+
* Implementation of a subset of functionality within
26+
* {@link org.eclipse.aether.util.filter.ScopeDependencyFilter} that is easier to unit test.
27+
*
28+
* @author Ashley Scopes
29+
* @since 5.0.2
30+
*/
31+
final class InclusiveScopeDependencyFilter implements DependencyFilter {
32+
33+
// Visible for testing only.
34+
private final Set<String> allowedScopes;
35+
36+
InclusiveScopeDependencyFilter(Set<String> allowedScopes) {
37+
this.allowedScopes = allowedScopes;
38+
}
39+
40+
@Override
41+
public boolean accept(DependencyNode node, List<DependencyNode> parents) {
42+
var dependency = node.getDependency();
43+
return dependency != null && allowedScopes.contains(dependency.getScope());
44+
}
45+
46+
// For testing only.
47+
@Override
48+
public boolean equals(@Nullable Object obj) {
49+
return obj instanceof InclusiveScopeDependencyFilter self
50+
&& self.allowedScopes.equals(allowedScopes);
51+
}
52+
53+
@Override
54+
public int hashCode() {
55+
return allowedScopes.hashCode();
56+
}
57+
}

protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/plugins/ProtocPluginResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ private Optional<ResolvedProtocPlugin> resolveBinaryMavenPlugin(
148148

149149
log.debug("Resolving binary Maven protoc plugin \"{}\"", plugin);
150150

151-
var path = artifactPathResolver.resolveExecutable(plugin);
151+
var path = artifactPathResolver.resolveArtifact(plugin);
152152

153153
var id = computeId(path, index);
154154

protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/ProtocResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,6 @@ private Optional<Path> resolveFromMavenRepositories(String version) throws Resol
150150
.classifier(platformClassifierFactory.getClassifier(ARTIFACT_ID))
151151
.build();
152152

153-
return Optional.of(artifactPathResolver.resolveExecutable(artifact));
153+
return Optional.of(artifactPathResolver.resolveArtifact(artifact));
154154
}
155155
}

0 commit comments

Comments
 (0)