1 /*
2  * Copyright (C) 2020 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 android.util;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 
22 import java.io.PrintWriter;
23 import java.io.Writer;
24 import java.util.Arrays;
25 
26 /**
27  * Lightweight wrapper around {@link PrintWriter} that automatically indents
28  * newlines based on internal state. It also automatically wraps long lines
29  * based on given line length.
30  * <p>
31  * Delays writing indent until first actual write on a newline, enabling indent
32  * modification after newline.
33  *
34  * @hide
35  */
36 // Exported to Mainline modules; cannot use annotations
37 // @android.ravenwood.annotation.RavenwoodKeepWholeClass
38 public class IndentingPrintWriter extends PrintWriter {
39     private final String mSingleIndent;
40     private final int mWrapLength;
41 
42     /** Mutable version of current indent */
43     private StringBuilder mIndentBuilder = new StringBuilder();
44     /** Cache of current {@link #mIndentBuilder} value */
45     private char[] mCurrentIndent;
46     /** Length of current line being built, excluding any indent */
47     private int mCurrentLength;
48 
49     /**
50      * Flag indicating if we're currently sitting on an empty line, and that
51      * next write should be prefixed with the current indent.
52      */
53     private boolean mEmptyLine = true;
54 
55     private char[] mSingleChar = new char[1];
56 
IndentingPrintWriter(@onNull Writer writer)57     public IndentingPrintWriter(@NonNull Writer writer) {
58         this(writer, "  ", -1);
59     }
60 
IndentingPrintWriter(@onNull Writer writer, @NonNull String singleIndent)61     public IndentingPrintWriter(@NonNull Writer writer, @NonNull String singleIndent) {
62         this(writer, singleIndent, null, -1);
63     }
64 
IndentingPrintWriter(@onNull Writer writer, @NonNull String singleIndent, String prefix)65     public IndentingPrintWriter(@NonNull Writer writer, @NonNull String singleIndent,
66             String prefix) {
67         this(writer, singleIndent, prefix, -1);
68     }
69 
IndentingPrintWriter(@onNull Writer writer, @NonNull String singleIndent, int wrapLength)70     public IndentingPrintWriter(@NonNull Writer writer, @NonNull String singleIndent,
71             int wrapLength) {
72         this(writer, singleIndent, null, wrapLength);
73     }
74 
IndentingPrintWriter(@onNull Writer writer, @NonNull String singleIndent, @Nullable String prefix, int wrapLength)75     public IndentingPrintWriter(@NonNull Writer writer, @NonNull String singleIndent,
76             @Nullable String prefix, int wrapLength) {
77         super(writer);
78         mSingleIndent = singleIndent;
79         mWrapLength = wrapLength;
80         if (prefix != null) {
81             mIndentBuilder.append(prefix);
82         }
83     }
84 
85     /**
86      * Overrides the indent set in the constructor for the next printed line.
87      *
88      * @deprecated Use the "prefix" constructor parameter
89      * @hide
90      */
91     @NonNull
92     @Deprecated
setIndent(@onNull String indent)93     public IndentingPrintWriter setIndent(@NonNull String indent) {
94         mIndentBuilder.setLength(0);
95         mIndentBuilder.append(indent);
96         mCurrentIndent = null;
97         return this;
98     }
99 
100     /**
101      * Overrides the indent set in the constructor with {@code singleIndent} repeated {@code indent}
102      * times.
103      *
104      * @deprecated Use the "prefix" constructor parameter
105      * @hide
106      */
107     @NonNull
108     @Deprecated
setIndent(int indent)109     public IndentingPrintWriter setIndent(int indent) {
110         mIndentBuilder.setLength(0);
111         for (int i = 0; i < indent; i++) {
112             increaseIndent();
113         }
114         return this;
115     }
116 
117     /**
118      * Increases the indent starting with the next printed line.
119      */
120     @NonNull
increaseIndent()121     public IndentingPrintWriter increaseIndent() {
122         mIndentBuilder.append(mSingleIndent);
123         mCurrentIndent = null;
124         return this;
125     }
126 
127     /**
128      * Decreases the indent starting with the next printed line.
129      */
130     @NonNull
decreaseIndent()131     public IndentingPrintWriter decreaseIndent() {
132         mIndentBuilder.delete(0, mSingleIndent.length());
133         mCurrentIndent = null;
134         return this;
135     }
136 
137     /**
138      * Prints a key-value pair.
139      */
140     @NonNull
print(@onNull String key, @Nullable Object value)141     public IndentingPrintWriter print(@NonNull String key, @Nullable Object value) {
142         String string;
143         if (value == null) {
144             string = "null";
145         } else if (value.getClass().isArray()) {
146             if (value.getClass() == boolean[].class) {
147                 string = Arrays.toString((boolean[]) value);
148             } else if (value.getClass() == byte[].class) {
149                 string = Arrays.toString((byte[]) value);
150             } else if (value.getClass() == char[].class) {
151                 string = Arrays.toString((char[]) value);
152             } else if (value.getClass() == double[].class) {
153                 string = Arrays.toString((double[]) value);
154             } else if (value.getClass() == float[].class) {
155                 string = Arrays.toString((float[]) value);
156             } else if (value.getClass() == int[].class) {
157                 string = Arrays.toString((int[]) value);
158             } else if (value.getClass() == long[].class) {
159                 string = Arrays.toString((long[]) value);
160             } else if (value.getClass() == short[].class) {
161                 string = Arrays.toString((short[]) value);
162             } else {
163                 string = Arrays.toString((Object[]) value);
164             }
165         } else {
166             string = String.valueOf(value);
167         }
168         print(key + "=" + string + " ");
169         return this;
170     }
171 
172     /**
173      * Prints a key-value pair, using hexadecimal format for the value.
174      */
175     @NonNull
printHexInt(@onNull String key, int value)176     public IndentingPrintWriter printHexInt(@NonNull String key, int value) {
177         print(key + "=0x" + Integer.toHexString(value) + " ");
178         return this;
179     }
180 
181     @Override
println()182     public void println() {
183         write('\n');
184     }
185 
186     @Override
write(int c)187     public void write(int c) {
188         mSingleChar[0] = (char) c;
189         write(mSingleChar, 0, 1);
190     }
191 
192     @Override
write(@onNull String s, int off, int len)193     public void write(@NonNull String s, int off, int len) {
194         final char[] buf = new char[len];
195         s.getChars(off, len - off, buf, 0);
196         write(buf, 0, len);
197     }
198 
199     @Override
write(@onNull char[] buf, int offset, int count)200     public void write(@NonNull char[] buf, int offset, int count) {
201         final int indentLength = mIndentBuilder.length();
202         final int bufferEnd = offset + count;
203         int lineStart = offset;
204         int lineEnd = offset;
205 
206         // March through incoming buffer looking for newlines
207         while (lineEnd < bufferEnd) {
208             char ch = buf[lineEnd++];
209             mCurrentLength++;
210             if (ch == '\n') {
211                 maybeWriteIndent();
212                 super.write(buf, lineStart, lineEnd - lineStart);
213                 lineStart = lineEnd;
214                 mEmptyLine = true;
215                 mCurrentLength = 0;
216             }
217 
218             // Wrap if we've pushed beyond line length
219             if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
220                 if (!mEmptyLine) {
221                     // Give ourselves a fresh line to work with
222                     super.write('\n');
223                     mEmptyLine = true;
224                     mCurrentLength = lineEnd - lineStart;
225                 } else {
226                     // We need more than a dedicated line, slice it hard
227                     maybeWriteIndent();
228                     super.write(buf, lineStart, lineEnd - lineStart);
229                     super.write('\n');
230                     mEmptyLine = true;
231                     lineStart = lineEnd;
232                     mCurrentLength = 0;
233                 }
234             }
235         }
236 
237         if (lineStart != lineEnd) {
238             maybeWriteIndent();
239             super.write(buf, lineStart, lineEnd - lineStart);
240         }
241     }
242 
maybeWriteIndent()243     private void maybeWriteIndent() {
244         if (mEmptyLine) {
245             mEmptyLine = false;
246             if (mIndentBuilder.length() != 0) {
247                 if (mCurrentIndent == null) {
248                     mCurrentIndent = mIndentBuilder.toString().toCharArray();
249                 }
250                 super.write(mCurrentIndent, 0, mCurrentIndent.length);
251             }
252         }
253     }
254 }
255