001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.hadoop.hdfs.tools;
019    
020    import java.util.ArrayList;
021    import java.util.LinkedList;
022    
023    import org.apache.commons.lang.StringUtils;
024    import org.apache.commons.lang.WordUtils;
025    import org.apache.hadoop.classification.InterfaceAudience;
026    
027    /**
028     * This class implements a "table listing" with column headers.
029     * 
030     * Example:
031     * 
032     * NAME   OWNER   GROUP   MODE       WEIGHT
033     * pool1  andrew  andrew  rwxr-xr-x     100
034     * pool2  andrew  andrew  rwxr-xr-x     100
035     * pool3  andrew  andrew  rwxr-xr-x     100
036     * 
037     */
038    @InterfaceAudience.Private
039    public class TableListing {
040      public enum Justification {
041        LEFT,
042        RIGHT;
043      }
044    
045      private static class Column {
046        private final ArrayList<String> rows;
047        private final Justification justification;
048        private final boolean wrap;
049    
050        private int wrapWidth = Integer.MAX_VALUE;
051        private int maxWidth;
052    
053        Column(String title, Justification justification, boolean wrap) {
054          this.rows = new ArrayList<String>();
055          this.justification = justification;
056          this.wrap = wrap;
057          this.maxWidth = 0;
058          addRow(title);
059        }
060    
061        private void addRow(String val) {
062          if (val == null) {
063            val = "";
064          }
065          if ((val.length() + 1) > maxWidth) {
066            maxWidth = val.length() + 1;
067          }
068          // Ceiling at wrapWidth, because it'll get wrapped
069          if (maxWidth > wrapWidth) {
070            maxWidth = wrapWidth;
071          }
072          rows.add(val);
073        }
074    
075        private int getMaxWidth() {
076          return maxWidth;
077        }
078    
079        private void setWrapWidth(int width) {
080          wrapWidth = width;
081          // Ceiling the maxLength at wrapWidth
082          if (maxWidth > wrapWidth) {
083            maxWidth = wrapWidth;
084          }
085          // Else we need to traverse through and find the real maxWidth
086          else {
087            maxWidth = 0;
088            for (int i=0; i<rows.size(); i++) {
089              int length = rows.get(i).length();
090              if (length > maxWidth) {
091                maxWidth = length;
092              }
093            }
094          }
095        }
096    
097        /**
098         * Return the ith row of the column as a set of wrapped strings, each at
099         * most wrapWidth in length.
100         */
101        String[] getRow(int idx) {
102          String raw = rows.get(idx);
103          // Line-wrap if it's too long
104          String[] lines = new String[] {raw};
105          if (wrap) {
106            lines = WordUtils.wrap(lines[0], wrapWidth, "\n", true).split("\n");
107          }
108          for (int i=0; i<lines.length; i++) {
109            if (justification == Justification.LEFT) {
110              lines[i] = StringUtils.rightPad(lines[i], maxWidth);
111            } else if (justification == Justification.RIGHT) {
112              lines[i] = StringUtils.leftPad(lines[i], maxWidth);
113            }
114          }
115          return lines;
116        }
117      }
118    
119      public static class Builder {
120        private final LinkedList<Column> columns = new LinkedList<Column>();
121        private boolean showHeader = true;
122        private int wrapWidth = Integer.MAX_VALUE;
123    
124        /**
125         * Create a new Builder.
126         */
127        public Builder() {
128        }
129    
130        public Builder addField(String title) {
131          return addField(title, Justification.LEFT, false);
132        }
133    
134        public Builder addField(String title, Justification justification) {
135          return addField(title, justification, false);
136        }
137    
138        public Builder addField(String title, boolean wrap) {
139          return addField(title, Justification.LEFT, wrap);
140        }
141    
142        /**
143         * Add a new field to the Table under construction.
144         * 
145         * @param title Field title.
146         * @param justification Right or left justification. Defaults to left.
147         * @param wrap Width at which to auto-wrap the content of the cell.
148         *        Defaults to Integer.MAX_VALUE.
149         * @return This Builder object
150         */
151        public Builder addField(String title, Justification justification, 
152            boolean wrap) {
153          columns.add(new Column(title, justification, wrap));
154          return this;
155        }
156    
157        /**
158         * Whether to hide column headers in table output
159         */
160        public Builder hideHeaders() {
161          this.showHeader = false;
162          return this;
163        }
164    
165        /**
166         * Whether to show column headers in table output. This is the default.
167         */
168        public Builder showHeaders() {
169          this.showHeader = true;
170          return this;
171        }
172    
173        /**
174         * Set the maximum width of a row in the TableListing. Must have one or
175         * more wrappable fields for this to take effect.
176         */
177        public Builder wrapWidth(int width) {
178          this.wrapWidth = width;
179          return this;
180        }
181    
182        /**
183         * Create a new TableListing.
184         */
185        public TableListing build() {
186          return new TableListing(columns.toArray(new Column[0]), showHeader,
187              wrapWidth);
188        }
189      }
190    
191      private final Column columns[];
192    
193      private int numRows;
194      private final boolean showHeader;
195      private final int wrapWidth;
196    
197      TableListing(Column columns[], boolean showHeader, int wrapWidth) {
198        this.columns = columns;
199        this.numRows = 0;
200        this.showHeader = showHeader;
201        this.wrapWidth = wrapWidth;
202      }
203    
204      /**
205       * Add a new row.
206       *
207       * @param row    The row of objects to add-- one per column.
208       */
209      public void addRow(String... row) {
210        if (row.length != columns.length) {
211          throw new RuntimeException("trying to add a row with " + row.length +
212                " columns, but we have " + columns.length + " columns.");
213        }
214        for (int i = 0; i < columns.length; i++) {
215          columns[i].addRow(row[i]);
216        }
217        numRows++;
218      }
219    
220      @Override
221      public String toString() {
222        StringBuilder builder = new StringBuilder();
223        // Calculate the widths of each column based on their maxWidths and
224        // the wrapWidth for the entire table
225        int width = (columns.length-1)*2; // inter-column padding
226        for (int i=0; i<columns.length; i++) {
227          width += columns[i].maxWidth;
228        }
229        // Decrease the column size of wrappable columns until the goal width
230        // is reached, or we can't decrease anymore
231        while (width > wrapWidth) {
232          boolean modified = false;
233          for (int i=0; i<columns.length; i++) {
234            Column column = columns[i];
235            if (column.wrap) {
236              int maxWidth = column.getMaxWidth();
237              if (maxWidth > 4) {
238                column.setWrapWidth(maxWidth-1);
239                modified = true;
240                width -= 1;
241                if (width <= wrapWidth) {
242                  break;
243                }
244              }
245            }
246          }
247          if (!modified) {
248            break;
249          }
250        }
251    
252        int startrow = 0;
253        if (!showHeader) {
254          startrow = 1;
255        }
256        String[][] columnLines = new String[columns.length][];
257        for (int i = startrow; i < numRows + 1; i++) {
258          int maxColumnLines = 0;
259          for (int j = 0; j < columns.length; j++) {
260            columnLines[j] = columns[j].getRow(i);
261            if (columnLines[j].length > maxColumnLines) {
262              maxColumnLines = columnLines[j].length;
263            }
264          }
265    
266          for (int c = 0; c < maxColumnLines; c++) {
267            // First column gets no left-padding
268            String prefix = "";
269            for (int j = 0; j < columns.length; j++) {
270              // Prepend padding
271              builder.append(prefix);
272              prefix = " ";
273              if (columnLines[j].length > c) {
274                builder.append(columnLines[j][c]);
275              } else {
276                builder.append(StringUtils.repeat(" ", columns[j].maxWidth));
277              }
278            }
279            builder.append("\n");
280          }
281        }
282        return builder.toString();
283      }
284    }