From 7de3efe18afbf7defbe2607bd18f61bcd461472d Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:01:55 -0400 Subject: [PATCH 1/4] CAMEL-23237: camel-jbang catalog tables fill the terminal width Standardize column definitions and expand the last free-text column (DESCRIPTION) of the catalog tables to the terminal width instead of capping it at 80, like gh/ps/systemctl. - Add common/CamelTableColumns toolkit: name()/since()/lastText() column factories plus measure() and lastColumnWidth() so the last column gets the exact remaining width. measure() mirrors freva's String.length() sizing so the remainder math matches what AsciiTable renders. - TerminalWidthHelper: add Windows mode con detection, add fillWidth() (no upper cap), change DEFAULT_WIDTH 120 -> 80, extract package-private parseColumns(). - Migrate CatalogBaseCommand, CatalogTransformer (drop the nameWidth()=60 override) and CatalogKamelet onto the toolkit. CatalogDoc left as-is (detail view with FANCY_ASCII + scaleWidth + NEWLINE wrapping). - --json remains the lossless path for automation; TTY output stays fit-to-terminal and ellipsis-truncated. Process and action command tables migrate in follow-up work. Co-Authored-By: Claude Opus 4.8 --- .../pages/camel-4x-upgrade-guide-4_22.adoc | 14 +++ .../commands/catalog/CatalogBaseCommand.java | 40 ++++--- .../core/commands/catalog/CatalogKamelet.java | 18 +-- .../commands/catalog/CatalogTransformer.java | 5 - .../jbang/core/common/CamelTableColumns.java | 107 ++++++++++++++++++ .../core/common/TerminalWidthHelper.java | 94 +++++++++++---- .../core/common/CamelTableColumnsTest.java | 74 ++++++++++++ .../core/common/TerminalWidthHelperTest.java | 57 ++++++++++ 8 files changed, 355 insertions(+), 54 deletions(-) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumns.java create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumnsTest.java diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index b8ae87f73de71..a8c0c7bdf40f1 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -120,3 +120,17 @@ discarding the message with no exception or log. Scalar value nodes are now sent as a single record. Only container nodes (`ArrayNode`, `ObjectNode`) are still split, which is unchanged. Any `convertBodyTo(...)` previously used as a workaround is no longer required. + +=== camel-jbang catalog tables fill the terminal width + +The `camel catalog` commands (`camel catalog component`, `camel catalog dataformat`, +`camel catalog language`, `camel catalog transformer`, `camel catalog kamelet`, ...) now size the +`DESCRIPTION` column to the detected terminal width instead of the previous fixed 80-character cap, +so wide terminals show more of the description before it is truncated with an ellipsis. The `NAME` +column width is also standardized across these commands (it previously differed per command, for +example 60 for `transformer` and 30 elsewhere). + +Terminal width is now detected on Windows (`cmd` / PowerShell) via `mode con`, in addition to the +existing `COLUMNS` / `stty size` detection. When no terminal can be detected (for example when the +output is piped or redirected), the width falls back to 80 columns. For full, untruncated output +suitable for scripting, use the `--json` option. \ No newline at end of file diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java index a83f99aa11136..b8ac84ee7605e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogBaseCommand.java @@ -20,18 +20,19 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.function.Function; import java.util.stream.Collectors; import com.github.freva.asciitable.AsciiTable; import com.github.freva.asciitable.Column; import com.github.freva.asciitable.HorizontalAlign; -import com.github.freva.asciitable.OverflowBehaviour; import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.dsl.jbang.core.commands.CamelCommand; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin; import org.apache.camel.dsl.jbang.core.commands.QuarkusPlatformMixin; +import org.apache.camel.dsl.jbang.core.common.CamelTableColumns; import org.apache.camel.dsl.jbang.core.common.CatalogLoader; import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates; import org.apache.camel.dsl.jbang.core.common.RuntimeType; @@ -159,27 +160,28 @@ public Integer doCall() throws Exception { .map(CatalogBaseDTO::toMap) .collect(Collectors.toList()))); } else { - // Compute description width: terminal minus fixed columns and border overhead - int fixedWidth = nameWidth() + 12 + 8; // LEVEL ~12 chars, SINCE ~8 chars - if (RuntimeType.quarkus == runtime) { - fixedWidth += 8; // NATIVE column - } - int descWidth = TerminalWidthHelper.flexWidth( - terminalWidth(), fixedWidth, TerminalWidthHelper.noBorderOverhead( - RuntimeType.quarkus == runtime ? 5 : 4), - 20, 80); + // Size the DESCRIPTION column to fill the terminal: measure the actual width of the other + // visible columns and give the remainder to DESCRIPTION (floored on narrow terminals). + boolean quarkus = RuntimeType.quarkus == runtime; + Function nameGetter = displayGav ? this::shortGav : r -> r.name; + int nameW = CamelTableColumns.measure(displayGav ? "ARTIFACT-ID" : "NAME", + displayGav ? Integer.MAX_VALUE : CamelTableColumns.NAME_MAX, rows, nameGetter); + int levelW = CamelTableColumns.measure("LEVEL", Integer.MAX_VALUE, rows, this::level); + int sinceW = CamelTableColumns.measure("SINCE", Integer.MAX_VALUE, rows, r -> r.since); + int overhead = TerminalWidthHelper.noBorderOverhead(quarkus ? 5 : 4); + int descWidth = quarkus + ? CamelTableColumns.lastColumnWidth(terminalWidth(), overhead, nameW, levelW, sinceW, + CamelTableColumns.measure("NATIVE", Integer.MAX_VALUE, rows, this::nativeSupported)) + : CamelTableColumns.lastColumnWidth(terminalWidth(), overhead, nameW, levelW, sinceW); printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList( - new Column().header("NAME").visible(!displayGav).dataAlign(HorizontalAlign.LEFT).maxWidth(nameWidth()) - .with(r -> r.name), + CamelTableColumns.name().visible(!displayGav).with(r -> r.name), new Column().header("ARTIFACT-ID").visible(displayGav).dataAlign(HorizontalAlign.LEFT) .with(this::shortGav), new Column().header("LEVEL").dataAlign(HorizontalAlign.LEFT).with(this::level), new Column().header("NATIVE").dataAlign(HorizontalAlign.CENTER) - .visible(RuntimeType.quarkus == runtime).with(this::nativeSupported), - new Column().header("SINCE").dataAlign(HorizontalAlign.RIGHT).with(r -> r.since), - new Column().header("DESCRIPTION").dataAlign(HorizontalAlign.LEFT) - .maxWidth(descWidth, OverflowBehaviour.ELLIPSIS_RIGHT) - .with(this::shortDescription)))); + .visible(quarkus).with(this::nativeSupported), + CamelTableColumns.since().with(r -> r.since), + CamelTableColumns.lastText("DESCRIPTION", descWidth).with(this::shortDescription)))); } } else if (filterName != null) { // suggest similar names when filter returns no results @@ -196,10 +198,6 @@ public Integer doCall() throws Exception { return 0; } - int nameWidth() { - return 30; - } - int sortRow(Row o1, Row o2) { String s = sort; int negate = 1; diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java index bc00499e416ed..18cae77dd45cd 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java @@ -29,6 +29,7 @@ import com.github.freva.asciitable.HorizontalAlign; import org.apache.camel.dsl.jbang.core.commands.CamelCommand; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.CamelTableColumns; import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper; import org.apache.camel.dsl.jbang.core.common.VersionHelper; import org.apache.camel.main.download.DependencyDownloaderClassLoader; @@ -113,18 +114,17 @@ public Integer doCall() throws Exception { rows.sort(this::sortRow); if (!rows.isEmpty()) { - int tw = terminalWidth(); - // Fixed columns: NAME (~30), TYPE (10), LEVEL (12) - int fixedWidth = 30 + 10 + 12; - int descWidth = TerminalWidthHelper.flexWidth( - tw, fixedWidth, TerminalWidthHelper.noBorderOverhead(4), - 20, 80); + // Size the DESCRIPTION column to fill the terminal from the measured width of the other columns. + int nameW = CamelTableColumns.measure("NAME", CamelTableColumns.NAME_MAX, rows, r -> r.name); + int typeW = Math.max(10, CamelTableColumns.measure("TYPE", Integer.MAX_VALUE, rows, r -> r.type)); + int levelW = Math.max(12, CamelTableColumns.measure("LEVEL", Integer.MAX_VALUE, rows, r -> r.supportLevel)); + int descWidth = CamelTableColumns.lastColumnWidth( + terminalWidth(), TerminalWidthHelper.noBorderOverhead(4), nameW, typeW, levelW); printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList( - new Column().header("NAME").dataAlign(HorizontalAlign.LEFT).with(r -> r.name), + CamelTableColumns.name().with(r -> r.name), new Column().header("TYPE").dataAlign(HorizontalAlign.LEFT).minWidth(10).with(r -> r.type), new Column().header("LEVEL").dataAlign(HorizontalAlign.LEFT).minWidth(12).with(r -> r.supportLevel), - new Column().header("DESCRIPTION").dataAlign(HorizontalAlign.LEFT).maxWidth(descWidth) - .with(this::getDescription)))); + CamelTableColumns.lastText("DESCRIPTION", descWidth).with(this::getDescription)))); } return 0; diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogTransformer.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogTransformer.java index 0d44412ff8165..317538e1c127b 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogTransformer.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogTransformer.java @@ -35,11 +35,6 @@ public CatalogTransformer(CamelJBangMain main) { super(main); } - @Override - int nameWidth() { - return 60; - } - @Override List collectRows() { List rows = new ArrayList<>(); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumns.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumns.java new file mode 100644 index 0000000000000..838ff1b78ab81 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumns.java @@ -0,0 +1,107 @@ +/* + * 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.camel.dsl.jbang.core.common; + +import java.util.Collection; +import java.util.function.Function; +import java.util.stream.IntStream; + +import com.github.freva.asciitable.Column; +import com.github.freva.asciitable.HorizontalAlign; +import com.github.freva.asciitable.OverflowBehaviour; + +/** + * Standardized {@link Column} definitions for camel-jbang table output. + * + *

