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 */
018package org.apache.hadoop.tools;
019
020import java.util.ArrayList;
021import java.util.LinkedList;
022
023import org.apache.commons.lang.StringUtils;
024import org.apache.commons.lang.WordUtils;
025import 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
039public 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}