1 /*
2  * Copyright 2021 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 androidx.appsearch.util;
18 
19 import androidx.annotation.RestrictTo;
20 import androidx.appsearch.annotation.CanIgnoreReturnValue;
21 
22 import org.jspecify.annotations.NonNull;
23 
24 /**
25  * Utility for building indented strings.
26  *
27  * <p>This is a wrapper for {@link StringBuilder} for appending strings with indentation.
28  * The indentation level can be increased by calling {@link #increaseIndentLevel()} and decreased
29  * by calling {@link #decreaseIndentLevel()}.
30  *
31  * <p>Indentation is applied after each newline character for the given indent level.
32  *
33  * @exportToFramework:hide
34  */
35 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
36 public class IndentingStringBuilder {
37     private final StringBuilder mStringBuilder = new StringBuilder();
38 
39     // Indicates whether next non-newline character should have an indent applied before it.
40     private boolean mIndentNext = false;
41     private int mIndentLevel = 0;
42 
43     /**
44      * Increases the indent level by one for appended strings.
45      */
46     @CanIgnoreReturnValue
increaseIndentLevel()47     public @NonNull IndentingStringBuilder increaseIndentLevel() {
48         mIndentLevel++;
49         return this;
50     }
51 
52     /**
53      * Decreases the indent level by one for appended strings.
54      */
55     @CanIgnoreReturnValue
decreaseIndentLevel()56     public @NonNull IndentingStringBuilder decreaseIndentLevel() throws IllegalStateException {
57         if (mIndentLevel == 0) {
58             throw new IllegalStateException("Cannot set indent level below 0.");
59         }
60         mIndentLevel--;
61         return this;
62     }
63 
64     /**
65      * Appends provided {@code String} at the current indentation level.
66      *
67      * <p>Indentation is applied after each newline character.
68      */
69     @CanIgnoreReturnValue
append(@onNull String str)70     public @NonNull IndentingStringBuilder append(@NonNull String str) {
71         applyIndentToString(str);
72         return this;
73     }
74 
75     /**
76      * Appends provided {@code Object}, represented as a {@code String}, at the current indentation
77      * level.
78      *
79      * <p>Indentation is applied after each newline character.
80      */
81     @CanIgnoreReturnValue
append(@onNull Object obj)82     public @NonNull IndentingStringBuilder append(@NonNull Object obj) {
83         applyIndentToString(obj.toString());
84         return this;
85     }
86 
87     @Override
toString()88     public @NonNull String toString() {
89         return mStringBuilder.toString();
90     }
91 
92     /**
93      * Adds indent string to the {@link StringBuilder} instance for current indent level.
94      */
applyIndent()95     private void applyIndent() {
96         for (int i = 0; i < mIndentLevel; i++) {
97             mStringBuilder.append("  ");
98         }
99     }
100 
101     /**
102      * Applies indent, for current indent level, after each newline character.
103      *
104      * <p>Consecutive newline characters are not indented.
105      */
applyIndentToString(@onNull String str)106     private void applyIndentToString(@NonNull String str) {
107         int index = str.indexOf("\n");
108         if (index == 0) {
109             // String begins with new line character: append newline and slide past newline.
110             mStringBuilder.append("\n");
111             mIndentNext = true;
112             if (str.length() > 1) {
113                 applyIndentToString(str.substring(index + 1));
114             }
115         } else if (index >= 1) {
116             // String contains new line character: divide string between newline, append new line,
117             // and recurse on each string.
118             String beforeIndentString = str.substring(0, index);
119             applyIndentToString(beforeIndentString);
120             mStringBuilder.append("\n");
121             mIndentNext = true;
122             if (str.length() > index + 1) {
123                 String afterIndentString = str.substring(index + 1);
124                 applyIndentToString(afterIndentString);
125             }
126         } else {
127             // String does not contain newline character: append string.
128             if (mIndentNext) {
129                 applyIndent();
130                 mIndentNext = false;
131             }
132             mStringBuilder.append(str);
133         }
134     }
135 }
136