1 /*
2  * Copyright (C) 2018 The Android Open Source Project
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 
17 package com.android.internal.util.dump;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.util.IndentingPrintWriter;
22 import android.util.Log;
23 import android.util.proto.ProtoOutputStream;
24 
25 import java.nio.charset.StandardCharsets;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.LinkedHashMap;
29 import java.util.LinkedList;
30 
31 /**
32  * Dump either to a proto or a print writer using the same interface.
33  *
34  * <p>This mirrors the interface of {@link ProtoOutputStream}.
35  */
36 public class DualDumpOutputStream {
37     private static final String LOG_TAG = DualDumpOutputStream.class.getSimpleName();
38 
39     // When writing to a proto, the proto
40     private final @Nullable ProtoOutputStream mProtoStream;
41 
42     // When printing in clear text, the writer
43     private final @Nullable IndentingPrintWriter mIpw;
44     // Temporary storage of data when printing to mIpw
45     private final LinkedList<DumpObject> mDumpObjects = new LinkedList<>();
46 
47     private static abstract class Dumpable {
48         final String name;
49 
Dumpable(String name)50         private Dumpable(String name) {
51             this.name = name;
52         }
53 
print(IndentingPrintWriter ipw, boolean printName)54         abstract void print(IndentingPrintWriter ipw, boolean printName);
55     }
56 
57     private static class DumpObject extends Dumpable {
58         private final LinkedHashMap<String, ArrayList<Dumpable>> mSubObjects = new LinkedHashMap<>();
59 
DumpObject(String name)60         private DumpObject(String name) {
61             super(name);
62         }
63 
64         @Override
print(IndentingPrintWriter ipw, boolean printName)65         void print(IndentingPrintWriter ipw, boolean printName) {
66             if (printName) {
67                 ipw.println(name + "={");
68             } else {
69                 ipw.println("{");
70             }
71             ipw.increaseIndent();
72 
73             for (ArrayList<Dumpable> subObject: mSubObjects.values()) {
74                 int numDumpables = subObject.size();
75 
76                 if (numDumpables == 1) {
77                     subObject.get(0).print(ipw, true);
78                 } else {
79                     ipw.println(subObject.get(0).name + "=[");
80                     ipw.increaseIndent();
81 
82                     for (int i = 0; i < numDumpables; i++) {
83                         subObject.get(i).print(ipw, false);
84                     }
85 
86                     ipw.decreaseIndent();
87                     ipw.println("]");
88                 }
89             }
90 
91             ipw.decreaseIndent();
92             ipw.println("}");
93         }
94 
95         /**
96          * Add new field / subobject to this object.
97          *
98          * <p>If a name is added twice, they will be printed as a array
99          *
100          * @param fieldName name of the field added
101          * @param d The dumpable to add
102          */
add(String fieldName, Dumpable d)103         public void add(String fieldName, Dumpable d) {
104             ArrayList<Dumpable> l = mSubObjects.get(fieldName);
105 
106             if (l == null) {
107                 l = new ArrayList<>(1);
108                 mSubObjects.put(fieldName, l);
109             }
110 
111             l.add(d);
112         }
113     }
114 
115     private static class DumpField extends Dumpable {
116         private final String mValue;
117 
DumpField(String name, String value)118         private DumpField(String name, String value) {
119             super(name);
120             this.mValue = value;
121         }
122 
123         @Override
print(IndentingPrintWriter ipw, boolean printName)124         void print(IndentingPrintWriter ipw, boolean printName) {
125             if (printName) {
126                 ipw.println(name + "=" + mValue);
127             } else {
128                 ipw.println(mValue);
129             }
130         }
131     }
132 
133     /**
134      * Create a new DualDumpOutputStream.
135      *
136      * @param proto the {@link ProtoOutputStream}
137      */
DualDumpOutputStream(@onNull ProtoOutputStream proto)138     public DualDumpOutputStream(@NonNull ProtoOutputStream proto) {
139         mProtoStream = proto;
140         mIpw = null;
141     }
142 
143     /**
144      * Create a new DualDumpOutputStream.
145      *
146      * @param ipw the {@link IndentingPrintWriter}
147      */
DualDumpOutputStream(@onNull IndentingPrintWriter ipw)148     public DualDumpOutputStream(@NonNull IndentingPrintWriter ipw) {
149         mProtoStream = null;
150         mIpw = ipw;
151 
152         // Add root object
153         mDumpObjects.add(new DumpObject(null));
154     }
155 
write(@onNull String fieldName, long fieldId, double val)156     public void write(@NonNull String fieldName, long fieldId, double val) {
157         if (mProtoStream != null) {
158             mProtoStream.write(fieldId, val);
159         } else {
160             mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
161         }
162     }
163 
write(@onNull String fieldName, long fieldId, boolean val)164     public void write(@NonNull String fieldName, long fieldId, boolean val) {
165         if (mProtoStream != null) {
166             mProtoStream.write(fieldId, val);
167         } else {
168             mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
169         }
170     }
171 
write(@onNull String fieldName, long fieldId, int val)172     public void write(@NonNull String fieldName, long fieldId, int val) {
173         if (mProtoStream != null) {
174             mProtoStream.write(fieldId, val);
175         } else {
176             mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
177         }
178     }
179 
write(@onNull String fieldName, long fieldId, float val)180     public void write(@NonNull String fieldName, long fieldId, float val) {
181         if (mProtoStream != null) {
182             mProtoStream.write(fieldId, val);
183         } else {
184             mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
185         }
186     }
187 
write(@onNull String fieldName, long fieldId, byte[] val)188     public void write(@NonNull String fieldName, long fieldId, byte[] val) {
189         if (mProtoStream != null) {
190             mProtoStream.write(fieldId, val);
191         } else {
192             mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, Arrays.toString(val)));
193         }
194     }
195 
write(@onNull String fieldName, long fieldId, long val)196     public void write(@NonNull String fieldName, long fieldId, long val) {
197         if (mProtoStream != null) {
198             mProtoStream.write(fieldId, val);
199         } else {
200             mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
201         }
202     }
203 
write(@onNull String fieldName, long fieldId, @Nullable String val)204     public void write(@NonNull String fieldName, long fieldId, @Nullable String val) {
205         if (mProtoStream != null) {
206             mProtoStream.write(fieldId, val);
207         } else {
208             mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
209         }
210     }
211 
start(@onNull String fieldName, long fieldId)212     public long start(@NonNull String fieldName, long fieldId) {
213         if (mProtoStream != null) {
214             return mProtoStream.start(fieldId);
215         } else {
216             DumpObject d = new DumpObject(fieldName);
217             mDumpObjects.getLast().add(fieldName, d);
218             mDumpObjects.addLast(d);
219             return System.identityHashCode(d);
220         }
221     }
222 
end(long token)223     public void end(long token) {
224         if (mProtoStream != null) {
225             mProtoStream.end(token);
226         } else {
227             if (System.identityHashCode(mDumpObjects.getLast()) != token) {
228                 Log.w(LOG_TAG, "Unexpected token for ending " + mDumpObjects.getLast().name
229                                 + " at " + Arrays.toString(Thread.currentThread().getStackTrace()));
230             }
231             mDumpObjects.removeLast();
232         }
233     }
234 
flush()235     public void flush() {
236         if (mProtoStream != null) {
237             mProtoStream.flush();
238         } else {
239             if (mDumpObjects.size() == 1) {
240                 mDumpObjects.getFirst().print(mIpw, false);
241 
242                 // Reset root object
243                 mDumpObjects.clear();
244                 mDumpObjects.add(new DumpObject(null));
245             }
246 
247             mIpw.flush();
248         }
249     }
250 
251     /**
252      * Add a dump from a different service into this dump.
253      *
254      * <p>Only for clear text dump. For proto dump use {@link #write(String, long, byte[])}.
255      *
256      * @param fieldName The name of the field
257      * @param nestedState The state of the dump
258      */
writeNested(@onNull String fieldName, byte[] nestedState)259     public void writeNested(@NonNull String fieldName, byte[] nestedState) {
260         if (mIpw == null) {
261             Log.w(LOG_TAG, "writeNested does not work for proto logging");
262             return;
263         }
264 
265         mDumpObjects.getLast().add(fieldName,
266                 new DumpField(fieldName, (new String(nestedState, StandardCharsets.UTF_8)).trim()));
267     }
268 
269     /**
270      * @return {@code true} iff we are dumping to a proto
271      */
isProto()272     public boolean isProto() {
273         return mProtoStream != null;
274     }
275 }
276