001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.resolution;
020
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.List;
024import java.util.Map;
025
026import org.eclipse.aether.RepositoryException;
027import org.eclipse.aether.repository.ArtifactRepository;
028import org.eclipse.aether.repository.LocalArtifactResult;
029import org.eclipse.aether.transfer.ArtifactFilteredOutException;
030import org.eclipse.aether.transfer.ArtifactNotFoundException;
031import org.eclipse.aether.transfer.RepositoryOfflineException;
032
033/**
034 * Thrown in case of a unresolvable artifacts.
035 */
036public class ArtifactResolutionException extends RepositoryException {
037    private final transient List<ArtifactResult> results;
038
039    /**
040     * Creates a new exception with the specified results.
041     *
042     * @param results The resolution results at the point the exception occurred, may be {@code null}.
043     */
044    public ArtifactResolutionException(List<ArtifactResult> results) {
045        super(getSmartMessage(results), getSmartCause(results));
046        if (results != null) {
047            getSuppressed(results).forEach(this::addSuppressed);
048        }
049        this.results = results != null ? results : Collections.emptyList();
050    }
051
052    /**
053     * Creates a new exception with the specified results and detail message.
054     *
055     * @param results The resolution results at the point the exception occurred, may be {@code null}.
056     * @param message The detail message, may be {@code null}.
057     */
058    public ArtifactResolutionException(List<ArtifactResult> results, String message) {
059        super(message, getSmartCause(results));
060        if (results != null) {
061            getSuppressed(results).forEach(this::addSuppressed);
062        }
063        this.results = results != null ? results : Collections.emptyList();
064    }
065
066    /**
067     * Creates a new exception with the specified results, detail message and cause.
068     *
069     * @param results The resolution results at the point the exception occurred, may be {@code null}.
070     * @param message The detail message, may be {@code null}.
071     * @param cause The exception that caused this one, may be {@code null}.
072     */
073    public ArtifactResolutionException(List<ArtifactResult> results, String message, Throwable cause) {
074        super(message, cause);
075        if (results != null) {
076            getSuppressed(results).forEach(this::addSuppressed);
077        }
078        this.results = results != null ? results : Collections.emptyList();
079    }
080
081    /**
082     * Gets the resolution results at the point the exception occurred. Despite being incomplete, callers might want to
083     * use these results to fail gracefully and continue their operation with whatever interim data has been gathered.
084     *
085     * @return The resolution results, never {@code null} (empty if unknown).
086     */
087    public List<ArtifactResult> getResults() {
088        return results;
089    }
090
091    /**
092     * Gets the first result from {@link #getResults()}. This is a convenience method for cases where callers know only
093     * a single result/request is involved.
094     *
095     * @return The (first) resolution result or {@code null} if none.
096     */
097    public ArtifactResult getResult() {
098        return (results != null && !results.isEmpty()) ? results.get(0) : null;
099    }
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}