• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 Google LLC
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 com.google.android.libraries.mobiledatadownload;
17 
18 import androidx.annotation.VisibleForTesting;
19 import com.google.common.base.Optional;
20 import com.google.common.collect.ImmutableList;
21 import com.google.common.util.concurrent.FutureCallback;
22 import com.google.common.util.concurrent.Futures;
23 import com.google.common.util.concurrent.ListenableFuture;
24 import java.io.PrintWriter;
25 import java.io.StringWriter;
26 import java.util.Collection;
27 import java.util.Locale;
28 import java.util.concurrent.CancellationException;
29 import java.util.concurrent.ExecutionException;
30 import javax.annotation.Nullable;
31 
32 /**
33  * Represents an exception that's an aggregate of multiple other exceptions.
34  *
35  * <p>The first aggregated exception in set as the cause (see {@link Exception#getCause}) of the
36  * {@link AggregateException}. The full list can be accessed with {@link #getFailures}.
37  */
38 public final class AggregateException extends Exception {
39   private static final String SEPARATOR_HEADER = "--- Failure %d ----------------------------\n";
40   private static final String SEPARATOR = "-------------------------------------------";
41 
42   /** The maximum number of causes to recursively print in an exception's message. */
43   private static final int MAX_CAUSE_DEPTH = 5;
44 
45   private final ImmutableList<Throwable> failures;
46 
AggregateException(String message, Throwable cause, ImmutableList<Throwable> failures)47   private AggregateException(String message, Throwable cause, ImmutableList<Throwable> failures) {
48     super(message, cause);
49     this.failures = failures;
50   }
51 
52   /** Returns the list of the aggregated failures. */
getFailures()53   public ImmutableList<Throwable> getFailures() {
54     return failures;
55   }
56 
57   /**
58    * Throws {@link AggregateException} if any of the future in {@code futures} fails with either
59    * {@link CancellationException} or {@link ExecutionException}.
60    *
61    * <p>The {@code callbackOptional}, if present, will be executed each time when a future inside
62    * {@code futures} completes.
63    */
throwIfFailed( Collection<ListenableFuture<T>> futures, Optional<FutureCallback<T>> callbackOptional, String message, Object... args)64   public static <T> void throwIfFailed(
65       Collection<ListenableFuture<T>> futures,
66       Optional<FutureCallback<T>> callbackOptional,
67       String message,
68       Object... args)
69       throws AggregateException {
70     ImmutableList.Builder<Throwable> builder = null;
71     for (ListenableFuture<T> future : futures) {
72       try {
73         T result = Futures.getDone(future);
74         if (callbackOptional.isPresent()) {
75           callbackOptional.get().onSuccess(result);
76         }
77       } catch (CancellationException | ExecutionException e) {
78         // Unwrap the cause of the execution exception, we will instead wrap it with the aggregate.
79         if (builder == null) {
80           builder = ImmutableList.builder();
81         }
82         Throwable unwrapped = unwrapException(e);
83         builder.add(unwrapped);
84         if (callbackOptional.isPresent()) {
85           callbackOptional.get().onFailure(unwrapped);
86         }
87       }
88     }
89     if (builder == null) {
90       return;
91     }
92     final ImmutableList<Throwable> failures = builder.build();
93     throw newInstance(failures, message, args);
94   }
95 
96   /**
97    * Throws {@link AggregateException} if any of the future in {@code futures} fails with either
98    * {@link CancellationException} or {@link ExecutionException}.
99    */
throwIfFailed( Collection<ListenableFuture<T>> futures, String message, Object... args)100   public static <T> void throwIfFailed(
101       Collection<ListenableFuture<T>> futures, String message, Object... args)
102       throws AggregateException {
103     throwIfFailed(futures, /* callbackOptional= */ Optional.absent(), message, args);
104   }
105 
106   /**
107    * Consolidates completed futures into a single failed futures if any failures exist.
108    *
109    * <p>If there are no failures, a void future will be returned.
110    *
111    * <p>If there is a single failure, that failure will be returned.
112    *
113    * <p>If there are multiple failures, an AggregateException containing all failures will be
114    * returned.
115    */
consolidateDoneFutures( Collection<ListenableFuture<T>> futures, String message, Object... args)116   public static <T> ListenableFuture<Void> consolidateDoneFutures(
117       Collection<ListenableFuture<T>> futures, String message, Object... args) {
118     try {
119       // Check if any futures are failed.
120       throwIfFailed(futures, message, args);
121     } catch (AggregateException e) {
122       if (e.getFailures().size() == 1) {
123         return Futures.immediateFailedFuture(e.getFailures().get(0));
124       } else {
125         return Futures.immediateFailedFuture(e);
126       }
127     }
128     return Futures.immediateVoidFuture();
129   }
130 
131   /** Constructs a new {@link AggregateException} with the given throwables. */
newInstance( ImmutableList<Throwable> failures, String message, Object... args)132   public static AggregateException newInstance(
133       ImmutableList<Throwable> failures, String message, Object... args) {
134     String prologue = String.format(Locale.US, message, args);
135     return new AggregateException(
136         failures.size() > 1
137             ? throwablesToString(
138                 prologue + "\n" + failures.size() + " failure(s) in total:\n", failures)
139             : prologue,
140         failures.get(0),
141         failures);
142   }
143 
144   @VisibleForTesting
unwrapException(Throwable t)145   static Throwable unwrapException(Throwable t) {
146     Throwable cause = t.getCause();
147     if (cause == null) {
148       return t;
149     }
150     Class<?> clazz = t.getClass();
151     if (clazz.equals(ExecutionException.class)) {
152       return unwrapException(cause);
153     }
154     return t;
155   }
156 
throwablesToString( @ullable String prologue, ImmutableList<Throwable> throwables)157   private static String throwablesToString(
158       @Nullable String prologue, ImmutableList<Throwable> throwables) {
159     try (StringWriter out = new StringWriter();
160         PrintWriter writer = new PrintWriter(out)) {
161       if (prologue != null) {
162         writer.println(prologue);
163       }
164       for (int i = 0; i < throwables.size(); i++) {
165         Throwable failure = throwables.get(i);
166         writer.printf(SEPARATOR_HEADER, (i + 1));
167         writer.println(throwableToString(failure));
168       }
169       writer.println(SEPARATOR);
170       return out.toString();
171     } catch (Throwable t) {
172       return "Failed to build string from throwables: " + t;
173     }
174   }
175 
176   @VisibleForTesting
throwableToString(Throwable failure)177   static String throwableToString(Throwable failure) {
178     return throwableToString(failure, /* depth= */ 1);
179   }
180 
throwableToString(Throwable failure, int depth)181   private static String throwableToString(Throwable failure, int depth) {
182     String message = failure.getClass().getName() + ": " + failure.getMessage();
183     Throwable cause = failure.getCause();
184     if (cause != null) {
185       if (depth >= MAX_CAUSE_DEPTH) {
186         return message + "\n(...)";
187       }
188       return message + "\nCaused by: " + throwableToString(cause, depth + 1);
189     }
190     return message;
191   }
192 }
193