+ * The same logical column (NAME, DESCRIPTION, ...) was previously hand-rolled in every command with differing widths + * and overflow behaviour. These factories give a single source of truth so tables render consistently, and so the last + * (rightmost) free-text column can be sized to fill the terminal width. + * + *

+ * Each factory returns a pre-configured {@link Column}; the caller chains {@code .with(getter)} (which returns the + * opaque {@code ColumnData}), so all configuration must happen before {@code with}. + */ +public final class CamelTableColumns { + + /** + * Maximum width for a NAME column. A name longer than this is the rare case; short names render at their content + * width regardless, so this only acts as a ceiling. + */ + public static final int NAME_MAX = 60; + + /** Minimum width for the last free-text column on narrow terminals. */ + public static final int LAST_MIN = 20; + + private CamelTableColumns() { + } + + /** Standard NAME column: left aligned, capped at {@link #NAME_MAX}. */ + public static Column name() { + return new Column().header("NAME").dataAlign(HorizontalAlign.LEFT).maxWidth(NAME_MAX); + } + + /** Standard SINCE column: right aligned. */ + public static Column since() { + return new Column().header("SINCE").dataAlign(HorizontalAlign.RIGHT); + } + + /** + * The rightmost free-text column. Grows up to {@code width} and truncates with an ellipsis so each row stays on a + * single line (use this for flat list/status tables; detail views may keep {@code NEWLINE} wrapping). + * + * @param header the column header + * @param width the maximum width, typically from {@link #lastColumnWidth(int, int, int...)} + */ + public static Column lastText(String header, int width) { + return new Column().header(header).dataAlign(HorizontalAlign.LEFT) + .maxWidth(width, OverflowBehaviour.ELLIPSIS_RIGHT); + } + + /** + * Actual rendered width of a structured column: the longest of the header and any cell value, capped at + * {@code maxWidth}. Pass {@link Integer#MAX_VALUE} for an unbounded column. + * + * @param header the column header (may be {@code null}) + * @param maxWidth the column's maximum width (ceiling) + * @param rows the rows being rendered + * @param getter accessor returning the cell value for a row + */ + public static int measure(String header, int maxWidth, Collection rows, Function getter) { + int width = header != null ? header.length() : 0; + for (T row : rows) { + String value = getter.apply(row); + if (value != null) { + width = Math.max(width, value.length()); + } + } + return Math.min(width, maxWidth); + } + + /** + * Width for the last column so the table reaches the terminal edge: the terminal width minus the (actual) widths of + * the other columns and the border overhead, floored at {@link #LAST_MIN}. + * + * @param terminalWidth total terminal width in columns + * @param borderOverhead overhead from borders/padding (see {@link TerminalWidthHelper#noBorderOverhead(int)}) + * @param otherWidths the measured widths of every other (visible) column + */ + public static int lastColumnWidth(int terminalWidth, int borderOverhead, int... otherWidths) { + int others = IntStream.of(otherWidths).sum(); + return TerminalWidthHelper.fillWidth(terminalWidth, others, borderOverhead, LAST_MIN); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java index e096f8f961502..19bbc3f98e516 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java @@ -16,12 +16,17 @@ */ package org.apache.camel.dsl.jbang.core.common; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Helper for detecting the terminal width to adapt table and command output. * *

- * Uses the {@code COLUMNS} environment variable or {@code stty size} to detect the terminal width. Falls back to a - * default width when the terminal size cannot be determined (e.g., when output is piped or redirected). + * Uses the {@code COLUMNS} environment variable, {@code stty size} (POSIX) or {@code mode con} (Windows) to detect the + * terminal width. Falls back to a default width when the terminal size cannot be determined (e.g., when output is piped + * or redirected). * *

* Avoids using JLine's {@code TerminalBuilder} for width detection because it sends escape sequence queries (DA1, CPR) @@ -29,9 +34,11 @@ */ public final class TerminalWidthHelper { - private static final int DEFAULT_WIDTH = 120; + private static final int DEFAULT_WIDTH = 80; private static final int MIN_WIDTH = 40; + private static final Pattern INTEGER = Pattern.compile("\\d+"); + private TerminalWidthHelper() { } @@ -39,8 +46,8 @@ private TerminalWidthHelper() { * Returns the current terminal width in columns. * *

- * Tries {@code COLUMNS} environment variable first, then {@code stty size}. Returns {@value #DEFAULT_WIDTH} if - * detection fails or if the output is not connected to a terminal. + * Tries the {@code COLUMNS} environment variable first, then {@code stty size} on POSIX or {@code mode con} on + * Windows. Returns {@value #DEFAULT_WIDTH} if detection fails or if the output is not connected to a terminal. */ public static int getTerminalWidth() { // Try COLUMNS env var first (set by most shells) @@ -55,26 +62,58 @@ public static int getTerminalWidth() { // ignore } } - // Fall back to stty which reads the terminal size without escape sequences + // Fall back to an OS native command that reads the terminal size without escape sequences + int w = isWindows() + ? readWidthFromCommand("cmd", "/c", "mode", "con") + : readWidthFromCommand("stty", "size"); + if (w > 0) { + return Math.max(w, MIN_WIDTH); + } + return DEFAULT_WIDTH; + } + + private static boolean isWindows() { + return System.getProperty("os.name", "").toLowerCase(Locale.ROOT).startsWith("windows"); + } + + private static int readWidthFromCommand(String... command) { try { - Process p = new ProcessBuilder("stty", "size") + Process p = new ProcessBuilder(command) .redirectInput(ProcessBuilder.Redirect.INHERIT) .start(); - String output = new String(p.getInputStream().readAllBytes()).trim(); + String output = new String(p.getInputStream().readAllBytes()); p.waitFor(); - if (!output.isEmpty()) { - String[] parts = output.split("\\s+"); - if (parts.length >= 2) { - int w = Integer.parseInt(parts[1]); - if (w > 0) { - return Math.max(w, MIN_WIDTH); - } - } - } + return parseColumns(output); } catch (Exception e) { - // ignore — stty not available (e.g. Windows) + // ignore — command not available (e.g. stty/mode missing) + return -1; } - return DEFAULT_WIDTH; + } + + /** + * Parses the column count from the output of {@code stty size} ("rows cols") or Windows {@code mode con} (a + * multi-line "Lines: N / Columns: N" block). Both place the column count as the second integer in the + * output, so it is parsed positionally rather than by label to survive localized Windows output where the + * {@code Columns:} label is translated. + * + * @param output the raw command output + * @return the column count, or {@code -1} if it cannot be determined + */ + static int parseColumns(String output) { + if (output == null) { + return -1; + } + Matcher m = INTEGER.matcher(output); + Integer first = null; + while (m.find()) { + int value = Integer.parseInt(m.group()); + if (first == null) { + first = value; + } else { + return value; + } + } + return -1; } /** @@ -96,6 +135,23 @@ public static int flexWidth( return Math.max(minFlexWidth, Math.min(maxFlexWidth, available)); } + /** + * Computes the width for the last column so the table fills the terminal width. Unlike + * {@link #flexWidth(int, int, int, int, int)} there is no upper cap: the column grows all the way to the terminal + * edge. + * + * @param terminalWidth total terminal width in columns + * @param fixedColumnsWidth sum of the (actual) widths of all other columns + * @param borderOverhead overhead from table borders and padding (use {@link #noBorderOverhead(int)} or + * {@link #fancyBorderOverhead(int)}) + * @param minWidth minimum width for the last column (used when terminal is narrow) + * @return the computed width for the last column + */ + public static int fillWidth( + int terminalWidth, int fixedColumnsWidth, int borderOverhead, int minWidth) { + return Math.max(minWidth, terminalWidth - fixedColumnsWidth - borderOverhead); + } + /** * Scales a column width proportionally based on available terminal width. All columns with the given preferred * widths are scaled proportionally to fit within the terminal. diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumnsTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumnsTest.java new file mode 100644 index 0000000000000..a08c520caff78 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumnsTest.java @@ -0,0 +1,74 @@ +/* + * 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.camel.dsl.jbang.core.common; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CamelTableColumnsTest { + + private static final Function IDENTITY = s -> s; + + // --- measure --- + + @Test + void measureUsesLongestCellWhenWiderThanHeader() { + List rows = List.of("ftp", "salesforce", "kafka"); + // longest value "salesforce" = 10, header "NAME" = 4 + assertEquals(10, CamelTableColumns.measure("NAME", CamelTableColumns.NAME_MAX, rows, IDENTITY)); + } + + @Test + void measureUsesHeaderWhenWiderThanCells() { + List rows = List.of("x", "y"); + // header "DESCRIPTION" = 11 is wider than any 1-char value + assertEquals(11, CamelTableColumns.measure("DESCRIPTION", Integer.MAX_VALUE, rows, IDENTITY)); + } + + @Test + void measureCapsAtMaxWidth() { + List rows = List.of("a-very-long-component-name-that-exceeds-the-cap-by-a-lot-indeed"); + // value length is > NAME_MAX, so it is capped at NAME_MAX + assertEquals(CamelTableColumns.NAME_MAX, + CamelTableColumns.measure("NAME", CamelTableColumns.NAME_MAX, rows, IDENTITY)); + } + + @Test + void measureIgnoresNullCells() { + List rows = Arrays.asList("ok", null, "fine"); + assertEquals(4, CamelTableColumns.measure("N", Integer.MAX_VALUE, rows, IDENTITY)); + } + + // --- lastColumnWidth --- + + @Test + void lastColumnWidthConsumesExactRemainder() { + // 200 terminal - (20 + 10 + 8) others - 6 borders = 156 for the last column + assertEquals(156, CamelTableColumns.lastColumnWidth(200, 6, 20, 10, 8)); + } + + @Test + void lastColumnWidthFloorsAtLastMinOnNarrowTerminal() { + // remainder would be negative, so it is floored at LAST_MIN + assertEquals(CamelTableColumns.LAST_MIN, CamelTableColumns.lastColumnWidth(40, 6, 30, 12, 10)); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelperTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelperTest.java index af8e5da22c3eb..67f3d01f11ec3 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelperTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelperTest.java @@ -31,6 +31,63 @@ void getTerminalWidthReturnsPositiveValue() { assertTrue(width >= 40, "Terminal width should be at least 40, got: " + width); } + // --- fillWidth --- + + @Test + void fillWidthFillsRemainingSpaceWithoutCap() { + // 200 cols - 80 others - 6 borders = 114 available; no upper cap, so the last column keeps all 114 + assertEquals(114, TerminalWidthHelper.fillWidth(200, 80, 6, 20)); + } + + @Test + void fillWidthFloorsAtMinOnNarrowTerminal() { + // 60 cols - 86 others - 6 borders = -32 available; floored at min 20 + assertEquals(20, TerminalWidthHelper.fillWidth(60, 86, 6, 20)); + } + + // --- parseColumns (shared by stty size and Windows mode con) --- + + @Test + void parseColumnsFromStty() { + // stty size prints "rows cols"; the column count is the second integer + assertEquals(80, TerminalWidthHelper.parseColumns("24 80")); + assertEquals(211, TerminalWidthHelper.parseColumns("51 211\n")); + } + + @Test + void parseColumnsFromWindowsModeCon() { + String output = """ + Status for device CON: + ---------------------- + Lines: 30 + Columns: 120 + Keyboard rate: 31 + Keyboard delay: 1 + Code page: 850 + """; + assertEquals(120, TerminalWidthHelper.parseColumns(output)); + } + + @Test + void parseColumnsFromLocalizedModeCon() { + // Non-English Windows translates the labels; parsing the second integer positionally still works + String output = """ + État du périphérique CON : + -------------------------- + Lignes : 30 + Colonnes : 120 + """; + assertEquals(120, TerminalWidthHelper.parseColumns(output)); + } + + @Test + void parseColumnsReturnsNegativeWhenUndetermined() { + assertEquals(-1, TerminalWidthHelper.parseColumns(null)); + assertEquals(-1, TerminalWidthHelper.parseColumns("")); + assertEquals(-1, TerminalWidthHelper.parseColumns("no numbers here")); + assertEquals(-1, TerminalWidthHelper.parseColumns("42")); // only one integer, no column value + } + // --- flexWidth --- @Test From 95928e7938db90218f7b611029e4e3fb72602ba5 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:27:41 -0400 Subject: [PATCH 2/4] CAMEL-23237: fix NO_BORDERS table overhead so filled tables stay within the terminal noBorderOverhead(n) returned (n-1)*2, but AsciiTable NO_BORDERS pads every column with one leading and one trailing space, so the real overhead is n*2. This was harmless while every table used capped flexWidth, but the new uncapped fillWidth/lastColumnWidth sizes the last column to the exact remainder, so the 2-column underestimate made the widest rows render at terminalWidth + 2 and soft-wrap. Fixed at the root (noBorderOverhead now returns n*2), which is safe for all existing flexWidth callers because they clamp their result to [min, max]; the change only ever removes a latent 2-column overflow. Updated the existing overhead assertions and added a rendering-level regression test that asserts a lastColumnWidth-sized NO_BORDERS table never exceeds the terminal width (arithmetic-only tests missed the off-by-2 because they encoded the same wrong value). Co-Authored-By: Claude Opus 4.8 --- .../core/common/TerminalWidthHelper.java | 5 +-- .../core/common/CamelTableColumnsTest.java | 36 +++++++++++++++++++ .../core/common/TerminalWidthHelperTest.java | 20 ++++++----- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java index 19bbc3f98e516..81c8ec6b78d94 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java @@ -175,10 +175,11 @@ public static int scaleWidth( } /** - * Border overhead for NO_BORDERS tables: 2 spaces between each column pair. + * Border overhead for NO_BORDERS tables. AsciiTable pads every column with one leading and one trailing space (no + * separator characters), so the overhead is exactly {@code 2 * columnCount} regardless of the number of columns. */ public static int noBorderOverhead(int columnCount) { - return (columnCount - 1) * 2; + return columnCount * 2; } /** diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumnsTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumnsTest.java index a08c520caff78..7c15ae7687c11 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumnsTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/CamelTableColumnsTest.java @@ -20,9 +20,13 @@ import java.util.List; import java.util.function.Function; +import com.github.freva.asciitable.AsciiTable; +import com.github.freva.asciitable.Column; +import com.github.freva.asciitable.HorizontalAlign; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class CamelTableColumnsTest { @@ -71,4 +75,36 @@ void lastColumnWidthFloorsAtLastMinOnNarrowTerminal() { // remainder would be negative, so it is floored at LAST_MIN assertEquals(CamelTableColumns.LAST_MIN, CamelTableColumns.lastColumnWidth(40, 6, 30, 12, 10)); } + + // --- rendering: a table sized with lastColumnWidth must fit the terminal --- + + @Test + void lastColumnWidthKeepsRenderedTableWithinTerminal() { + // The whole point of filling the terminal is that the rendered line still fits inside it. A single-line + // description longer than the last column exercises the exact-fit case where an off-by-one in the border + // overhead would overflow the terminal by a couple of columns. + int terminalWidth = 100; + List rows = List.of( + new String[] { + "ftp", "Stable", "4.0", + "A very long component description that definitely exceeds the remaining width and must be truncated" }, + new String[] { "salesforce-streaming", "Preview", "3.1", "short" }); + + int nameW = CamelTableColumns.measure("NAME", CamelTableColumns.NAME_MAX, rows, r -> r[0]); + int levelW = CamelTableColumns.measure("LEVEL", Integer.MAX_VALUE, rows, r -> r[1]); + int sinceW = CamelTableColumns.measure("SINCE", Integer.MAX_VALUE, rows, r -> r[2]); + int descWidth = CamelTableColumns.lastColumnWidth( + terminalWidth, TerminalWidthHelper.noBorderOverhead(4), nameW, levelW, sinceW); + + String table = AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList( + CamelTableColumns.name().with(r -> r[0]), + new Column().header("LEVEL").dataAlign(HorizontalAlign.LEFT).with(r -> r[1]), + CamelTableColumns.since().with(r -> r[2]), + CamelTableColumns.lastText("DESCRIPTION", descWidth).with(r -> r[3]))); + + for (String line : table.split("\n")) { + assertTrue(line.length() <= terminalWidth, + "Rendered line width %d exceeds terminal %d: [%s]".formatted(line.length(), terminalWidth, line)); + } + } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelperTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelperTest.java index 67f3d01f11ec3..3b331cf0dcfb9 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelperTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelperTest.java @@ -158,14 +158,16 @@ void scaleWidthNeverExceedsPreferred() { @Test void noBorderOverheadSingleColumn() { - assertEquals(0, TerminalWidthHelper.noBorderOverhead(1)); + // one leading + one trailing padding space around the single column + assertEquals(2, TerminalWidthHelper.noBorderOverhead(1)); } @Test void noBorderOverheadMultipleColumns() { - assertEquals(2, TerminalWidthHelper.noBorderOverhead(2)); - assertEquals(6, TerminalWidthHelper.noBorderOverhead(4)); - assertEquals(16, TerminalWidthHelper.noBorderOverhead(9)); + // each column contributes 2 padding spaces (one on each side) + assertEquals(4, TerminalWidthHelper.noBorderOverhead(2)); + assertEquals(8, TerminalWidthHelper.noBorderOverhead(4)); + assertEquals(18, TerminalWidthHelper.noBorderOverhead(9)); } // --- fancyBorderOverhead --- @@ -225,19 +227,19 @@ void scaledColumnsPreserveWidthOnWideTerminal() { void flexWidthForNoBordersProcessCommand() { // Simulate ListProcess: 9 columns, fixed ~56 chars, NAME flex (max 40), error flex (max 70) int tw = 80; - int borders = TerminalWidthHelper.noBorderOverhead(9); // 16 + int borders = TerminalWidthHelper.noBorderOverhead(9); // 18 int nameW = TerminalWidthHelper.flexWidth(tw, 56, borders, 15, 40); - // 80 - 56 - 16 = 8, but min is 15 + // 80 - 56 - 18 = 6, but min is 15 assertEquals(15, nameW); tw = 120; nameW = TerminalWidthHelper.flexWidth(tw, 56, borders, 15, 40); - // 120 - 56 - 16 = 48, capped at max 40 + // 120 - 56 - 18 = 46, capped at max 40 assertEquals(40, nameW); tw = 100; nameW = TerminalWidthHelper.flexWidth(tw, 56, borders, 15, 40); - // 100 - 56 - 16 = 28 - assertEquals(28, nameW); + // 100 - 56 - 18 = 26 + assertEquals(26, nameW); } } From c0d80f70e6d5cc191cd5ba527ea3490f7bd631f3 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:08:45 -0400 Subject: [PATCH 3/4] CAMEL-23237: restore 120-column fallback and migrate UpdateList onto the table toolkit Address PR review feedback: - Revert DEFAULT_WIDTH from 80 back to 120. 80 is too narrow for today's terminals (davsclaus, gnodet), and lowering the shared no-TTY fallback narrowed every not-yet-migrated NO_BORDERS table, not just the catalog commands. - Migrate UpdateList onto CamelTableColumns: measure the actual rendered width of VERSION / RUNTIME / RUNTIME VERSION and size DESCRIPTION to fill the terminal via lastColumnWidth(), instead of the hand-rolled capped flexWidth. This is the structural fix for the UpdateListTest CI failure (oscerd): the DESCRIPTION column is now sized from measured widths rather than depending on the shared fallback constant. Dropped VERSION's minWidth(10) so the measured width equals the rendered width, which the fill math requires. - Update the 4.22 upgrade guide to state the 120-column fallback. Co-Authored-By: Claude Opus 4.8 --- .../pages/camel-4x-upgrade-guide-4_22.adoc | 2 +- .../core/commands/update/UpdateList.java | 21 +++++++++++-------- .../core/common/TerminalWidthHelper.java | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index a8c0c7bdf40f1..618fba992cd6e 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -132,5 +132,5 @@ example 60 for `transformer` and 30 elsewhere). Terminal width is now detected on Windows (`cmd` / PowerShell) via `mode con`, in addition to the existing `COLUMNS` / `stty size` detection. When no terminal can be detected (for example when the -output is piped or redirected), the width falls back to 80 columns. For full, untruncated output +output is piped or redirected), the width falls back to 120 columns. For full, untruncated output suitable for scripting, use the `--json` option. \ No newline at end of file diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateList.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateList.java index 57a9182715ec0..739aafdce5ca7 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateList.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/UpdateList.java @@ -37,6 +37,7 @@ import com.github.freva.asciitable.HorizontalAlign; import org.apache.camel.dsl.jbang.core.commands.CamelCommand; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.CamelTableColumns; import org.apache.camel.dsl.jbang.core.common.RuntimeType; import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper; import org.apache.camel.dsl.jbang.core.common.VersionHelper; @@ -183,21 +184,23 @@ public Integer doCall() throws Exception { .map(UpdateListDTO::toMap) .collect(Collectors.toList()))); } else { - int tw = terminalWidth(); - // Fixed columns: VERSION (10), RUNTIME (~18), RUNTIME VERSION (~17) - int fixedWidth = 10 + 18 + 17; - int descWidth = TerminalWidthHelper.flexWidth( - tw, fixedWidth, TerminalWidthHelper.noBorderOverhead(4), - 20, 80); + // Size the DESCRIPTION column to fill the terminal: measure the actual rendered width of the + // other columns so the remainder handed to the last column is exact (see CamelTableColumns). + int versionW = CamelTableColumns.measure("VERSION", Integer.MAX_VALUE, rows, r -> r.version().toString()); + int runtimeW = CamelTableColumns.measure("RUNTIME", Integer.MAX_VALUE, rows, r -> r.runtime()); + int runtimeVersionW + = CamelTableColumns.measure("RUNTIME VERSION", Integer.MAX_VALUE, rows, r -> r.runtimeVersion()); + int overhead = TerminalWidthHelper.noBorderOverhead(4); + int descWidth + = CamelTableColumns.lastColumnWidth(terminalWidth(), overhead, versionW, runtimeW, runtimeVersionW); printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList( - new Column().header("VERSION").minWidth(10).dataAlign(HorizontalAlign.LEFT) + new Column().header("VERSION").dataAlign(HorizontalAlign.LEFT) .with(r -> r.version().toString()), new Column().header("RUNTIME") .dataAlign(HorizontalAlign.LEFT).with(r -> r.runtime()), new Column().header("RUNTIME VERSION") .dataAlign(HorizontalAlign.LEFT).with(r -> r.runtimeVersion()), - new Column().header("DESCRIPTION").maxWidth(descWidth) - .dataAlign(HorizontalAlign.LEFT).with(r -> r.description())))); + CamelTableColumns.lastText("DESCRIPTION", descWidth).with(r -> r.description())))); } return 0; diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java index 81c8ec6b78d94..24e1e7e3ff045 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/TerminalWidthHelper.java @@ -34,7 +34,7 @@ */ public final class TerminalWidthHelper { - private static final int DEFAULT_WIDTH = 80; + private static final int DEFAULT_WIDTH = 120; private static final int MIN_WIDTH = 40; private static final Pattern INTEGER = Pattern.compile("\\d+"); From 58f578648cb9f7e35c1247063ab3ff7a17b86b3b Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:29:01 -0400 Subject: [PATCH 4/4] CAMEL-23237: migrate camel infra list table onto the CamelTableColumns toolkit Follow the same terminal-filling model as the catalog tables (review follow-up from @davsclaus): - DESCRIPTION now fills the terminal width via CamelTableColumns; the other columns are measured so it gets the exact remainder. - IMPLEMENTATION is capped and SERVICE_DATA keeps a compact fixed width; both truncate with an ellipsis (ELLIPSIS_RIGHT) instead of letting the raw JSON service data overflow and soft-wrap the terminal. Full, structured service data is still available via --json. Also extract the --json service-data handling into a testable parseServiceData() helper and cover it with two tests: - serviceData is embedded as nested JSON (not an escaped string) in --json - unparseable service data is dropped rather than emitted, and no longer risks an NPE when there is no data (added an explicit null guard). Documented the infra list display change in the 4.22 upgrade guide. Co-Authored-By: Claude Opus 4.8 --- .../pages/camel-4x-upgrade-guide-4_22.adoc | 6 +- .../core/commands/infra/InfraBaseCommand.java | 57 ++++++++++++------- .../jbang/core/commands/infra/InfraTest.java | 30 ++++++++++ 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index 618fba992cd6e..7e7c43f79387b 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -133,4 +133,8 @@ example 60 for `transformer` and 30 elsewhere). Terminal width is now detected on Windows (`cmd` / PowerShell) via `mode con`, in addition to the existing `COLUMNS` / `stty size` detection. When no terminal can be detected (for example when the output is piped or redirected), the width falls back to 120 columns. For full, untruncated output -suitable for scripting, use the `--json` option. \ No newline at end of file +suitable for scripting, use the `--json` option. + +The `camel infra list` table now sizes its `DESCRIPTION` column to the terminal width, and truncates +the `IMPLEMENTATION` and `SERVICE_DATA` columns with an ellipsis instead of letting the raw service +data overflow the terminal. The complete, structured service data remains available via `--json`. diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java index 474cdb266f6aa..e59bfd51653ad 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java @@ -48,6 +48,7 @@ import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.dsl.jbang.core.commands.CamelCommand; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.CamelTableColumns; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper; import org.apache.camel.dsl.jbang.core.model.InfraBaseDTO; @@ -192,40 +193,54 @@ public int listServices(Consumer> serviceConsumer) throws IOException if (jsonOutput) { printer().println( Jsoner.serialize( - rows.stream().map(row -> { - Object serviceDataObj = null; - try { - serviceDataObj = Jsoner.deserialize(row.serviceData()); - } catch (DeserializationException e) { - // ignore - } - return new InfraBaseDTO(row.alias, row.aliasImplementation, row.description, serviceDataObj); - }) + rows.stream().map(row -> new InfraBaseDTO( + row.alias, row.aliasImplementation, row.description, + parseServiceData(row.serviceData()))) .map(InfraBaseDTO::toMap) .collect(Collectors.toList()))); } else { int tw = terminalWidth(); - // Fixed columns: PID (~8), ALIAS (width+2), SERVICE_DATA (~30), DESCRIPTION (~30) - int fixedWidth = (width + 2) + 30 + 30; - if (showPidColumn()) { - fixedWidth += 8; - } - int implWidth = TerminalWidthHelper.flexWidth( - tw, fixedWidth, TerminalWidthHelper.noBorderOverhead(showPidColumn() ? 5 : 4), - 20, 35); + // Size DESCRIPTION to fill the terminal: measure the other columns so it gets the exact remainder. + // IMPLEMENTATION is capped and SERVICE_DATA keeps a compact fixed width; both truncate with an ellipsis + // instead of overflowing the terminal (the full, structured service data is available via --json). + int serviceDataWidth = 30; + int aliasWidth = width + 2; + int pidWidth = showPidColumn() + ? CamelTableColumns.measure("PID", Integer.MAX_VALUE, rows, r -> r.pid) : 0; + int implWidth = CamelTableColumns.measure("IMPLEMENTATION", 35, rows, Row::aliasImplementation); + int overhead = TerminalWidthHelper.noBorderOverhead(showPidColumn() ? 5 : 4); + int descWidth = CamelTableColumns.lastColumnWidth( + tw, overhead, pidWidth, aliasWidth, implWidth, serviceDataWidth); printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList( new Column().header("PID").visible(showPidColumn()).headerAlign(HorizontalAlign.CENTER).with(r -> r.pid), - new Column().header("ALIAS").minWidth(width + 2).dataAlign(HorizontalAlign.LEFT) + new Column().header("ALIAS").minWidth(aliasWidth).dataAlign(HorizontalAlign.LEFT) .with(Row::alias), - new Column().header("IMPLEMENTATION").maxWidth(implWidth, OverflowBehaviour.NEWLINE) + new Column().header("IMPLEMENTATION").maxWidth(implWidth, OverflowBehaviour.ELLIPSIS_RIGHT) .dataAlign(HorizontalAlign.LEFT).with(Row::aliasImplementation), - new Column().header("DESCRIPTION").dataAlign(HorizontalAlign.LEFT).with(Row::description), - new Column().header("SERVICE_DATA").dataAlign(HorizontalAlign.LEFT).with(Row::serviceData)))); + CamelTableColumns.lastText("DESCRIPTION", descWidth).with(Row::description), + new Column().header("SERVICE_DATA").maxWidth(serviceDataWidth, OverflowBehaviour.ELLIPSIS_RIGHT) + .dataAlign(HorizontalAlign.LEFT).with(Row::serviceData)))); } return 0; } + /** + * Parses the raw service-data JSON string (read from the infra {@code .json} file) into a structured object so it + * is emitted as nested JSON by {@code --json}, rather than as an escaped string. Returns {@code null} (so the + * {@code serviceData} field is omitted) when there is no data or the stored content is not valid JSON. + */ + static Object parseServiceData(String serviceData) { + if (serviceData == null) { + return null; + } + try { + return Jsoner.deserialize(serviceData); + } catch (DeserializationException e) { + return null; + } + } + private String getServiceData(String key, String pid) { Path jsonFilePath = CommandLineHelper.getCamelDir().resolve(getJsonFileName(key, pid)); if (jsonFilePath.toFile().exists()) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraTest.java index f82c23ac25f3d..e8983b74be705 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraTest.java @@ -21,6 +21,8 @@ import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.model.InfraBaseDTO; +import org.apache.camel.util.json.Jsoner; import org.assertj.core.api.Assertions; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; @@ -69,4 +71,32 @@ public void listServices() throws Exception { Assertions.assertThat(output).contains("amqp"); Assertions.assertThat(output).contains("minio"); } + + @Test + public void serviceDataInJsonIsNestedNotEscaped() { + String rawServiceData = "{\"host\":\"localhost\",\"port\":61616}"; + Object serviceData = InfraBaseCommand.parseServiceData(rawServiceData); + + InfraBaseDTO dto = new InfraBaseDTO("artemis", "amqp", "AMQP broker", serviceData); + String json = Jsoner.serialize(List.of(dto.toMap())); + + // serviceData is embedded as a nested JSON object (so --json is machine-parseable), not a string-escaped blob + Assertions.assertThat(json).contains("\"serviceData\":{"); + Assertions.assertThat(json).contains("\"host\":\"localhost\""); + Assertions.assertThat(json).doesNotContain("\\\"host\\\""); + } + + @Test + public void malformedServiceDataIsOmittedFromJson() { + Object serviceData = InfraBaseCommand.parseServiceData("{not valid json"); + + // unparseable content is dropped rather than emitted as an escaped string + Assertions.assertThat(serviceData).isNull(); + + InfraBaseDTO dto = new InfraBaseDTO("artemis", "amqp", "AMQP broker", serviceData); + String json = Jsoner.serialize(List.of(dto.toMap())); + + Assertions.assertThat(json).contains("\"alias\":\"artemis\""); + Assertions.assertThat(json).doesNotContain("serviceData"); + } }