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 package com.android.tradefed.util;
17 
18 import java.util.LinkedList;
19 import java.util.List;
20 import java.util.stream.IntStream;
21 
22 /** Helper class to display a matrix of String elements in a table. */
23 public class TableBuilder {
24     /*
25      *  Sample:
26      *
27      *  +======================Metric Regressions=======================+
28      *  | Metric Name | Pre Avg | Post Avg | False Positive Probability |
29      *  +===============================================================+
30      *  | Run Metrics (1 compared, 0 changed)                           |
31      *  +---------------------------------------------------------------+
32      *  | Test Metrics (9 compared, 3 changed)                          |
33      *  +---------------------------------------------------------------+
34      *  | > com.android.uiautomator.samples.skeleton.DemoTestCase#testD |
35      *  | emo3                                                          |
36      *  | stat1       | 100.54  | 817.40   | 0.101                      |
37      *  |                                                               |
38      *  | > com.android.uiautomator.samples.skeleton.DemoTestCase#testD |
39      *  | emo2                                                          |
40      *  | stat3       | 12.32   | 100.91   | 0.101                      |
41      *  | stat4       | 104.46  | 24.61    | 0.101                      |
42      *  |                                                               |
43      *  +===============================================================+
44      */
45     // Blank space on the left of the table.
46     private int mTableOffset = 2;
47     // Padding on left & right side of each column.
48     private int mPadding = 1;
49     private final int mColumns;
50     private int mRowLength;
51     private int[] mColumnWidth;
52 
53     private List<Line> mTable;
54     private StringBuilder mBuilder;
55 
56     private interface Line {
build()57         void build();
58     }
59 
60     private class Separator implements Line {
61         char mEnd;
62         char mPipe;
63         String mText;
64 
Separator(char end, char pipe, String text)65         Separator(char end, char pipe, String text) {
66             mEnd = end;
67             mPipe = pipe;
68             mText = text;
69         }
70 
71         @Override
build()72         public void build() {
73             addPadding(mTableOffset);
74             mBuilder.append(mEnd);
75             int blanks = mRowLength - mText.length() - 2;
76             for (int i = 0; i < blanks / 2; i++) {
77                 mBuilder.append(mPipe);
78             }
79             mBuilder.append(mText);
80             for (int i = 0; i < blanks / 2; i++) {
81                 mBuilder.append(mPipe);
82             }
83             // Correct for odd length
84             if (blanks % 2 != 0) {
85                 mBuilder.append(mPipe);
86             }
87             mBuilder.append(mEnd).append('\n');
88         }
89     }
90 
91     private class SingleColumn implements Line {
92         String mText;
93 
SingleColumn(String text)94         SingleColumn(String text) {
95             mText = text;
96         }
97 
98         @Override
build()99         public void build() {
100             int width = mRowLength - 2 - mPadding * 2; // Deduct boundaries
101             String text = mText;
102             while (text != null) {
103                 addPadding(mTableOffset);
104                 mBuilder.append('|');
105                 addPadding(mPadding);
106                 if (text.length() > width) {
107                     mBuilder.append(text.substring(0, width));
108                     text = text.substring(width);
109                 } else {
110                     mBuilder.append(text);
111                     addPadding(width - text.length());
112                     text = null;
113                 }
114                 addPadding(mPadding);
115                 mBuilder.append("|\n");
116             }
117         }
118     }
119 
120     private class MultiColumn implements Line {
121         String[] mColumns;
122 
MultiColumn(String[] columns)123         MultiColumn(String[] columns) {
124             mColumns = columns;
125         }
126 
127         @Override
build()128         public void build() {
129             addPadding(mTableOffset);
130             mBuilder.append("|");
131             for (int i = 0; i < mColumns.length; i++) {
132                 addPadding(mPadding);
133                 mBuilder.append(mColumns[i]);
134                 addPadding(mColumnWidth[i] - mColumns[i].length() + mPadding);
135                 mBuilder.append('|');
136             }
137             mBuilder.append('\n');
138         }
139     }
140 
141     /**
142      * Constructs a TableBuilder with specific number of columns.
143      *
144      * @param numColumns number of columns in this table.
145      */
TableBuilder(int numColumns)146     public TableBuilder(int numColumns) {
147         mColumns = numColumns;
148         mColumnWidth = new int[numColumns];
149         mTable = new LinkedList<>();
150     }
151 
152     /**
153      * Sets the number of white space before and after each column element
154      *
155      * @param padding the number of white space
156      * @return this
157      */
setPadding(int padding)158     public TableBuilder setPadding(int padding) {
159         mPadding = padding;
160         return this;
161     }
162 
163     /**
164      * Sets the number of white space on the left of the whole table
165      *
166      * @param offset the number of white space
167      * @return this
168      */
setOffset(int offset)169     public TableBuilder setOffset(int offset) {
170         mTableOffset = offset;
171         return this;
172     }
173 
174     /**
175      * Adds a title to this table. Sample: +======================TITLE=======================+
176      *
177      * @param title title
178      * @return this
179      */
addTitle(String title)180     public TableBuilder addTitle(String title) {
181         mTable.add(new Separator('+', '=', title));
182         return this;
183     }
184 
185     /**
186      * Adds a row separator like: +---------------------------------------------+
187      *
188      * @return this
189      */
addSingleLineSeparator()190     public TableBuilder addSingleLineSeparator() {
191         return addSeparator('+', '-');
192     }
193 
194     /**
195      * Adds a row separator like: +=============================================+
196      *
197      * @return this
198      */
addDoubleLineSeparator()199     public TableBuilder addDoubleLineSeparator() {
200         return addSeparator('+', '=');
201     }
202 
203     /**
204      * Adds a row separator like: | | (blank space between two pipes)
205      *
206      * @return this
207      */
addBlankLineSeparator()208     public TableBuilder addBlankLineSeparator() {
209         return addSeparator('|', ' ');
210     }
211 
212     /**
213      * Adds a custom row separator.
214      *
215      * @param end the two end character.
216      * @param pipe the character connecting two ends
217      * @return this
218      */
addSeparator(char end, char pipe)219     public TableBuilder addSeparator(char end, char pipe) {
220         mTable.add(new Separator(end, pipe, ""));
221         return this;
222     }
223 
224     /**
225      * Adds a single long line. TableBuilder will wrap it if it is too long. See example above.
226      *
227      * @param line the line.
228      * @return this
229      */
addLine(String line)230     public TableBuilder addLine(String line) {
231         mTable.add(new SingleColumn(line));
232         return this;
233     }
234 
235     /**
236      * Adds a line. The number of columns in line must equal numColumns provided in the constructor.
237      *
238      * @param line the line.
239      * @return this
240      * @throws IllegalArgumentException when the number of columns in line does not agree with
241      *     numColumns provided in the constructor.
242      */
addLine(String[] line)243     public TableBuilder addLine(String[] line) {
244         if (line.length != mColumns) {
245             throw new IllegalArgumentException(
246                     String.format("Expect %d columns, actual %d columns", mColumns, line.length));
247         }
248         mTable.add(new MultiColumn(line));
249         for (int i = 0; i < mColumns; i++) {
250             if (line[i].length() > mColumnWidth[i]) {
251                 mColumnWidth[i] = line[i].length();
252             }
253         }
254         return this;
255     }
256 
257     /**
258      * Builds the table and return as a string.
259      *
260      * @return the table in string format.
261      */
build()262     public String build() {
263         mBuilder = new StringBuilder();
264         mRowLength = IntStream.of(mColumnWidth).map(i -> i + 2 * mPadding + 1).sum() + 1;
265         for (Line line : mTable) {
266             line.build();
267         }
268         return mBuilder.toString();
269     }
270 
addPadding(int padding)271     private void addPadding(int padding) {
272         for (int i = 0; i < padding; i++) {
273             mBuilder.append(' ');
274         }
275     }
276 }
277