From ba3e2ca5cc9798644324e029046ca13c0177054c Mon Sep 17 00:00:00 2001 From: Clebert Suconic Date: Wed, 9 Sep 2026 18:01:35 -0400 Subject: [PATCH] ARTEMIS-6242 Improve TableOut output Assisted by Bob --- .../artemis/cli/commands/lock/LockList.java | 8 +- .../artemis/cli/commands/queue/StatQueue.java | 20 +- .../artemis/cli/commands/tools/PrintData.java | 57 +++-- .../apache/activemq/cli/test/ArtemisTest.java | 225 ++++++++++++------ .../cli/test/MessageSerializerTest.java | 4 +- .../activemq/artemis/utils/TableOut.java | 104 ++++++-- .../activemq/artemis/utils/TableOutTest.java | 130 ++++++++-- .../impl/journal/DescribeJournal.java | 25 +- docs/user-manual/versions.adoc | 7 + .../tests/db/paging/PrintDataTest.java | 2 +- 10 files changed, 444 insertions(+), 138 deletions(-) diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/lock/LockList.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/lock/LockList.java index 9ae8152317f0..f2df89ef1dab 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/lock/LockList.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/lock/LockList.java @@ -36,6 +36,9 @@ public class LockList extends ConnectionAbstract { @CommandLine.Option(names = "--sleep", description = "Monitor locks continuously by repeating the command at the specified interval in milliseconds. Use -1 to disable (default).", defaultValue = "-1") private long sleep = -1; + @CommandLine.Option(names = "--ascii", description = "Use ASCII table output style") + private boolean ascii = false; + @Override public Object execute(ActionContext context) throws Exception { super.execute(context); @@ -93,10 +96,12 @@ private void printStats(ActionContext context, JsonArray array) { columnSizes[2] = Math.max(columnSizes[2], lock.getString("status", "").length()); } - TableOut tableOut = new TableOut("|", 2, columnSizes); + TableOut tableOut = new TableOut("|", 2, columnSizes).setAscii(ascii); // Print header + tableOut.printTopSeparator(context.out); tableOut.print(context.out, fieldTitles, centralize); + tableOut.printSeparator(context.out); // Print data rows for (int i = 0; i < array.size(); i++) { @@ -108,6 +113,7 @@ private void printStats(ActionContext context, JsonArray array) { }; tableOut.print(context.out, columns, centralize); } + tableOut.printBottomSeparator(context.out); context.out.println(); } diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/queue/StatQueue.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/queue/StatQueue.java index 6bb42bb60b4b..100f711f79e2 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/queue/StatQueue.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/queue/StatQueue.java @@ -33,6 +33,7 @@ import org.apache.activemq.artemis.json.JsonArray; import org.apache.activemq.artemis.json.JsonObject; import org.apache.activemq.artemis.utils.TableOut; +import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @@ -119,6 +120,18 @@ public enum OPERATION { @Option(names = "--json", description = "Prints the queue stat information in JSON format, useful for scripts") private boolean json; + @CommandLine.Option(names = "--ascii", description = "Use ASCII table output style") + private boolean ascii = false; + + public boolean isAscii() { + return ascii; + } + + public StatQueue setAscii(boolean ascii) { + this.ascii = ascii; + return this; + } + public boolean isSingleLineHeader() { return singleLineHeader; } @@ -340,12 +353,16 @@ private void printStats(String result) { getColumnSizes(array.getJsonObject(i), columnSizes); } - TableOut tableOut = new TableOut("|", 2, columnSizes); + TableOut tableOut = new TableOut("|", 2, columnSizes).setAscii(ascii); if (singleLineHeader) { printHeadings(columnSizes); } else { + tableOut.printTopSeparator(getActionContext().out); tableOut.print(getActionContext().out, fieldTitles, centralize); + if (!ascii) { + tableOut.printSeparator(getActionContext().out); + } } for (int i = 0; i < array.size(); i++) { @@ -355,6 +372,7 @@ private void printStats(String result) { printQueueStats(array.getJsonObject(i), columnSizes, centralize, tableOut); statCount++; } + tableOut.printBottomSeparator(getActionContext().out); if (count > maxRows) { getActionContext().out.println(String.format("WARNING: the displayed queues are %d/%d, set maxRows to display more queues.", maxRows, count)); diff --git a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/tools/PrintData.java b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/tools/PrintData.java index 0b244387c5b6..41d968117c16 100644 --- a/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/tools/PrintData.java +++ b/artemis-cli/src/main/java/org/apache/activemq/artemis/cli/commands/tools/PrintData.java @@ -63,6 +63,7 @@ import org.apache.activemq.artemis.utils.actors.ArtemisExecutor; import org.apache.activemq.artemis.utils.collections.LinkedList; import org.apache.activemq.artemis.utils.collections.LinkedListIterator; +import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @@ -84,8 +85,8 @@ public class PrintData extends DBOption { @Option(names = "--skip-journal", description = "Do not print data from the messages journal.") private boolean skipJournal = false; - @Option(names = "--legacy", description = "Use legacy semicolon-separated output format instead of table columns.") - private boolean legacyOutput = false; + @CommandLine.Option(names = "--ascii", description = "Use ASCII table output style") + private boolean ascii = false; private static final String BINDINGS_BANNER = "B I N D I N G S J O U R N A L"; private static final String MESSAGES_BANNER = "M E S S A G E S J O U R N A L"; @@ -93,12 +94,12 @@ public class PrintData extends DBOption { MessagePersister.registerPersister(CoreMessagePersister.getInstance()); } - public boolean isLegacyOutput() { - return legacyOutput; + public boolean isAscii() { + return ascii; } - public PrintData setLegacyOutput(boolean legacyOutput) { - this.legacyOutput = legacyOutput; + public PrintData setAscii(boolean ascii) { + this.ascii = ascii; return this; } @@ -112,7 +113,7 @@ public Object execute(ActionContext context) throws Exception { if (configuration.isJDBC()) { printDataJDBC(configuration, context.out); } else { - printData(new File(getBinding()), new File(getJournal()), new File(getPaging()), context.out, safe, reclaimed, skipBindings, skipJournal, maxPages, legacyOutput); + printData(new File(getBinding()), new File(getJournal()), new File(getPaging()), context.out, safe, reclaimed, skipBindings, skipJournal, maxPages, ascii); } } catch (Exception e) { treatError(e, "data", "print"); @@ -130,13 +131,13 @@ public void printDataJDBC(Configuration configuration, PrintStream out) throws E printBanner(out, BINDINGS_BANNER); - DescribeJournal bindings = DescribeJournal.printSurvivingRecords(storageManager.getBindingsJournal(), out, safe, legacyOutput); + DescribeJournal bindings = DescribeJournal.printSurvivingRecords(storageManager.getBindingsJournal(), out, safe, ascii); printBanner(out, MESSAGES_BANNER); - DescribeJournal describeJournal = DescribeJournal.printSurvivingRecords(storageManager.getMessageJournal(), out, safe, legacyOutput); + DescribeJournal describeJournal = DescribeJournal.printSurvivingRecords(storageManager.getMessageJournal(), out, safe, ascii); - printPages(describeJournal, storageManager, pagingmanager, out, safe, maxPages, bindings, legacyOutput); + printPages(describeJournal, storageManager, pagingmanager, out, safe, maxPages, bindings, ascii); cleanup(); @@ -162,7 +163,7 @@ public static void printData(File bindingsDirectory, File messagesDirectory, Fil printData(bindingsDirectory, messagesDirectory, pagingDirectory, out, safe, reclaimed, skipBindings, skipJournal, maxPages, false); } - public static void printData(File bindingsDirectory, File messagesDirectory, File pagingDirectory, PrintStream out, boolean safe, boolean reclaimed, boolean skipBindings, boolean skipJournal, int maxPages, boolean legacyOutput) throws Exception { + public static void printData(File bindingsDirectory, File messagesDirectory, File pagingDirectory, PrintStream out, boolean safe, boolean reclaimed, boolean skipBindings, boolean skipJournal, int maxPages, boolean ascii) throws Exception { // printing the banner and version Artemis.printBanner(out); @@ -186,7 +187,7 @@ public static void printData(File bindingsDirectory, File messagesDirectory, Fil out.println(); bindingsDescribe = null; } else { - bindingsDescribe = printBindings(bindingsDirectory, out, safe, true, true, reclaimed, legacyOutput); + bindingsDescribe = printBindings(bindingsDirectory, out, safe, true, true, reclaimed, ascii); } printBanner(out, MESSAGES_BANNER); @@ -195,7 +196,7 @@ public static void printData(File bindingsDirectory, File messagesDirectory, Fil out.println(); } DescribeJournal describeJournal = null; - describeJournal = printMessages(messagesDirectory, out, safe, !skipJournal, !skipJournal, reclaimed, legacyOutput); + describeJournal = printMessages(messagesDirectory, out, safe, !skipJournal, !skipJournal, reclaimed, ascii); if (describeJournal == null) { return; @@ -207,7 +208,7 @@ public static void printData(File bindingsDirectory, File messagesDirectory, Fil out.println(".... skipping"); out.println(); } else { - printPages(pagingDirectory, describeJournal, out, safe, maxPages, bindingsDescribe, legacyOutput); + printPages(pagingDirectory, describeJournal, out, safe, maxPages, bindingsDescribe, ascii); } } catch (Exception e) { e.printStackTrace(); @@ -220,10 +221,10 @@ public static DescribeJournal printMessages(File messagesDirectory, PrintStream return printMessages(messagesDirectory, out, safe, printRecords, printSurviving, reclaimed, false); } - public static DescribeJournal printMessages(File messagesDirectory, PrintStream out, boolean safe, boolean printRecords, boolean printSurviving, boolean reclaimed, boolean legacyOutput) { + public static DescribeJournal printMessages(File messagesDirectory, PrintStream out, boolean safe, boolean printRecords, boolean printSurviving, boolean reclaimed, boolean ascii) { DescribeJournal describeJournal; try { - describeJournal = DescribeJournal.describeMessagesJournal(messagesDirectory, out, safe, printRecords, printSurviving, reclaimed, legacyOutput); + describeJournal = DescribeJournal.describeMessagesJournal(messagesDirectory, out, safe, printRecords, printSurviving, reclaimed, ascii); } catch (Exception e) { e.printStackTrace(); return null; @@ -235,9 +236,9 @@ public static DescribeJournal printBindings(File bindingsDirectory, PrintStream return printBindings(bindingsDirectory, out, safe, printRecords, printSurviving, reclaimed, false); } - public static DescribeJournal printBindings(File bindingsDirectory, PrintStream out, boolean safe, boolean printRecords, boolean printSurviving, boolean reclaimed, boolean legacyOutput) { + public static DescribeJournal printBindings(File bindingsDirectory, PrintStream out, boolean safe, boolean printRecords, boolean printSurviving, boolean reclaimed, boolean ascii) { try { - return DescribeJournal.describeBindingsJournal(bindingsDirectory, out, safe, printRecords, printSurviving, reclaimed, legacyOutput); + return DescribeJournal.describeBindingsJournal(bindingsDirectory, out, safe, printRecords, printSurviving, reclaimed, ascii); } catch (Exception e) { e.printStackTrace(); return null; @@ -251,7 +252,7 @@ protected static void printBanner(PrintStream out, String x2) { out.println("********************************************"); } - private static void printPages(File pageDirectory, DescribeJournal describeJournal, PrintStream out, boolean safe, int maxPages, DescribeJournal bindingsDescribe, boolean legacyOutput) { + private static void printPages(File pageDirectory, DescribeJournal describeJournal, PrintStream out, boolean safe, int maxPages, DescribeJournal bindingsDescribe, boolean ascii) { ActiveMQThreadFactory daemonFactory = new ActiveMQThreadFactory("cli", true, PrintData.class.getClassLoader()); final ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(1, daemonFactory); final ExecutorService executor = Executors.newFixedThreadPool(10, daemonFactory); @@ -264,7 +265,7 @@ private static void printPages(File pageDirectory, DescribeJournal describeJourn addressSettingsRepository.setDefault(new AddressSettings()); PagingManager manager = new PagingManagerImpl(pageStoreFactory, addressSettingsRepository); - printPages(describeJournal, sm, manager, out, safe, maxPages, bindingsDescribe, legacyOutput); + printPages(describeJournal, sm, manager, out, safe, maxPages, bindingsDescribe, ascii); } catch (Exception e) { e.printStackTrace(); } finally { @@ -279,7 +280,7 @@ private static void printPages(DescribeJournal describeJournal, PrintStream out, boolean safe, int maxPages, DescribeJournal bindingsDescribe, - boolean legacyOutput) throws Exception { + boolean ascii) throws Exception { PageCursorsInfo cursorACKs = calculateCursorsInfo(describeJournal.getRecords()); Set existingQueues = new HashSet<>(); @@ -294,7 +295,7 @@ private static void printPages(DescribeJournal describeJournal, SimpleString[] stores = manager.getStoreNames(); int[] pageColumnSizes = {10, 10, 12, safe ? 10 : 100, 10}; - TableOut pageTable = legacyOutput ? null : new TableOut("|", 2, pageColumnSizes); + TableOut pageTable = ascii ? null : new TableOut("|", 2, pageColumnSizes); for (SimpleString store : stores) { PagingStore pgStore = manager.getPageStore(store); @@ -318,7 +319,7 @@ private static void printPages(DescribeJournal describeJournal, pgid++; page = pgStore.newPageObject(pgid); } - if (legacyOutput) { + if (ascii) { out.println("******* Page " + pgid); } page.open(false); @@ -328,7 +329,7 @@ private static void printPages(DescribeJournal describeJournal, int msgID = 0; if (pageTable != null) { - pageTable.printSeparator(out); + pageTable.printTopSeparator(out); pageTable.print(out, new String[]{"Page", "Msg", "TX", safe ? "Size" : "Message", "Queues"}); pageTable.printSeparator(out); } @@ -381,7 +382,7 @@ private static void printPages(DescribeJournal describeJournal, queuesStr.append(", **PG_TX_NOT_FOUND**"); } - if (legacyOutput) { + if (ascii) { if (safe) { try { out.print("pg=" + pgid + ", msg=" + msgID + ",pgTX=" + msg.getTransactionID() + ", msg=" + msg.getMessage().getClass().getSimpleName() + "(safe data, size=" + msg.getMessage().getPersistentSize() + ")"); @@ -416,8 +417,14 @@ private static void printPages(DescribeJournal describeJournal, msgID++; } + pgid++; } + + if (pageTable != null) { + pageTable.printBottomSeparator(out); + } + } } } diff --git a/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java b/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java index bdcaa40aff45..f2c437a1f4ba 100644 --- a/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java +++ b/artemis-cli/src/test/java/org/apache/activemq/cli/test/ArtemisTest.java @@ -97,6 +97,7 @@ import org.apache.activemq.artemis.utils.RandomUtil; import org.apache.activemq.artemis.utils.SensitiveDataCodec; import org.apache.activemq.artemis.utils.StringUtil; +import org.apache.activemq.artemis.utils.TableOut; import org.apache.activemq.artemis.utils.Wait; import org.apache.activemq.artemis.utils.XmlProvider; import org.apache.commons.configuration2.PropertiesConfiguration; @@ -1635,7 +1636,21 @@ public void testAutoTune() throws Exception { @Test @Timeout(60) - public void testQstat() throws Exception { + public void testQstatNewFormat() throws Exception { + testQstat(false); + } + + @Test + @Timeout(60) + public void testQstatASCII() throws Exception { + testQstat(true); + } + + private void testQstat(boolean ascii) throws Exception { + + final int dataRowIndex = ascii ? 2 : 4; + final String colSep = ascii ? "\\|" : TableOut.BOX_VERTICAL; + final int overhead = ascii ? 2 : 5; // lines added beyond N data rows File instanceQstat = new File(temporaryFolder, "instanceQStat"); setupAuth(instanceQstat); @@ -1668,22 +1683,11 @@ public void testQstat() throws Exception { Message message = consumer.receive(100); } - //check all queues containing "Test1" are displayed + //check the json output is correct, we will parse the messageCount on Queue Test20 TestActionContext context = new TestActionContext(); StatQueue statQueue = new StatQueue(); statQueue.setUser("admin"); statQueue.setPassword("admin"); - statQueue.setQueueName("Test1"); - statQueue.execute(context); - List lines = getOutputLines(context, false); - // Header line + 3 queues - assertEquals(5, lines.size(), "rows returned using queueName=Test1"); - - //check the json output is correct, we will parse the messageCount on Queue Test20 - context = new TestActionContext(); - statQueue = new StatQueue(); - statQueue.setUser("admin"); - statQueue.setPassword("admin"); statQueue.setJson(true); statQueue.execute(context); { @@ -1692,15 +1696,28 @@ public void testQstat() throws Exception { arrayQueues.stream().filter(jsonValue -> jsonValue.asJsonObject().getString("name").equals("Test20")).forEach(jsonValue -> Assertions.assertEquals(20, Integer.parseInt(jsonValue.asJsonObject().getString("messageCount")))); } + //check all queues containing "Test1" are displayed + context = new TestActionContext(); + statQueue = new StatQueue(); + statQueue.setUser("admin"); + statQueue.setPassword("admin"); + statQueue.setQueueName("Test1"); + statQueue.setAscii(ascii); + statQueue.execute(context); + List lines = getOutputLines(context, false); + // 3 queues + assertEquals(3 + overhead, lines.size(), "rows returned using queueName=Test1"); + //check all queues are displayed when no Filter set context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); statQueue.setPassword("admin"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - // Header line + 4 queues (at least - possibly other infra queues as well) - assertTrue(5 <= lines.size(), "rows returned filtering no name "); + // at least 4 queues (possibly more infra queues) + assertTrue(4 + overhead <= lines.size(), "rows returned filtering no name"); //check all queues containing "Test1" are displayed using Filter field NAME context = new TestActionContext(); @@ -1710,10 +1727,11 @@ public void testQstat() throws Exception { statQueue.setFieldName("NAME"); statQueue.setOperationName("CONTAINS"); statQueue.setValue("Test1"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - // Header line + 3 queues - assertEquals(5, lines.size(), "rows returned filtering by NAME "); + // 3 queues + assertEquals(3 + overhead, lines.size(), "rows returned filtering by NAME"); //check all queues NOT containing "management" are displayed using Filter field NAME context = new TestActionContext(); @@ -1723,10 +1741,11 @@ public void testQstat() throws Exception { statQueue.setFieldName("NAME"); statQueue.setOperationName("NOT_CONTAINS"); statQueue.setValue("management"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - // Header line + 6 queues (Test1/11/12/20+DLQ+ExpiryQueue, but not activemq.management.d6dbba78-d76f-43d6-a2c9-fc0575ed6f5d) - assertEquals(8, lines.size(), "rows returned filtering by NAME operation NOT_CONTAINS"); + // 6 queues (Test1/11/12/20+DLQ+ExpiryQueue, but not activemq.management.*) + assertEquals(6 + overhead, lines.size(), "rows returned filtering by NAME operation NOT_CONTAINS"); //check only queue named "Test1" is displayed using Filter field NAME and operation EQUALS context = new TestActionContext(); @@ -1736,13 +1755,13 @@ public void testQstat() throws Exception { statQueue.setFieldName("NAME"); statQueue.setOperationName("EQUALS"); statQueue.setValue("Test1"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - //Header line + 1 queue only - assertEquals(3, lines.size(), "rows returned filtering by NAME operation EQUALS"); + // 1 queue + assertEquals(1 + overhead, lines.size(), "rows returned filtering by NAME operation EQUALS"); //verify contents of queue stat line is correct - String queueTest1 = lines.get(2); - String[] parts = queueTest1.split("\\|"); + String[] parts = lines.get(dataRowIndex).split(colSep); assertEquals("Test1", parts[1].trim(), "queue name"); assertEquals("Test1", parts[2].trim(), "address name"); assertEquals("2", parts[3].trim(), "Consumer count"); @@ -1761,12 +1780,13 @@ public void testQstat() throws Exception { statQueue.setFieldName("ADDRESS"); statQueue.setOperationName("CONTAINS"); statQueue.setValue("Test1"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - // Header line + 3 queues - assertEquals(5, lines.size(), "rows returned filtering by ADDRESS"); + // 3 queues + assertEquals(3 + overhead, lines.size(), "rows returned filtering by ADDRESS"); - //check all queues containing address "Test1" are displayed using Filter field MESSAGE_COUNT + //check all queues filtered by MESSAGE_COUNT CONTAINS context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); @@ -1774,13 +1794,13 @@ public void testQstat() throws Exception { statQueue.setFieldName("MESSAGE_COUNT"); statQueue.setOperationName("CONTAINS"); statQueue.setValue("10"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); + // 0 queues + assertEquals(0 + overhead, lines.size(), "rows returned filtering by MESSAGE_COUNT"); - // Header line + 0 queues - assertEquals(2, lines.size(), "rows returned filtering by MESSAGE_COUNT"); - - //check all queues containing address "Test1" are displayed using Filter field MESSAGE_ADDED + //check all queues filtered by MESSAGES_ADDED CONTAINS context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); @@ -1788,12 +1808,13 @@ public void testQstat() throws Exception { statQueue.setFieldName("MESSAGES_ADDED"); statQueue.setOperationName("CONTAINS"); statQueue.setValue("20"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - // Header line + 0 queues - assertEquals(2, lines.size(), "rows returned filtering by MESSAGES_ADDED"); + // 0 queues + assertEquals(0 + overhead, lines.size(), "rows returned filtering by MESSAGES_ADDED"); - //check queues with greater_than 19 MESSAGE_ADDED displayed + //check queues with greater_than 19 MESSAGE_ADDED displayed context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); @@ -1801,13 +1822,13 @@ public void testQstat() throws Exception { statQueue.setFieldName("MESSAGES_ADDED"); statQueue.setOperationName("GREATER_THAN"); statQueue.setValue("19"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - - // Header line + 1 queues - assertEquals(3, lines.size(), "rows returned filtering by MESSAGES_ADDED"); - String[] columns = lines.get(2).split("\\|"); - assertEquals("Test20", columns[2].trim(), "queue name filtered by MESSAGES_ADDED GREATER_THAN "); + // 1 queue + assertEquals(1 + overhead, lines.size(), "rows returned filtering by MESSAGES_ADDED GREATER_THAN"); + String[] columns = lines.get(dataRowIndex).split(colSep); + assertEquals("Test20", columns[2].trim(), "queue name filtered by MESSAGES_ADDED GREATER_THAN"); //check queues with less_than 2 MESSAGE_ADDED displayed context = new TestActionContext(); @@ -1817,20 +1838,22 @@ public void testQstat() throws Exception { statQueue.setFieldName("MESSAGES_ADDED"); statQueue.setOperationName("LESS_THAN"); statQueue.setValue("2"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - - // Header line + "at least" 2 queues + // at least 2 queues assertTrue(2 <= lines.size(), "rows returned filtering by MESSAGES_ADDED LESS_THAN"); - - //walk the result returned and the specific destinations are not part of the output + //walk the result returned and verify specific destinations are not part of the output for (String line : lines) { - columns = line.split("\\|"); - assertNotEquals("Test20", columns[2].trim(), "ensure Test20 is not part of returned result"); - assertNotEquals("Test1", columns[2].trim(), "ensure Test1 is not part of returned result"); + logger.debug("line: {}", line); + columns = line.split(colSep); + if (columns.length > 2) { + assertNotEquals("Test20", columns[2].trim(), "ensure Test20 is not part of returned result"); + assertNotEquals("Test1", columns[2].trim(), "ensure Test1 is not part of returned result"); + } } - //check all queues containing address "Test1" are displayed using Filter field DELIVERING_COUNT + //check filtering by DELIVERING_COUNT context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); @@ -1838,14 +1861,14 @@ public void testQstat() throws Exception { statQueue.setFieldName("DELIVERING_COUNT"); statQueue.setOperationName("EQUALS"); statQueue.setValue("10"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - columns = lines.get(2).split("\\|"); - // Header line + 1 queues - assertEquals(3, lines.size(), "rows returned filtering by DELIVERING_COUNT"); - assertEquals("Test1", columns[2].trim(), "queue name filtered by DELIVERING_COUNT "); + // 1 queue + assertEquals(1 + overhead, lines.size(), "rows returned filtering by DELIVERING_COUNT"); + assertEquals("Test1", lines.get(dataRowIndex).split(colSep)[2].trim(), "queue name filtered by DELIVERING_COUNT"); - //check all queues containing address "Test1" are displayed using Filter field CONSUMER_COUNT + //check filtering by CONSUMER_COUNT context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); @@ -1853,14 +1876,14 @@ public void testQstat() throws Exception { statQueue.setFieldName("CONSUMER_COUNT"); statQueue.setOperationName("EQUALS"); statQueue.setValue("2"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - columns = lines.get(2).split("\\|"); - // Header line + 1 queues - assertEquals(3, lines.size(), "rows returned filtering by CONSUMER_COUNT "); - assertEquals("Test1", columns[2].trim(), "queue name filtered by CONSUMER_COUNT "); + // 1 queue + assertEquals(1 + overhead, lines.size(), "rows returned filtering by CONSUMER_COUNT"); + assertEquals("Test1", lines.get(dataRowIndex).split(colSep)[2].trim(), "queue name filtered by CONSUMER_COUNT"); - //check all queues containing address "Test1" are displayed using Filter field MESSAGE_ACKED + //check filtering by MESSAGES_ACKED context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); @@ -1868,35 +1891,37 @@ public void testQstat() throws Exception { statQueue.setFieldName("MESSAGES_ACKED"); statQueue.setOperationName("EQUALS"); statQueue.setValue("5"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - columns = lines.get(2).split("\\|"); - // Header line + 1 queues - assertEquals(3, lines.size(), "rows returned filtering by MESSAGE_ACKED "); - assertEquals("Test1", columns[2].trim(), "queue name filtered by MESSAGE_ACKED"); + // 1 queue + assertEquals(1 + overhead, lines.size(), "rows returned filtering by MESSAGES_ACKED"); + assertEquals("Test1", lines.get(dataRowIndex).split(colSep)[2].trim(), "queue name filtered by MESSAGES_ACKED"); - //check no queues are displayed when name does not match + //check no queues are displayed when name does not match context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); statQueue.setPassword("admin"); statQueue.setQueueName("no_queue_name"); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - // Header line + 0 queues - assertEquals(2, lines.size(), "rows returned by queueName for no Matching queue "); + // 0 queues + assertEquals(0 + overhead, lines.size(), "rows returned by queueName for no matching queue"); - //check maxrows is taking effect" + //check maxrows is taking effect context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); statQueue.setPassword("admin"); statQueue.setQueueName("Test1"); statQueue.setMaxRows(1); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - // Header line + 1 queue only + warning line - assertEquals(4, lines.size(), "rows returned by maxRows=1"); + // 1 queue + 1 warning line + assertEquals(1 + overhead + 1, lines.size(), "rows returned by maxRows=1"); } finally { stopServer(); @@ -1934,7 +1959,20 @@ public void testHugeQstat() throws Exception { @Test @Timeout(60) - public void testQstatColumnWidth() throws Exception { + public void testQstatColumnWidthNewFormat() throws Exception { + testQstatColumnWidth(false); + } + + @Test + @Timeout(60) + public void testQstatColumnWidthASCII() throws Exception { + testQstatColumnWidth(true); + } + + private void testQstatColumnWidth(boolean ascii) throws Exception { + + final int dataRowIndex = ascii ? 2 : 4; + final String colSep = ascii ? "\\|" : TableOut.BOX_VERTICAL; File instanceQstat = new File(temporaryFolder, "instanceQStat"); setupAuth(instanceQstat); @@ -1952,53 +1990,84 @@ public void testQstatColumnWidth() throws Exception { final String NAME = "012345678901234567890123456789"; sendMessages(session, NAME, 1); + // default maxColumnSize=25: NAME (30 chars) wraps across 3 data rows -> N=3 TestActionContext context = new TestActionContext(); StatQueue statQueue = new StatQueue(); statQueue.setUser("admin"); statQueue.setPassword("admin"); statQueue.setQueueName(NAME); + statQueue.setAscii(ascii); statQueue.execute(context); List lines = getOutputLines(context, false); - assertEquals(4, lines.size(), "rows returned"); - String[] split = lines.get(1).split("\\|"); + for (String s : lines) { + System.out.println(s); + } + if (ascii) { + assertEquals(4, lines.size(), "rows returned"); + } else { + assertEquals(7, lines.size(), "rows returned"); + } + logger.info("lines " + dataRowIndex + " = " + lines.get(dataRowIndex)); + String[] split = lines.get(dataRowIndex).split(colSep); assertEquals(StatQueue.DEFAULT_MAX_COLUMN_SIZE, split[1].length()); + // maxColumnSize=15: NAME (30 chars) wraps across 3 data rows -> N=3 context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); statQueue.setPassword("admin"); statQueue.setQueueName(NAME); statQueue.setMaxColumnSize(15); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - assertEquals(5, lines.size(), "rows returned"); - split = lines.get(1).split("\\|"); + for (String s : lines) { + System.out.println(s); + } + if (ascii) { + assertEquals(5, lines.size(), "rows returned"); + } else { + assertEquals(8, lines.size(), "rows returned"); + } + split = lines.get(dataRowIndex).split(colSep); assertEquals(15, split[1].length()); + // maxColumnSize=50: NAME (30 chars) fits in 1 data row -> N=1 context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); statQueue.setPassword("admin"); statQueue.setQueueName(NAME); statQueue.setMaxColumnSize(50); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); - assertEquals(3, lines.size(), "rows returned"); - split = lines.get(1).split("\\|"); + if (ascii) { + assertEquals(3, lines.size(), "rows returned"); + } else { + assertEquals(6, lines.size(), "rows returned"); + } + split = lines.get(dataRowIndex).split(colSep); assertEquals(NAME.length(), split[1].length()); + // maxColumnSize=-1 (no limit): NAME fits in 1 data row -> N=1 context = new TestActionContext(); statQueue = new StatQueue(); statQueue.setUser("admin"); statQueue.setPassword("admin"); statQueue.setQueueName(NAME); statQueue.setMaxColumnSize(-1); + statQueue.setAscii(ascii); statQueue.execute(context); lines = getOutputLines(context, false); for (String line : lines) { System.out.println(line); } - assertEquals(3, lines.size(), "rows returned"); + if (ascii) { + assertEquals(3, lines.size(), "rows returned"); + } else { + assertEquals(6, lines.size(), "rows returned"); + } } finally { stopServer(); } @@ -2143,7 +2212,7 @@ public void testQstatWarnings() throws Exception { statQueue.execute(context); lines = getOutputLines(context, false); // Header line + DEFAULT_MAX_ROWS queues + warning line - assertEquals(2 + StatQueue.DEFAULT_MAX_ROWS, lines.size(), "rows returned using queueName=Test"); + assertEquals(5 + StatQueue.DEFAULT_MAX_ROWS, lines.size(), "rows returned using queueName=Test"); assertFalse(lines.get(lines.size() - 1).startsWith("WARNING")); //check all queues containing "Test" are displayed @@ -2156,7 +2225,7 @@ public void testQstatWarnings() throws Exception { statQueue.execute(context); lines = getOutputLines(context, false); // Header line + DEFAULT_MAX_ROWS queues - assertEquals(2 + StatQueue.DEFAULT_MAX_ROWS, lines.size(), "rows returned using queueName=Test"); + assertEquals(5 + StatQueue.DEFAULT_MAX_ROWS, lines.size(), "rows returned using queueName=Test"); assertFalse(lines.get(lines.size() - 1).startsWith("WARNING")); sendMessages(session, "Test" + StatQueue.DEFAULT_MAX_ROWS, 1); @@ -2170,7 +2239,7 @@ public void testQstatWarnings() throws Exception { statQueue.execute(context); lines = getOutputLines(context, false); // Header line + DEFAULT_MAX_ROWS queues + warning line - assertEquals(2 + StatQueue.DEFAULT_MAX_ROWS + 1, lines.size(), "rows returned using queueName=Test"); + assertEquals(5 + StatQueue.DEFAULT_MAX_ROWS + 1, lines.size(), "rows returned using queueName=Test"); assertTrue(lines.get(lines.size() - 1).startsWith("WARNING")); //check all queues containing "Test" are displayed @@ -2183,7 +2252,7 @@ public void testQstatWarnings() throws Exception { statQueue.execute(context); lines = getOutputLines(context, false); // Header line + DEFAULT_MAX_ROWS queues + warning line - assertEquals(2 + StatQueue.DEFAULT_MAX_ROWS + 1, lines.size(), "rows returned using queueName=Test"); + assertEquals(5 + StatQueue.DEFAULT_MAX_ROWS + 1, lines.size(), "rows returned using queueName=Test"); assertTrue(lines.get(lines.size() - 1).startsWith("WARNING")); } finally { diff --git a/artemis-cli/src/test/java/org/apache/activemq/cli/test/MessageSerializerTest.java b/artemis-cli/src/test/java/org/apache/activemq/cli/test/MessageSerializerTest.java index a13e7fb9798c..d7cdb3e5aca5 100644 --- a/artemis-cli/src/test/java/org/apache/activemq/cli/test/MessageSerializerTest.java +++ b/artemis-cli/src/test/java/org/apache/activemq/cli/test/MessageSerializerTest.java @@ -48,6 +48,7 @@ import org.apache.activemq.artemis.jms.client.ActiveMQDestination; import org.apache.activemq.artemis.utils.CompositeAddress; import org.apache.activemq.artemis.utils.RandomUtil; +import org.apache.activemq.artemis.utils.TableOut; import org.apache.activemq.artemis.utils.Wait; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -132,8 +133,9 @@ private boolean verifyMessageCount(String address, int messageCount) throws Exce .execute(context); int currentMessageCount; try { + List outLines = getOutputLines(context, false); // parse the value for MESSAGE_COUNT from the output - currentMessageCount = Integer.parseInt(getOutputLines(context, false).get(2).split("\\|")[4].trim()); + currentMessageCount = Integer.parseInt(outLines.get(4).split(TableOut.BOX_VERTICAL)[4].trim()); } catch (Exception e) { currentMessageCount = 0; } diff --git a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/TableOut.java b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/TableOut.java index 93b2ae19df5f..27c585f3d346 100644 --- a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/TableOut.java +++ b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/TableOut.java @@ -23,11 +23,36 @@ public class TableOut { + // Unicode box-drawing characters + public static final String BOX_HORIZONTAL = "─"; + public static final String BOX_VERTICAL = "│"; + public static final String BOX_TOP_LEFT = "┌"; + public static final String BOX_TOP_RIGHT = "┐"; + public static final String BOX_TOP_MID = "┬"; + public static final String BOX_MID_LEFT = "├"; + public static final String BOX_MID_RIGHT = "┤"; + public static final String BOX_MID_MID = "┼"; + public static final String BOX_BOTTOM_LEFT = "└"; + public static final String BOX_BOTTOM_RIGHT = "┘"; + public static final String BOX_BOTTOM_MID = "┴"; + final String separator; final int[] columnSizes; final int indentation; final String indentationString; + + boolean ascii; + + public boolean isAscii() { + return ascii; + } + + public TableOut setAscii(boolean ascii) { + this.ascii = ascii; + return this; + } + public TableOut(String separator, int indentation, int[] columnSizes) { this.separator = separator; this.columnSizes = columnSizes; @@ -37,12 +62,41 @@ public TableOut(String separator, int indentation, int[] columnSizes) { indentationString = " ".repeat(indentation); } + /** Print the top border: ┌───┬───┐ (no-op in ascii style) */ + public void printTopSeparator(PrintStream stream) { + if (!ascii) { + printBoxLine(stream, BOX_TOP_LEFT, BOX_TOP_MID, BOX_TOP_RIGHT); + } + } + + /** Print a middle separator: ├───┼───┤ (or plain dashes in ascii style) */ public void printSeparator(PrintStream stream) { - int totalWidth = separator.length() * (columnSizes.length + 1); - for (int columnSize : columnSizes) { - totalWidth += columnSize; + if (ascii) { + int totalWidth = separator.length() * (columnSizes.length + 1); + for (int columnSize : columnSizes) { + totalWidth += columnSize; + } + stream.println("-".repeat(totalWidth)); + } else { + printBoxLine(stream, BOX_MID_LEFT, BOX_MID_MID, BOX_MID_RIGHT); } - stream.println("-".repeat(totalWidth)); + } + + /** Print the bottom border: └───┴───┘ (no-op in ascii style) */ + public void printBottomSeparator(PrintStream stream) { + if (!ascii) { + printBoxLine(stream, BOX_BOTTOM_LEFT, BOX_BOTTOM_MID, BOX_BOTTOM_RIGHT); + } + } + + private void printBoxLine(PrintStream stream, String left, String mid, String right) { + StringBuilder line = new StringBuilder(); + line.append(left); + for (int i = 0; i < columnSizes.length; i++) { + line.append(BOX_HORIZONTAL.repeat(columnSizes[i])); + line.append(i < columnSizes.length - 1 ? mid : right); + } + stream.println(line); } public void print(PrintStream stream, String[] columns) { @@ -67,7 +121,7 @@ public void print(PrintStream stream, List[] splitColumns, boolean[] cen int lineNumber = 0; do { hasMoreLines = false; - stream.print(separator); + stream.print(ascii ? separator : BOX_VERTICAL); for (int column = 0; column < splitColumns.length; column++) { StringBuilder cell = new StringBuilder(); @@ -92,7 +146,7 @@ public void print(PrintStream stream, List[] splitColumns, boolean[] cen cell.append(" "); } stream.print(cell); - stream.print(separator); + stream.print(ascii ? separator : BOX_VERTICAL); } stream.println(); lineNumber++; @@ -104,18 +158,38 @@ public List splitLine(final String column, int size) { List cells = new ArrayList<>(); for (int position = 0; position < column.length();) { - int identationUsed; - String identationStringUsed; + int indentationUsed; + String indentationStringUsed; if (position == 0 || indentation == 0) { - identationUsed = 0; - identationStringUsed = ""; + indentationUsed = 0; + indentationStringUsed = ""; } else { - identationUsed = indentation; - identationStringUsed = this.indentationString; + indentationUsed = indentation; + indentationStringUsed = this.indentationString; } - int maxPosition = Math.min(size - identationUsed, column.length() - position); - cells.add(identationStringUsed + column.substring(position, position + maxPosition)); - position += maxPosition; + int available = size - indentationUsed; + int remaining = column.length() - position; + + if (remaining <= available) { + // everything fits — no split needed + cells.add(indentationStringUsed + column.substring(position)); + break; + } + + // look backwards from the hard-break position for the last non-alphanumeric character + // but only accept it if it falls at or beyond the halfway point of the available width + int hardBreak = available; + int naturalBreak = -1; + for (int i = hardBreak - 1; i >= available / 2; i--) { + if (!Character.isLetterOrDigit(column.charAt(position + i))) { + naturalBreak = i + 1; // split after the non-alphanumeric character + break; + } + } + + int splitAt = naturalBreak > 0 ? naturalBreak : hardBreak; + cells.add(indentationStringUsed + column.substring(position, position + splitAt)); + position += splitAt; } return cells; diff --git a/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/TableOutTest.java b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/TableOutTest.java index 6c81ca535313..cf1f10d50f96 100644 --- a/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/TableOutTest.java +++ b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/TableOutTest.java @@ -19,40 +19,142 @@ import java.util.List; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class TableOutTest { @Test public void testSplitString() { String bigCell = "1234554321321"; - TableOut tableOut = new TableOut("|", 0, new int[] {10, 3, 3}); + TableOut tableOut = new TableOut("|", 0, new int[]{10, 3, 3}); List lines = tableOut.splitLine(bigCell, 5); - Assertions.assertEquals(3, lines.size()); - Assertions.assertEquals("12345", lines.get(0)); - Assertions.assertEquals("54321", lines.get(1)); - Assertions.assertEquals("321", lines.get(2)); + assertEquals(3, lines.size()); + assertEquals("12345", lines.get(0)); + assertEquals("54321", lines.get(1)); + assertEquals("321", lines.get(2)); + } + + @Test + public void testNaturalSplit() { + String big = "Thisisbigbig "; + String bigCell = big + "short ".repeat(10); + TableOut tableOut = new TableOut("|", 0, new int[]{10, 3, 3}); + List lines = tableOut.splitLine(bigCell, 14); + for (int i = 1; i < lines.size(); i++) { + assertTrue(lines.get(i).startsWith("short ")); + } } @Test public void testSplitStringIdented() { String bigCell = "1234532132"; - TableOut tableOut = new TableOut("|", 2, new int[] {10, 3, 3}); + TableOut tableOut = new TableOut("|", 2, new int[]{10, 3, 3}); List lines = tableOut.splitLine(bigCell, 5); - Assertions.assertEquals(3, lines.size()); - Assertions.assertEquals("12345", lines.get(0)); - Assertions.assertEquals(" 321", lines.get(1)); - Assertions.assertEquals(" 32", lines.get(2)); + assertEquals(3, lines.size()); + assertEquals("12345", lines.get(0)); + assertEquals(" 321", lines.get(1)); + assertEquals(" 32", lines.get(2)); } @Test public void testOutLine() { // the output is visual, however this test is good to make sure the output at least works without any issues - TableOut tableOut = new TableOut("|", 2, new int[] {5, 20, 20}); + TableOut tableOut = new TableOut("|", 2, new int[]{5, 20, 20}); + tableOut.printTopSeparator(System.out); tableOut.print(System.out, new String[]{"This is a big title", "1234567", "1234"}); - tableOut = new TableOut("|", 0, new int[] {10, 20, 20}); - tableOut.print(System.out, new String[]{"This is a big title", "1234567", "1234"}, new boolean[] {true, true, true}); + tableOut.printSeparator(System.out); + tableOut.print(System.out, new String[]{"row1", "value1", "value2"}); + tableOut.printBottomSeparator(System.out); + tableOut = new TableOut("|", 0, new int[]{10, 20, 20}); + tableOut.printTopSeparator(System.out); + tableOut.print(System.out, new String[]{"This is a big title", "1234567", "1234"}, new boolean[]{true, true, true}); + tableOut.printBottomSeparator(System.out); + } + + @Test + public void testBoxDrawingCharacters() { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + java.io.PrintStream ps = new java.io.PrintStream(baos); + + TableOut tableOut = new TableOut("|", 0, new int[]{5, 5}); + tableOut.printTopSeparator(ps); + tableOut.print(ps, new String[]{"Col1", "Col2"}); + tableOut.printSeparator(ps); + tableOut.print(ps, new String[]{"r1c1", "r1c2"}); + tableOut.printBottomSeparator(ps); + + String output = baos.toString(); + String[] lines = output.split(System.lineSeparator()); + + // top border uses ┌ ┬ ┐ + assertTrue(lines[0].startsWith("┌"), "Top line should start with ┌"); + assertTrue(lines[0].contains("┬"), "Top line should contain ┬"); + assertTrue(lines[0].endsWith("┐"), "Top line should end with ┐"); + + // data rows use │ + assertTrue(lines[1].startsWith("│"), "Data row should start with │"); + assertTrue(lines[1].endsWith("│"), "Data row should end with │"); + + // middle separator uses ├ ┼ ┤ + assertTrue(lines[2].startsWith("├"), "Middle separator should start with ├"); + assertTrue(lines[2].contains("┼"), "Middle separator should contain ┼"); + assertTrue(lines[2].endsWith("┤"), "Middle separator should end with ┤"); + + // bottom border uses └ ┴ ┘ + assertTrue(lines[4].startsWith("└"), "Bottom line should start with └"); + assertTrue(lines[4].contains("┴"), "Bottom line should contain ┴"); + assertTrue(lines[4].endsWith("┘"), "Bottom line should end with ┘"); + } + + @Test + public void testAscii() { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + java.io.PrintStream ps = new java.io.PrintStream(baos); + + TableOut tableOut = new TableOut("|", 0, new int[]{5, 5}); + tableOut.setAscii(true); + tableOut.printTopSeparator(ps); // no-op in ascii mode + tableOut.print(ps, new String[]{"Col1", "Col2"}); + tableOut.printSeparator(ps); // plain dashes in ascii mode + tableOut.print(ps, new String[]{"r1c1", "r1c2"}); + tableOut.printBottomSeparator(ps); // no-op in ascii mode + + String output = baos.toString(); + String[] lines = output.split(System.lineSeparator()); + + // ascii mode: only 3 lines — header row, dash separator, data row (no top/bottom border) + assertEquals(3, lines.length, "ascii mode should produce exactly 3 lines"); + + // header row uses plain | delimiter, no box-drawing characters + assertTrue(lines[0].startsWith("|"), "Header row should start with |"); + assertTrue(lines[0].endsWith("|"), "Header row should end with |"); + assertTrue(lines[0].contains("Col1"), "Header row should contain Col1"); + assertTrue(lines[0].contains("Col2"), "Header row should contain Col2"); + + // separator is plain dashes only + assertTrue(lines[1].matches("-+"), "Separator should be plain dashes"); + + // data row uses plain | delimiter, no box-drawing characters + assertTrue(lines[2].startsWith("|"), "Data row should start with |"); + assertTrue(lines[2].endsWith("|"), "Data row should end with |"); + assertTrue(lines[2].contains("r1c1"), "Data row should contain r1c1"); + assertTrue(lines[2].contains("r1c2"), "Data row should contain r1c2"); + + // no Unicode box-drawing characters anywhere + assertFalse(output.contains(TableOut.BOX_TOP_LEFT), "ascii mode should not contain " + TableOut.BOX_TOP_LEFT); + assertFalse(output.contains(TableOut.BOX_TOP_MID), "ascii mode should not contain " + TableOut.BOX_TOP_MID); + assertFalse(output.contains(TableOut.BOX_TOP_RIGHT), "ascii mode should not contain " + TableOut.BOX_TOP_RIGHT); + assertFalse(output.contains(TableOut.BOX_VERTICAL), "ascii mode should not contain " + TableOut.BOX_VERTICAL); + assertFalse(output.contains(TableOut.BOX_MID_LEFT), "ascii mode should not contain " + TableOut.BOX_MID_LEFT); + assertFalse(output.contains(TableOut.BOX_MID_MID), "ascii mode should not contain " + TableOut.BOX_MID_MID); + assertFalse(output.contains(TableOut.BOX_MID_RIGHT), "ascii mode should not contain " + TableOut.BOX_MID_RIGHT); + assertFalse(output.contains(TableOut.BOX_BOTTOM_LEFT), "ascii mode should not contain " + TableOut.BOX_BOTTOM_LEFT); + assertFalse(output.contains(TableOut.BOX_BOTTOM_MID), "ascii mode should not contain " + TableOut.BOX_BOTTOM_MID); + assertFalse(output.contains(TableOut.BOX_BOTTOM_RIGHT), "ascii mode should not contain " + TableOut.BOX_BOTTOM_RIGHT); } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/DescribeJournal.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/DescribeJournal.java index c7156cb99c10..1498fdfe632f 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/DescribeJournal.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/persistence/impl/journal/DescribeJournal.java @@ -246,7 +246,7 @@ private static DescribeJournal describeJournal(SequentialFileFactory fileFactory for (JournalFile file : files) { recordsPrintStream.println("#" + file + " (size=" + file.getFile().size() + ")"); if (journalTable != null) { - journalTable.printSeparator(recordsPrintStream); + journalTable.printTopSeparator(recordsPrintStream); journalTable.print(recordsPrintStream, new String[]{"Operation", "ID", "Record Type", "TX", "Compact", "Decoded"}); journalTable.printSeparator(recordsPrintStream); } @@ -389,6 +389,9 @@ public void checkRecordCounter(RecordInfo info) { } } }, null, reclaimed, null); + if (journalTable != null) { + journalTable.printBottomSeparator(out); + } } recordsPrintStream.println(); @@ -474,7 +477,7 @@ public int hashCode() { final int[] survivingColumnSizes = {10, 8, 30, 7, 120}; final TableOut survivingTable = legacyOutput ? null : new TableOut("|", 2, survivingColumnSizes); if (survivingTable != null) { - survivingTable.printSeparator(out); + survivingTable.printTopSeparator(out); survivingTable.print(out, new String[]{"Operation", "ID", "Record Type", "Compact", "Decoded"}); survivingTable.printSeparator(out); } @@ -542,6 +545,10 @@ public int hashCode() { } } + if (survivingTable != null) { + survivingTable.printBottomSeparator(out); + } + if (!counters.isEmpty()) { out.println("### Page Counters"); printCounters(out, counters, legacyOutput); @@ -556,7 +563,9 @@ public int hashCode() { TableOut preparedTable = null; if (!legacyOutput) { preparedTable = new TableOut("|", 2, survivingColumnSizes); + preparedTable.printTopSeparator(out); preparedTable.print(out, new String[]{"Operation", "ID", "Record Type", "Compact", "Decoded"}); + preparedTable.printSeparator(out); } for (RecordInfo info : tx.getRecords()) { @@ -618,21 +627,27 @@ public int hashCode() { out.println("### Message Counts ###"); int[] countColumnSizes = {25, 12}; TableOut countTable = new TableOut("|", 2, countColumnSizes); + countTable.printTopSeparator(out); countTable.print(out, new String[]{"Metric", "Count"}); + countTable.printSeparator(out); countTable.print(out, new String[]{"Messages", String.valueOf(messageCount)}); countTable.print(out, new String[]{"Large Messages", String.valueOf(largeMessageCount)}); countTable.print(out, new String[]{"Prepared Messages", String.valueOf(preparedMessageCount)}); countTable.print(out, new String[]{"Prepared Large Messages", String.valueOf(preparedLargeMessageCount)}); + countTable.printBottomSeparator(out); if (!messageRefCounts.isEmpty()) { out.println(); out.println("### Message References ###"); int[] refColumnSizes = {12, 12}; TableOut refTable = new TableOut("|", 2, refColumnSizes); + refTable.printTopSeparator(out); refTable.print(out, new String[]{"Queue", "Count"}); + refTable.printSeparator(out); messageRefCounts.forEach((queueId, count) -> { refTable.print(out, new String[]{String.valueOf(queueId), String.valueOf(count)}); }); + refTable.printBottomSeparator(out); } if (!preparedMessageRefCount.isEmpty()) { @@ -640,10 +655,13 @@ public int hashCode() { out.println("### Prepared Message References ###"); int[] prepRefColumnSizes = {12, 12}; TableOut prepRefTable = new TableOut("|", 2, prepRefColumnSizes); + prepRefTable.printTopSeparator(out); prepRefTable.print(out, new String[]{"Queue", "Count"}); + prepRefTable.printSeparator(out); preparedMessageRefCount.forEach((queueId, count) -> { prepRefTable.print(out, new String[]{String.valueOf(queueId), String.valueOf(count)}); }); + prepRefTable.printBottomSeparator(out); } } @@ -664,10 +682,13 @@ protected static void printCounters(final PrintStream out, final Map entry : counters.entrySet()) { table.print(out, new String[]{String.valueOf(entry.getKey()), String.valueOf(entry.getValue().getValue())}); } + table.printBottomSeparator(out); } } diff --git a/docs/user-manual/versions.adoc b/docs/user-manual/versions.adoc index 92a21330a2b9..113f2e8914f5 100644 --- a/docs/user-manual/versions.adoc +++ b/docs/user-manual/versions.adoc @@ -15,6 +15,13 @@ NOTE: Follow the general upgrade procedure outlined in the xref:upgrading.adoc#u :sectnums!: +== Version 2.58.0 + +=== Highlights +* https://issues.apache.org/jira/browse/ARTEMIS-6242[ARTEMIS-6242] / https://issues.apache.org/jira/browse/ARTEMIS-6216[ARTEMIS-6216] - CLI table output (`queue stat`, `lock list`, `print-data`) now uses Unicode box-drawing characters by default. + ** Use `--ascii` for plain ASCII output. + +=== Upgrading from 2.57.0 == Version 2.57.0 === Highlights diff --git a/tests/db-tests/src/test/java/org/apache/activemq/artemis/tests/db/paging/PrintDataTest.java b/tests/db-tests/src/test/java/org/apache/activemq/artemis/tests/db/paging/PrintDataTest.java index 3d4b5d813595..810bff0c9031 100644 --- a/tests/db-tests/src/test/java/org/apache/activemq/artemis/tests/db/paging/PrintDataTest.java +++ b/tests/db-tests/src/test/java/org/apache/activemq/artemis/tests/db/paging/PrintDataTest.java @@ -88,7 +88,7 @@ public void testData() throws Exception { } server.stop(); - PrintData printData = new PrintData().setLegacyOutput(true); + PrintData printData = new PrintData().setAscii(true); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); PrintStream printStream = new PrintStream(byteArrayOutputStream, true, StandardCharsets.UTF_8.name());