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}