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