diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/cli/QueueCLI.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/cli/QueueCLI.java index c8d71514e2..2df7aeb8d0 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/cli/QueueCLI.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/cli/QueueCLI.java @@ -23,6 +23,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; +import java.util.Arrays; import java.util.List; import java.util.Set; @@ -36,6 +37,7 @@ import org.apache.hadoop.util.ToolRunner; import org.apache.hadoop.yarn.api.records.NodeLabel; import org.apache.hadoop.yarn.api.records.QueueInfo; +import org.apache.hadoop.yarn.client.util.FormattingCLIUtils; import org.apache.hadoop.yarn.exceptions.YarnException; import org.apache.hadoop.classification.VisibleForTesting; @@ -222,27 +224,20 @@ private void printQueueInfo(PrintWriter writer, QueueInfo queueInfo) { } private void printQueueInfos(PrintWriter writer, List queueInfos) { - writer.print(queueInfos.size() + " queues were found : \n"); - writer.print("Queue Name\tQueue Path\tState\tCapacity\tCurrent Capacity" + - "\tMaximum Capacity\tWeight\tMaximum Parallel Apps\n"); + String titleString = queueInfos.size() + " queues were found"; + List headerStrings = Arrays.asList("Queue Name", "Queue Path", "State", "Capacity", + "Current Capacity", "Maximum Capacity", "Weight", "Maximum Parallel Apps"); + FormattingCLIUtils formattingCLIUtils = new FormattingCLIUtils(titleString) + .addHeaders(headerStrings); + DecimalFormat df = new DecimalFormat("#.00"); for (QueueInfo queueInfo : queueInfos) { - writer.print(queueInfo.getQueueName()); - writer.print("\t"); - writer.print(queueInfo.getQueuePath()); - writer.print("\t"); - writer.print(queueInfo.getQueueState()); - DecimalFormat df = new DecimalFormat("#.00"); - writer.print("\t"); - writer.print(df.format(queueInfo.getCapacity() * 100) + "%"); - writer.print("\t"); - writer.print(df.format(queueInfo.getCurrentCapacity() * 100) + "%"); - writer.print("\t"); - writer.print(df.format(queueInfo.getMaximumCapacity() * 100) + "%"); - writer.print("\t"); - writer.print(df.format(queueInfo.getWeight())); - writer.print("\t"); - writer.print(queueInfo.getMaxParallelApps()); - writer.print("\n"); + formattingCLIUtils.addLine(queueInfo.getQueueName(), queueInfo.getQueuePath(), + queueInfo.getQueueState(), df.format(queueInfo.getCapacity() * 100) + "%", + df.format(queueInfo.getCurrentCapacity() * 100) + "%", + df.format(queueInfo.getMaximumCapacity() * 100) + "%", + df.format(queueInfo.getWeight()), + queueInfo.getMaxParallelApps()); } + writer.print(formattingCLIUtils.render()); } } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/util/FormattingCLIUtils.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/util/FormattingCLIUtils.java new file mode 100644 index 0000000000..19fe3927ab --- /dev/null +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/util/FormattingCLIUtils.java @@ -0,0 +1,277 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.yarn.client.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The main core class that generates the ASCII TABLE. + */ +public final class FormattingCLIUtils { + /** Table title. */ + private String title; + /** Last processed row type. */ + private TableRowType lastTableRowType; + /** StringBuilder object used to concatenate strings. */ + private StringBuilder join; + /** An ordered Map that holds each row of data. */ + private List tableRows; + /** Maps the maximum length of each column. */ + private Map maxColMap; + + /** + * Contains the title constructor. + * @param title titleName + */ + public FormattingCLIUtils(String title) { + this.init(); + this.title = title; + } + + /** + * Initialize the data. + */ + private void init() { + this.join = new StringBuilder(); + this.tableRows = new ArrayList<>(); + this.maxColMap = new HashMap<>(); + } + + /** + * Adds elements from the collection to the header data in the table. + * @param headers Header data + * @return FormattingCLIUtils object + */ + public FormattingCLIUtils addHeaders(List headers) { + return this.appendRows(TableRowType.HEADER, headers.toArray()); + } + + /** + * Adds a row of normal data to the table. + * @param objects Common row data + * @return FormattingCLIUtils object + */ + public FormattingCLIUtils addLine(Object... objects) { + return this.appendRows(TableRowType.LINE, objects); + } + + /** + * Adds the middle row of data to the table. + * @param tableRowType TableRowType + * @param objects Table row data + * @return FormattingCLIUtils object + */ + private FormattingCLIUtils appendRows(TableRowType tableRowType, Object... objects) { + if (objects != null && objects.length > 0) { + int len = objects.length; + if (this.maxColMap.size() > len) { + throw new IllegalArgumentException("The number of columns that inserted a row " + + "of data into the table is different from the number of previous columns, check!"); + } + List lines = new ArrayList<>(); + for (int i = 0; i < len; i++) { + Object o = objects[i]; + String value = o == null ? "null" : o.toString(); + lines.add(value); + Integer maxColSize = this.maxColMap.get(i); + if (maxColSize == null) { + this.maxColMap.put(i, value.length()); + continue; + } + if (value.length() > maxColSize) { + this.maxColMap.put(i, value.length()); + } + } + this.tableRows.add(new TableRow(tableRowType, lines)); + } + return this; + } + + /** + * Builds the string for the row of the table title. + */ + private void buildTitle() { + if (this.title != null) { + int maxTitleSize = 0; + for (Integer maxColSize : this.maxColMap.values()) { + maxTitleSize += maxColSize; + } + maxTitleSize += 3 * (this.maxColMap.size() - 1); + if (this.title.length() > maxTitleSize) { + this.title = this.title.substring(0, maxTitleSize); + } + this.join.append("+"); + for (int i = 0; i < maxTitleSize + 2; i++) { + this.join.append("-"); + } + this.join.append("+\n") + .append("|") + .append(StrUtils.center(this.title, maxTitleSize + 2, ' ')) + .append("|\n"); + this.lastTableRowType = TableRowType.TITLE; + } + } + + /** + * Build the table, first build the title, and then walk through each row of data to build. + */ + private void buildTable() { + this.buildTitle(); + for (int i = 0, len = this.tableRows.size(); i < len; i++) { + List data = this.tableRows.get(i).data; + switch (this.tableRows.get(i).tableRowType) { + case HEADER: + if (this.lastTableRowType != TableRowType.HEADER) { + this.buildRowBorder(data); + } + this.buildRowLine(data); + this.buildRowBorder(data); + break; + case LINE: + this.buildRowLine(data); + if (i == len - 1) { + this.buildRowBorder(data); + } + break; + default: + break; + } + } + } + + /** + * Method to build a border row. + * @param data dataLine + */ + private void buildRowBorder(List data) { + this.join.append("+"); + for (int i = 0, len = data.size(); i < len; i++) { + for (int j = 0; j < this.maxColMap.get(i) + 2; j++) { + this.join.append("-"); + } + this.join.append("+"); + } + this.join.append("\n"); + } + + /** + * A way to build rows of data. + * @param data dataLine + */ + private void buildRowLine(List data) { + this.join.append("|"); + for (int i = 0, len = data.size(); i < len; i++) { + this.join.append(StrUtils.center(data.get(i), this.maxColMap.get(i) + 2, ' ')) + .append("|"); + } + this.join.append("\n"); + } + + /** + * Rendering is born as a result. + * @return ASCII string of Table + */ + public String render() { + this.buildTable(); + return this.join.toString(); + } + + /** + * The type of each table row and the entity class of the data. + */ + private static class TableRow { + private TableRowType tableRowType; + private List data; + TableRow(TableRowType tableRowType, List data) { + this.tableRowType = tableRowType; + this.data = data; + } + } + + /** + * An enumeration class that distinguishes between table headers and normal table data. + */ + private enum TableRowType { + TITLE, HEADER, LINE + } + + /** + * String utility class. + */ + private static final class StrUtils { + /** + * Puts a string in the middle of a given size. + * @param str Character string + * @param size Total size + * @param padChar Fill character + * @return String result + */ + private static String center(String str, int size, char padChar) { + if (str != null && size > 0) { + int strLen = str.length(); + int pads = size - strLen; + if (pads > 0) { + str = leftPad(str, strLen + pads / 2, padChar); + str = rightPad(str, size, padChar); + } + } + return str; + } + + /** + * Left-fill the given string and size. + * @param str String + * @param size totalSize + * @param padChar Fill character + * @return String result + */ + private static String leftPad(final String str, int size, char padChar) { + int pads = size - str.length(); + return pads <= 0 ? str : repeat(padChar, pads).concat(str); + } + + /** + * Right-fill the given string and size. + * @param str String + * @param size totalSize + * @param padChar Fill character + * @return String result + */ + private static String rightPad(final String str, int size, char padChar) { + int pads = size - str.length(); + return pads <= 0 ? str : str.concat(repeat(padChar, pads)); + } + + /** + * Re-fill characters as strings. + * @param ch String + * @param repeat Number of repeats + * @return String + */ + private static String repeat(char ch, int repeat) { + char[] buf = new char[repeat]; + for (int i = repeat - 1; i >= 0; i--) { + buf[i] = ch; + } + return new String(buf); + } + } +} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java index 469cb2f72f..0558edae50 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java @@ -40,6 +40,7 @@ import java.io.UnsupportedEncodingException; import java.text.DecimalFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.EnumSet; @@ -82,6 +83,7 @@ import org.apache.hadoop.yarn.api.records.YarnApplicationAttemptState; import org.apache.hadoop.yarn.api.records.YarnApplicationState; import org.apache.hadoop.yarn.client.api.YarnClient; +import org.apache.hadoop.yarn.client.util.FormattingCLIUtils; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.exceptions.ApplicationAttemptNotFoundException; import org.apache.hadoop.yarn.exceptions.ApplicationNotFoundException; @@ -1774,28 +1776,21 @@ public void testGetQueueInfos() throws Exception { verify(client).getAllQueues(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintWriter writer = new PrintWriter(baos); - writer.print(queueInfos.size() + " queues were found : \n"); - writer.print("Queue Name\tQueue Path\tState\tCapacity\tCurrent Capacity\t" + - "Maximum Capacity\tWeight\tMaximum Parallel Apps\n"); + String titleString = queueInfos.size() + " queues were found"; + List headerStrings = Arrays.asList("Queue Name", "Queue Path", "State", "Capacity", + "Current Capacity", "Maximum Capacity", "Weight", "Maximum Parallel Apps"); + FormattingCLIUtils formattingCLIUtils = new FormattingCLIUtils(titleString) + .addHeaders(headerStrings); + DecimalFormat df = new DecimalFormat("#.00"); for (QueueInfo queueInfoe : queueInfos) { - writer.print(queueInfoe.getQueueName()); - writer.print("\t"); - writer.print(queueInfoe.getQueuePath()); - writer.print("\t"); - writer.print(queueInfoe.getQueueState()); - DecimalFormat df = new DecimalFormat("#.00"); - writer.print("\t"); - writer.print(df.format(queueInfoe.getCapacity() * 100) + "%"); - writer.print("\t"); - writer.print(df.format(queueInfoe.getCurrentCapacity() * 100) + "%"); - writer.print("\t"); - writer.print(df.format(queueInfoe.getMaximumCapacity() * 100) + "%"); - writer.print("\t"); - writer.print(df.format(queueInfoe.getWeight())); - writer.print("\t"); - writer.print(queueInfoe.getMaxParallelApps()); - writer.print("\n"); + formattingCLIUtils.addLine(queueInfoe.getQueueName(), queueInfoe.getQueuePath(), + queueInfoe.getQueueState(), df.format(queueInfoe.getCapacity() * 100) + "%", + df.format(queueInfoe.getCurrentCapacity() * 100) + "%", + df.format(queueInfoe.getMaximumCapacity() * 100) + "%", + df.format(queueInfoe.getWeight()), + queueInfoe.getMaxParallelApps()); } + writer.print(formattingCLIUtils.render()); writer.close(); String queueInfoStr = baos.toString("UTF-8"); Assert.assertEquals(queueInfoStr, sysOutStream.toString());