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        return buffer.toString();
137    }
138
139    /**
140     * This method tries to be smart and figure out "cause", but it results in somewhat incomplete result. Maven Core
141     * and probably many other code relies on it, so is left in place, but client code should use {@link #getResults()}
142     * and {@link ArtifactResult#getMappedExceptions()} methods to build more appropriate error messages.
143     */
144    private static Throwable getSmartCause(List<? extends ArtifactResult> results) {
145        if (results == null) {
146            return null;
147        }
148        for (ArtifactResult result : results) {
149            if (!result.isResolved()) {
150                Throwable notFound = null, offline = null;
151                for (Throwable t : result.getExceptions()) {
152                    if (t instanceof ArtifactNotFoundException) {
153                        if (notFound == null || notFound instanceof ArtifactFilteredOutException) {
154                            notFound = t;
155                        }
156                        if (offline == null && t.getCause() instanceof RepositoryOfflineException) {
157                            offline = t;
158                        }
159                    } else {
160                        return t;
161                    }
162                }
163                if (offline != null) {
164                    return offline;
165                }
166                if (notFound != null) {
167                    return notFound;
168                }
169            }
170        }
171        return null;
172    }
173
174    /**
175     * Builds a forest of exceptions to be used as suppressed, and it will contain the whole forest of exceptions per
176     * repository.
177     */
178    private static List<Throwable> getSuppressed(List<? extends ArtifactResult> results) {
179        ArrayList<Throwable> result = new ArrayList<>(results.size());
180        for (ArtifactResult artifactResult : results) {
181            if (!artifactResult.isResolved()) {
182                ArtifactResolutionException root = new ArtifactResolutionException(
183                        null,
184                        "Failed to resolve artifact "
185                                + artifactResult.getRequest().getArtifact());
186                for (Map.Entry<ArtifactRepository, List<Exception>> entry :
187                        artifactResult.getMappedExceptions().entrySet()) {
188                    for (Exception e : entry.getValue()) {
189                        root.addSuppressed(e);
190                    }
191                }
192                result.add(root);
193            }
194        }
195        return result;
196    }
197}