content: /*
   * Copyright (C) 2018 The Android Open Source Project
   *
   * 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 com.android.internal.util.dump;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.util.Log;
  import android.util.proto.ProtoOutputStream;
  
  import com.android.internal.util.IndentingPrintWriter;
  
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.LinkedHashMap;
  import java.util.LinkedList;
  
  /**
   * Dump either to a proto or a print writer using the same interface.
   *
   * <p>This mirrors the interface of {@link ProtoOutputStream}.
   */
  public class DualDumpOutputStream {
      private static final String LOG_TAG = DualDumpOutputStream.class.getSimpleName();
  
      // When writing to a proto, the proto
      private final @Nullable ProtoOutputStream mProtoStream;
  
      // When printing in clear text, the writer
      private final @Nullable IndentingPrintWriter mIpw;
      // Temporary storage of data when printing to mIpw
      private final LinkedList<DumpObject> mDumpObjects = new LinkedList<>();
  
      private static abstract class Dumpable {
          final String name;
  
          private Dumpable(String name) {
              this.name = name;
          }
  
          abstract void print(IndentingPrintWriter ipw, boolean printName);
      }
  
      private static class DumpObject extends Dumpable {
          private final LinkedHashMap<String, ArrayList<Dumpable>> mSubObjects = new LinkedHashMap<>();
  
          private DumpObject(String name) {
              super(name);
          }
  
          @Override
          void print(IndentingPrintWriter ipw, boolean printName) {
              if (printName) {
                  ipw.println(name + "={");
              } else {
                  ipw.println("{");
              }
              ipw.increaseIndent();
  
              for (ArrayList<Dumpable> subObject: mSubObjects.values()) {
                  int numDumpables = subObject.size();
  
                  if (numDumpables == 1) {
                      subObject.get(0).print(ipw, true);
                  } else {
                      ipw.println(subObject.get(0).name + "=[");
                      ipw.increaseIndent();
  
                      for (int i = 0; i < numDumpables; i++) {
                          subObject.get(i).print(ipw, false);
                      }
  
                      ipw.decreaseIndent();
                      ipw.println("]");
                  }
              }
  
              ipw.decreaseIndent();
              ipw.println("}");
          }
  
          /**
           * Add new field / subobject to this object.
           *
           * <p>If a name is added twice, they will be printed as a array
           *
           * @param fieldName name of the field added
           * @param d The dumpable to add
           */
          public void add(String fieldName, Dumpable d) {
              ArrayList<Dumpable> l = mSubObjects.get(fieldName);
  
              if (l == null) {
                  l = new ArrayList<>(1);
                  mSubObjects.put(fieldName, l);
              }
  
              l.add(d);
          }
      }
  
      private static class DumpField extends Dumpable {
          private final String mValue;
  
          private DumpField(String name, String value) {
              super(name);
              this.mValue = value;
          }
  
          @Override
          void print(IndentingPrintWriter ipw, boolean printName) {
              if (printName) {
                  ipw.println(name + "=" + mValue);
              } else {
                  ipw.println(mValue);
              }
          }
      }
  
      /**
       * Create a new DualDumpOutputStream.
       *
       * @param proto the {@link ProtoOutputStream}
       */
      public DualDumpOutputStream(@NonNull ProtoOutputStream proto) {
          mProtoStream = proto;
          mIpw = null;
      }
  
      /**
       * Create a new DualDumpOutputStream.
       *
       * @param ipw the {@link IndentingPrintWriter}
       */
      public DualDumpOutputStream(@NonNull IndentingPrintWriter ipw) {
          mProtoStream = null;
          mIpw = ipw;
  
          // Add root object
          mDumpObjects.add(new DumpObject(null));
      }
  
      public void write(@NonNull String fieldName, long fieldId, double val) {
          if (mProtoStream != null) {
              mProtoStream.write(fieldId, val);
          } else {
              mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
          }
      }
  
      public void write(@NonNull String fieldName, long fieldId, boolean val) {
          if (mProtoStream != null) {
              mProtoStream.write(fieldId, val);
          } else {
              mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
          }
      }
  
      public void write(@NonNull String fieldName, long fieldId, int val) {
          if (mProtoStream != null) {
              mProtoStream.write(fieldId, val);
          } else {
              mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
          }
      }
  
      public void write(@NonNull String fieldName, long fieldId, float val) {
          if (mProtoStream != null) {
              mProtoStream.write(fieldId, val);
          } else {
              mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
          }
      }
  
      public void write(@NonNull String fieldName, long fieldId, byte[] val) {
          if (mProtoStream != null) {
              mProtoStream.write(fieldId, val);
          } else {
              mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, Arrays.toString(val)));
          }
      }
  
      public void write(@NonNull String fieldName, long fieldId, long val) {
          if (mProtoStream != null) {
              mProtoStream.write(fieldId, val);
          } else {
              mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
          }
      }
  
      public void write(@NonNull String fieldName, long fieldId, @Nullable String val) {
          if (mProtoStream != null) {
              mProtoStream.write(fieldId, val);
          } else {
              mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
          }
      }
  
      public long start(@NonNull String fieldName, long fieldId) {
          if (mProtoStream != null) {
              return mProtoStream.start(fieldId);
          } else {
              DumpObject d = new DumpObject(fieldName);
              mDumpObjects.getLast().add(fieldName, d);
              mDumpObjects.addLast(d);
              return System.identityHashCode(d);
          }
      }
  
      public void end(long token) {
          if (mProtoStream != null) {
              mProtoStream.end(token);
          } else {
              if (System.identityHashCode(mDumpObjects.getLast()) != token) {
                  Log.w(LOG_TAG, "Unexpected token for ending " + mDumpObjects.getLast().name
                                  + " at " + Arrays.toString(Thread.currentThread().getStackTrace()));
              }
              mDumpObjects.removeLast();
          }
      }
  
      public void flush() {
          if (mProtoStream != null) {
              mProtoStream.flush();
          } else {
              if (mDumpObjects.size() == 1) {
                  mDumpObjects.getFirst().print(mIpw, false);
  
                  // Reset root object
                  mDumpObjects.clear();
                  mDumpObjects.add(new DumpObject(null));
              }
  
              mIpw.flush();
          }
      }
  
      /**
       * Add a dump from a different service into this dump.
       *
       * <p>Only for clear text dump. For proto dump use {@link #write(String, long, byte[])}.
       *
       * @param fieldName The name of the field
       * @param nestedState The state of the dump
       */
      public void writeNested(@NonNull String fieldName, byte[] nestedState) {
          if (mIpw == null) {
              Log.w(LOG_TAG, "writeNested does not work for proto logging");
              return;
          }
  
          mDumpObjects.getLast().add(fieldName,
                  new DumpField(fieldName, (new String(nestedState, StandardCharsets.UTF_8)).trim()));
      }
  
      /**
       * @return {@code true} iff we are dumping to a proto
       */
      public boolean isProto() {
          return mProtoStream != null;
      }
  }
  