code: /*
   * Copyright (c) 2016-present, RxJava Contributors.
   *
   * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
   * compliance with the License. You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software distributed under the License is
   * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
   * the License for the specific language governing permissions and limitations under the License.
   */
  
  package io.reactivex.rxjava3.exceptions;
  
  import java.io.*;
  import java.util.*;
  
  import io.reactivex.rxjava3.annotations.NonNull;
  
  /**
   * Represents an exception that is a composite of one or more other exceptions. A {@code CompositeException}
   * does not modify the structure of any exception it wraps, but at print-time it iterates through the list of
   * Throwables contained in the composite in order to print them all.
   *
   * Its invariant is to contain an immutable, ordered (by insertion order), unique list of non-composite
   * exceptions. You can retrieve individual exceptions in this list with {@link #getExceptions()}.
   *
   * The {@link #printStackTrace()} implementation handles the StackTrace in a customized way instead of using
   * {@code getCause()} so that it can avoid circular references.
   *
   * If you invoke {@link #getCause()}, it will lazily create the causal chain but will stop if it finds any
   * Throwable in the chain that it has already seen.
   */
  public final class CompositeException extends RuntimeException {
  
      private static final long serialVersionUID = 3026362227162912146L;
  
      private final List<Throwable> exceptions;
      private final String message;
      private Throwable cause;
  
      /**
       * Constructs a CompositeException with the given array of Throwables as the
       * list of suppressed exceptions.
       * @param exceptions the Throwables to have as initially suppressed exceptions
       *
       * @throws IllegalArgumentException if <code>exceptions</code> is empty.
       */
      public CompositeException(@NonNull Throwable... exceptions) {
          this(exceptions == null ?
                  Collections.singletonList(new NullPointerException("exceptions was null")) : Arrays.asList(exceptions));
      }
  
      /**
       * Constructs a CompositeException with the given array of Throwables as the
       * list of suppressed exceptions.
       * @param errors the Throwables to have as initially suppressed exceptions
       *
       * @throws IllegalArgumentException if <code>errors</code> is empty.
       */
      public CompositeException(@NonNull Iterable<? extends Throwable> errors) {
          Set<Throwable> deDupedExceptions = new LinkedHashSet<>();
          if (errors != null) {
              for (Throwable ex : errors) {
                  if (ex instanceof CompositeException) {
                      deDupedExceptions.addAll(((CompositeException) ex).getExceptions());
                  } else
                  if (ex != null) {
                      deDupedExceptions.add(ex);
                  } else {
                      deDupedExceptions.add(new NullPointerException("Throwable was null!"));
                  }
              }
          } else {
              deDupedExceptions.add(new NullPointerException("errors was null"));
          }
          if (deDupedExceptions.isEmpty()) {
              throw new IllegalArgumentException("errors is empty");
          }
          List<Throwable> localExceptions = new ArrayList<>(deDupedExceptions);
          this.exceptions = Collections.unmodifiableList(localExceptions);
          this.message = exceptions.size() + " exceptions occurred. ";
      }
  
      /**
       * Retrieves the list of exceptions that make up the {@code CompositeException}.
       *
       * @return the exceptions that make up the {@code CompositeException}, as a {@link List} of {@link Throwable}s
       */
      @NonNull
      public List<Throwable> getExceptions() {
          return exceptions;
      }
  
      @Override
      @NonNull
      public String getMessage() {
          return message;
      }
  
      @Override
      @NonNull
      public synchronized Throwable getCause() { // NOPMD
          if (cause == null) {
              String separator = System.getProperty("line.separator");
              if (exceptions.size() > 1) {
                  Map<Throwable, Boolean> seenCauses = new IdentityHashMap<>();
  
                  StringBuilder aggregateMessage = new StringBuilder();
                  aggregateMessage.append("Multiple exceptions (").append(exceptions.size()).append(")").append(separator);
  
                  for (Throwable inner : exceptions) {
                      int depth = 0;
                      while (inner != null) {
                          for (int i = 0; i < depth; i++) {
                              aggregateMessage.append("  ");
                          }
                          aggregateMessage.append("|-- ");
                          aggregateMessage.append(inner.getClass().getCanonicalName()).append(": ");
                          String innerMessage = inner.getMessage();
                          if (innerMessage != null && innerMessage.contains(separator)) {
                              aggregateMessage.append(separator);
                              for (String line : innerMessage.split(separator)) {
                                  for (int i = 0; i < depth + 2; i++) {
                                      aggregateMessage.append("  ");
                                  }
                                  aggregateMessage.append(line).append(separator);
                              }
                          } else {
                              aggregateMessage.append(innerMessage);
                              aggregateMessage.append(separator);
                          }
  
                          for (int i = 0; i < depth + 2; i++) {
                              aggregateMessage.append("  ");
                          }
                          StackTraceElement[] st = inner.getStackTrace();
                          if (st.length > 0) {
                              aggregateMessage.append("at ").append(st[0]).append(separator);
                          }
  
                          if (!seenCauses.containsKey(inner)) {
                              seenCauses.put(inner, true);
  
                              inner = inner.getCause();
                              depth++;
                          } else {
                              inner = inner.getCause();
                              if (inner != null) {
                                  for (int i = 0; i < depth + 2; i++) {
                                      aggregateMessage.append("  ");
                                  }
                                  aggregateMessage.append("|-- ");
                                  aggregateMessage.append("(cause not expanded again) ");
                                  aggregateMessage.append(inner.getClass().getCanonicalName()).append(": ");
                                  aggregateMessage.append(inner.getMessage());
                                  aggregateMessage.append(separator);
                              }
                              break;
                          }
                      }
                  }
  
                  cause = new ExceptionOverview(aggregateMessage.toString().trim());
              } else {
                  cause = exceptions.get(0);
              }
          }
          return cause;
      }
  
      /**
       * All of the following {@code printStackTrace} functionality is derived from JDK {@link Throwable}
       * {@code printStackTrace}. In particular, the {@code PrintStreamOrWriter} abstraction is copied wholesale.
       *
       * Changes from the official JDK implementation:<ul>
       * <li>no infinite loop detection</li>
       * <li>smaller critical section holding {@link PrintStream} lock</li>
       * <li>explicit knowledge about the exceptions {@link List} that this loops through</li>
       * </ul>
       */
      @Override
      public void printStackTrace() {
          printStackTrace(System.err);
      }
  
      @Override
      public void printStackTrace(PrintStream s) {
          printStackTrace(new WrappedPrintStream(s));
      }
  
      @Override
      public void printStackTrace(PrintWriter s) {
          printStackTrace(new WrappedPrintWriter(s));
      }
  
      /**
       * Special handling for printing out a {@code CompositeException}.
       * Loops through all inner exceptions and prints them out.
       *
       * @param output
       *            stream to print to
       */
      private void printStackTrace(PrintStreamOrWriter output) {
          output.append(this).append("\n");
          for (StackTraceElement myStackElement : getStackTrace()) {
              output.append("\tat ").append(myStackElement).append("\n");
          }
          int i = 1;
          for (Throwable ex : exceptions) {
              output.append("  ComposedException ").append(i).append(" :\n");
              appendStackTrace(output, ex, "\t");
              i++;
          }
          output.append("\n");
      }
  
      private void appendStackTrace(PrintStreamOrWriter output, Throwable ex, String prefix) {
          output.append(prefix).append(ex).append('\n');
          for (StackTraceElement stackElement : ex.getStackTrace()) {
              output.append("\t\tat ").append(stackElement).append('\n');
          }
          if (ex.getCause() != null) {
              output.append("\tCaused by: ");
              appendStackTrace(output, ex.getCause(), "");
          }
      }
  
      abstract static class PrintStreamOrWriter {
          /**
           * Prints the object's string representation via the underlying PrintStream or PrintWriter.
           * @param o the object to print
           * @return this
           */
          abstract PrintStreamOrWriter append(Object o);
      }
  
      /**
       * Same abstraction and implementation as in JDK to allow PrintStream and PrintWriter to share implementation.
       */
      static final class WrappedPrintStream extends PrintStreamOrWriter {
          private final PrintStream printStream;
  
          WrappedPrintStream(PrintStream printStream) {
              this.printStream = printStream;
          }
  
          @Override
          WrappedPrintStream append(Object o) {
              printStream.print(o);
              return this;
          }
      }
  
      /**
       * Same abstraction and implementation as in JDK to allow PrintStream and PrintWriter to share implementation.
       */
      static final class WrappedPrintWriter extends PrintStreamOrWriter {
          private final PrintWriter printWriter;
  
          WrappedPrintWriter(PrintWriter printWriter) {
              this.printWriter = printWriter;
          }
  
          @Override
          WrappedPrintWriter append(Object o) {
              printWriter.print(o);
              return this;
          }
      }
  
      /**
       * Contains a formatted message with a simplified representation of the exception graph
       * contained within the CompositeException.
       */
      static final class ExceptionOverview extends RuntimeException {
  
          private static final long serialVersionUID = 3875212506787802066L;
  
          ExceptionOverview(String message) {
              super(message);
          }
  
          @Override
          public synchronized Throwable fillInStackTrace() {
              return this;
          }
      }
  
      /**
       * Returns the number of suppressed exceptions.
       * @return the number of suppressed exceptions
       */
      public int size() {
          return exceptions.size();
      }
  }
  