View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.resolution;
20  
21  import java.util.ArrayList;
22  import java.util.Collections;
23  import java.util.List;
24  import java.util.Map;
25  
26  import org.eclipse.aether.RepositoryException;
27  import org.eclipse.aether.repository.ArtifactRepository;
28  import org.eclipse.aether.repository.LocalArtifactResult;
29  import org.eclipse.aether.transfer.ArtifactFilteredOutException;
30  import org.eclipse.aether.transfer.ArtifactNotFoundException;
31  import org.eclipse.aether.transfer.RepositoryOfflineException;
32  
33  /**
34   * Thrown in case of a unresolvable artifacts.
35   */
36  public class ArtifactResolutionException extends RepositoryException {
37      private final transient List<ArtifactResult> results;
38  
39      /**
40       * Creates a new exception with the specified results.
41       *
42       * @param results The resolution results at the point the exception occurred, may be {@code null}.
43       */
44      public ArtifactResolutionException(List<ArtifactResult> results) {
45          super(getSmartMessage(results), getSmartCause(results));
46          if (results != null) {
47              getSuppressed(results).forEach(this::addSuppressed);
48          }
49          this.results = results != null ? results : Collections.emptyList();
50      }
51  
52      /**
53       * Creates a new exception with the specified results and detail message.
54       *
55       * @param results The resolution results at the point the exception occurred, may be {@code null}.
56       * @param message The detail message, may be {@code null}.
57       */
58      public ArtifactResolutionException(List<ArtifactResult> results, String message) {
59          super(message, getSmartCause(results));
60          if (results != null) {
61              getSuppressed(results).forEach(this::addSuppressed);
62          }
63          this.results = results != null ? results : Collections.emptyList();
64      }
65  
66      /**
67       * Creates a new exception with the specified results, detail message and cause.
68       *
69       * @param results The resolution results at the point the exception occurred, may be {@code null}.
70       * @param message The detail message, may be {@code null}.
71       * @param cause The exception that caused this one, may be {@code null}.
72       */
73      public ArtifactResolutionException(List<ArtifactResult> results, String message, Throwable cause) {
74          super(message, cause);
75          if (results != null) {
76              getSuppressed(results).forEach(this::addSuppressed);
77          }
78          this.results = results != null ? results : Collections.emptyList();
79      }
80  
81      /**
82       * Gets the resolution results at the point the exception occurred. Despite being incomplete, callers might want to
83       * use these results to fail gracefully and continue their operation with whatever interim data has been gathered.
84       *
85       * @return The resolution results, never {@code null} (empty if unknown).
86       */
87      public List<ArtifactResult> getResults() {
88          return results;
89      }
90  
91      /**
92       * Gets the first result from {@link #getResults()}. This is a convenience method for cases where callers know only
93       * a single result/request is involved.
94       *
95       * @return The (first) resolution result or {@code null} if none.
96       */
97      public ArtifactResult getResult() {
98          return (results != null && !results.isEmpty()) ? results.get(0) : null;
99      }
100 
101     private static String getSmartMessage(List<? extends ArtifactResult> results) {
102         if (results == null) {
103             return null;
104         }
105         StringBuilder buffer = new StringBuilder(256);
106 
107         buffer.append("The following artifacts could not be resolved: ");
108 
109         String sep = "";
110         for (ArtifactResult result : results) {
111             if (!result.isResolved()) {
112                 buffer.append(sep);
113                 buffer.append(result.getRequest().getArtifact());
114                 LocalArtifactResult localResult = result.getLocalArtifactResult();
115                 if (localResult != null) {
116                     buffer.append(" (");
117                     if (localResult.getPath() != null) {
118                         buffer.append("present");
119                         if (!localResult.isAvailable()) {
120                             buffer.append(", but unavailable");
121                         }
122                     } else {
123                         buffer.append("absent");
124                     }
125                     buffer.append(")");
126                 }
127                 sep = ", ";
128             }
129         }
130 
131         Throwable cause = getSmartCause(results);
132         if (cause != null) {
133             buffer.append(": ").append(cause.getMessage());
134         }
135 
136         String additionalFailures = getAdditionalFailures(results, cause);
137         if (!additionalFailures.isEmpty()) {
138             buffer.append(" (additional failures: ").append(additionalFailures).append(")");
139         }
140 
141         return buffer.toString();
142     }
143 
144     private static String getAdditionalFailures(List<? extends ArtifactResult> results, Throwable primaryCause) {
145         List<String> failures = new ArrayList<>();
146         for (ArtifactResult result : results) {
147             if (!result.isResolved()) {
148                 for (Map.Entry<ArtifactRepository, List<Exception>> entry :
149                         result.getMappedExceptions().entrySet()) {
150                     for (Exception exception : entry.getValue()) {
151                         if (exception != primaryCause) {
152                             String message = exception.getMessage();
153                             if (message == null) {
154                                 message = exception.getClass().getSimpleName();
155                             }
156                             failures.add(result.getRequest().getArtifact() + " from "
157                                     + entry.getKey().getId() + ": " + message);
158                         }
159                     }
160                 }
161             }
162         }
163         Collections.sort(failures);
164         return String.join("; ", failures);
165     }
166 
167     /**
168      * This method tries to be smart and figure out "cause", but it results in somewhat incomplete result. Maven Core
169      * and probably many other code relies on it, so is left in place, but client code should use {@link #getResults()}
170      * and {@link ArtifactResult#getMappedExceptions()} methods to build more appropriate error messages.
171      */
172     private static Throwable getSmartCause(List<? extends ArtifactResult> results) {
173         if (results == null) {
174             return null;
175         }
176         for (ArtifactResult result : results) {
177             if (!result.isResolved()) {
178                 Throwable notFound = null, offline = null;
179                 for (Throwable t : result.getExceptions()) {
180                     if (t instanceof ArtifactNotFoundException) {
181                         if (notFound == null || notFound instanceof ArtifactFilteredOutException) {
182                             notFound = t;
183                         }
184                         if (offline == null && t.getCause() instanceof RepositoryOfflineException) {
185                             offline = t;
186                         }
187                     } else {
188                         return t;
189                     }
190                 }
191                 if (offline != null) {
192                     return offline;
193                 }
194                 if (notFound != null) {
195                     return notFound;
196                 }
197             }
198         }
199         return null;
200     }
201 
202     /**
203      * Builds a forest of exceptions to be used as suppressed, and it will contain the whole forest of exceptions per
204      * repository.
205      */
206     private static List<Throwable> getSuppressed(List<? extends ArtifactResult> results) {
207         ArrayList<Throwable> result = new ArrayList<>(results.size());
208         for (ArtifactResult artifactResult : results) {
209             if (!artifactResult.isResolved()) {
210                 ArtifactResolutionException root = new ArtifactResolutionException(
211                         null,
212                         "Failed to resolve artifact "
213                                 + artifactResult.getRequest().getArtifact());
214                 for (Map.Entry<ArtifactRepository, List<Exception>> entry :
215                         artifactResult.getMappedExceptions().entrySet()) {
216                     for (Exception e : entry.getValue()) {
217                         root.addSuppressed(e);
218                     }
219                 }
220                 result.add(root);
221             }
222         }
223         return result;
224     }
225 }