This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new ec523aec4683 CAMEL-24259: camel-tui - Add Type Converters tab
ec523aec4683 is described below
commit ec523aec46831b87cddd5423d4adf67e71e1efcb
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 27 19:46:50 2026 +0200
CAMEL-24259: camel-tui - Add Type Converters tab
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/spi/TypeConverterRegistry.java | 11 +
.../impl/converter/CoreTypeConverterRegistry.java | 12 +
.../camel/impl/console/TypeConverterConsole.java | 16 +-
.../camel/cli/connector/LocalCliConnector.java | 14 +
.../dsl/jbang/core/commands/tui/TabRegistry.java | 7 +-
.../dsl/jbang/core/commands/tui/TuiIcons.java | 1 +
.../jbang/core/commands/tui/TypeConvertersTab.java | 410 +++++++++++++++++++++
7 files changed, 469 insertions(+), 2 deletions(-)
diff --git
a/core/camel-api/src/main/java/org/apache/camel/spi/TypeConverterRegistry.java
b/core/camel-api/src/main/java/org/apache/camel/spi/TypeConverterRegistry.java
index 887b197f08fb..d48acb05e478 100644
---
a/core/camel-api/src/main/java/org/apache/camel/spi/TypeConverterRegistry.java
+++
b/core/camel-api/src/main/java/org/apache/camel/spi/TypeConverterRegistry.java
@@ -179,6 +179,17 @@ public interface TypeConverterRegistry extends
StaticService, CamelContextAware
*/
int size();
+ /**
+ * Lists all registered type converters with their from-to type pairs.
+ *
+ * @return an unmodifiable map of all registered type convertible pairs to
their converters, or an empty map if not
+ * supported.
+ * @since 4.22
+ */
+ default Map<TypeConvertible<?, ?>, TypeConverter> listTypeConverters() {
+ return Map.of();
+ }
+
/**
* The logging level to use when logging that a type converter already
exists when attempting to add a duplicate
* type converter.
diff --git
a/core/camel-base/src/main/java/org/apache/camel/impl/converter/CoreTypeConverterRegistry.java
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/CoreTypeConverterRegistry.java
index 082e83ed2762..736bca82c758 100644
---
a/core/camel-base/src/main/java/org/apache/camel/impl/converter/CoreTypeConverterRegistry.java
+++
b/core/camel-base/src/main/java/org/apache/camel/impl/converter/CoreTypeConverterRegistry.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.impl.converter;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -625,6 +626,17 @@ public abstract class CoreTypeConverterRegistry extends
ServiceSupport implement
return converters.size();
}
+ @Override
+ public Map<TypeConvertible<?, ?>, TypeConverter> listTypeConverters() {
+ Map<TypeConvertible<?, ?>, TypeConverter> answer = new
LinkedHashMap<>();
+ for (var e : converters.entrySet()) {
+ if (e.getValue() != MISS_CONVERTER) {
+ answer.put(e.getKey(), e.getValue());
+ }
+ }
+ return Collections.unmodifiableMap(answer);
+ }
+
public LoggingLevel getTypeConverterExistsLoggingLevel() {
return typeConverterExistsLoggingLevel;
}
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/TypeConverterConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/TypeConverterConsole.java
index e44c13ba5820..1d3504fc4e0c 100644
---
a/core/camel-console/src/main/java/org/apache/camel/impl/console/TypeConverterConsole.java
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/TypeConverterConsole.java
@@ -18,9 +18,12 @@ package org.apache.camel.impl.console;
import java.util.Map;
+import org.apache.camel.TypeConverter;
import org.apache.camel.spi.TypeConverterRegistry;
+import org.apache.camel.spi.TypeConvertible;
import org.apache.camel.spi.annotations.DevConsole;
import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
@DevConsole(name = "type-converters", description = "Camel Type Converter
information")
@@ -65,12 +68,23 @@ public class TypeConverterConsole extends
AbstractDevConsole {
statistics.computeIfEnabled(statistics::getHitCounter, v ->
props.put("hitCounter", v));
statistics.computeIfEnabled(statistics::getMissCounter, v ->
props.put("missCounter", v));
statistics.computeIfEnabled(statistics::getFailedCounter, v ->
props.put("failedCounter", v));
- statistics.computeIfEnabled(statistics::getFailedCounter, v ->
props.put("noopCounter", v));
+ statistics.computeIfEnabled(statistics::getNoopCounter, v ->
props.put("noopCounter", v));
if (!props.isEmpty()) {
root.put("statistics", props);
}
+ JsonArray arr = new JsonArray();
+ for (Map.Entry<TypeConvertible<?, ?>, TypeConverter> e :
reg.listTypeConverters().entrySet()) {
+ TypeConvertible<?, ?> tc = e.getKey();
+ JsonObject jo = new JsonObject();
+ jo.put("from", tc.getFrom().getCanonicalName());
+ jo.put("to", tc.getTo().getCanonicalName());
+ jo.put("converterClass", e.getValue().getClass().getName());
+ arr.add(jo);
+ }
+ root.put("converters", arr);
+
return root;
}
}
diff --git
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index 5b7d11c61dd4..4da6c850f7b3 100644
---
a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++
b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -392,6 +392,8 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
doActionCliDebug(root);
} else if ("spring-boot-configuration".equals(action)) {
doActionSpringBootConfigurationTask(root);
+ } else if ("type-converters".equals(action)) {
+ doActionTypeConvertersTask();
}
} catch (Exception e) {
LOG.warn("Error executing action: {} due to: {}. This exception is
ignored.", action != null ? action : af,
@@ -854,6 +856,18 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
}
}
+ private void doActionTypeConvertersTask() throws IOException {
+ DevConsole dc =
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+ .resolveById("type-converters");
+ if (dc != null) {
+ JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON);
+ LOG.trace("Updating output file: {}", outputFile);
+ IOHelper.writeText(json.toJson(), outputFile);
+ } else {
+ IOHelper.writeText("{}", outputFile);
+ }
+ }
+
private void doActionJvmTask() throws IOException {
DevConsole dc =
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
.resolveById("jvm");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 803f174b7f97..19501cef973e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -105,6 +105,7 @@ class TabRegistry {
private SqlQueryTab sqlQueryTab;
private InternalTasksTab internalTasksTab;
private SqlTraceTab sqlTraceTab;
+ private TypeConvertersTab typeConvertersTab;
private MonitorTab activeMoreTab;
private List<MoreTab> moreTabs;
@@ -132,6 +133,7 @@ class TabRegistry {
sqlQueryTab = new SqlQueryTab(ctx);
internalTasksTab = new InternalTasksTab(ctx);
sqlTraceTab = new SqlTraceTab(ctx);
+ typeConvertersTab = new TypeConvertersTab(ctx);
endpointsTab = new EndpointsTab(ctx, dataService.metrics());
networkTab = new NetworkTab(ctx, dataService.metrics());
httpTab = new HttpTab(ctx);
@@ -233,7 +235,10 @@ class TabRegistry {
new MoreTab(
TuiIcons.TAB_MAVEN_DEPENDENCIES, "Maven Dependencies",
"&Maven Dependencies",
mavenDependenciesTab,
- "Project"));
+ "Project"),
+ new MoreTab(
+ TuiIcons.TAB_TYPE_CONVERTERS, "Type Converters",
"T&ype Converters",
+ typeConvertersTab, "Project"));
}
// ---- Tab access ----
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 6c9c186ce06f..55e6e4f60db0 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -152,6 +152,7 @@ final class TuiIcons {
static final String TAB_EVENTS = "📣";
static final String TAB_INTERNAL_TASKS = "🔩";
static final String TAB_ROUTE_CONTROLLER = "🚦";
+ static final String TAB_TYPE_CONVERTERS = "🔄";
/** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link
TabRegistry#TAB_MORE} (in order). */
static final List<String> PRIMARY_TAB_ICONS = List.of(
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
new file mode 100644
index 000000000000..2d6045d2329a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TypeConvertersTab.java
@@ -0,0 +1,410 @@
+/*
+ * 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.commands.tui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class TypeConvertersTab extends AbstractTableTab {
+
+ private static final String[] SCOPES = { "all", "non-jdk", "custom",
"core" };
+
+ private final AtomicBoolean loading = new AtomicBoolean(false);
+
+ private int scopeIndex;
+ private boolean filterInputActive;
+ private TextInputState filterInputState = new TextInputState("");
+ private String filterTerm;
+ private List<ConverterData> allConverters = Collections.emptyList();
+ private String lastPid;
+
+ TypeConvertersTab(MonitorContext ctx) {
+ super(ctx, "from", "to");
+ }
+
+ @Override
+ protected int getRowCount() {
+ return sortedConverters().size();
+ }
+
+ @Override
+ public void onTabSelected() {
+ String pid = ctx.selectedPid;
+ if (pid != null && !pid.equals(lastPid)) {
+ lastPid = pid;
+ allConverters = Collections.emptyList();
+ }
+ if (allConverters.isEmpty()) {
+ loadConverters();
+ }
+ }
+
+ @Override
+ public void onIntegrationChanged() {
+ allConverters = Collections.emptyList();
+ lastPid = null;
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (filterInputActive) {
+ return handleFilterInput(ke);
+ }
+ if (ke.isChar('/')) {
+ filterInputActive = true;
+ filterInputState = new TextInputState(filterTerm != null ?
filterTerm : "");
+ return true;
+ }
+ return super.handleKeyEvent(ke);
+ }
+
+ private boolean handleFilterInput(KeyEvent ke) {
+ if (ke.isKey(KeyCode.ESCAPE)) {
+ filterInputActive = false;
+ return true;
+ }
+ if (ke.isConfirm()) {
+ String text = filterInputState.text().trim();
+ filterTerm = text.isEmpty() ? null : text;
+ filterInputActive = false;
+ tableState.select(0);
+ return true;
+ }
+ FormHelper.handleTextInput(ke, filterInputState);
+ return true;
+ }
+
+ @Override
+ public boolean handleEscape() {
+ if (filterTerm != null) {
+ filterTerm = null;
+ tableState.select(0);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean handleTabKeyEvent(KeyEvent ke) {
+ if (ke.isCharIgnoreCase('f')) {
+ scopeIndex = (scopeIndex + 1) % SCOPES.length;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
+ if (loading.get() && allConverters.isEmpty()) {
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(Span.styled(" Loading
type converters...", Style.EMPTY.dim()))))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Type Converters ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ List<ConverterData> visible = sortedConverters();
+ renderTable(frame, area, visible);
+ }
+
+ private void renderTable(Frame frame, Rect area, List<ConverterData>
visible) {
+ List<Row> rows = new ArrayList<>();
+ for (ConverterData c : visible) {
+ boolean core = isCoreConverter(c);
+ String kindLabel = core ? "core" : "custom";
+ Style kindStyle = core ? Style.EMPTY.dim() :
Style.EMPTY.fg(Theme.baseFg());
+ rows.add(Row.from(
+ Cell.from(Span.styled(displayName(c.from),
Style.EMPTY.fg(Theme.accent()))),
+ Cell.from(Span.styled(displayName(c.to), Style.EMPTY)),
+ Cell.from(Span.styled(kindLabel, kindStyle))));
+ }
+
+ if (rows.isEmpty()) {
+ rows.add(emptyRow("No type converters", 3));
+ }
+
+ String scope = SCOPES[scopeIndex];
+ String title = filterTerm != null
+ ? String.format(" Type Converters [%d/%d] scope:%s
filter:\"%s\" ", visible.size(), allConverters.size(),
+ scope, filterTerm)
+ : String.format(" Type Converters [%d/%d] scope:%s ",
visible.size(), allConverters.size(), scope);
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled(sortLabel("FROM", "from"),
sortStyle("from"))),
+ Cell.from(Span.styled(sortLabel("TO", "to"),
sortStyle("to"))),
+ Cell.from(Span.styled("KIND", Style.EMPTY))))
+ .widths(
+ Constraint.percentage(40),
+ Constraint.percentage(40),
+ Constraint.fill())
+ .highlightStyle(Theme.selectionBg())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+ .build();
+
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderScrollbar(frame, visible.size());
+ }
+
+ @Override
+ public void renderFooter(List<Span> spans) {
+ if (filterInputActive) {
+ spans.add(Span.styled(" /", Theme.label().bold()));
+ spans.add(Span.raw(filterInputState.text() + "â–ˆ "));
+ hint(spans, "Enter", "filter");
+ hintLast(spans, "Esc", "cancel");
+ return;
+ }
+ hint(spans, "Esc", filterTerm != null ? "clear" : "back");
+ hint(spans, "f", "scope [" + SCOPES[scopeIndex] + "]");
+ if (filterTerm != null) {
+ spans.add(Span.styled(" /", Theme.label().bold()));
+ spans.add(Span.raw("\"" + filterTerm + "\" "));
+ } else {
+ hint(spans, "/", "filter");
+ }
+ hint(spans, TuiIcons.HINT_SCROLL, "navigate");
+ hintLast(spans, "s", "sort");
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ List<ConverterData> visible = sortedConverters();
+ if (visible.isEmpty()) {
+ return null;
+ }
+ List<String> items = visible.stream().map(c -> c.from + " -> " +
c.to).toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Type Converters");
+ }
+
+ private List<ConverterData> sortedConverters() {
+ String scope = SCOPES[scopeIndex];
+ String ft = filterTerm != null ? filterTerm.toLowerCase() : null;
+ List<ConverterData> result = new ArrayList<>();
+ for (ConverterData c : allConverters) {
+ if ("non-jdk".equals(scope) && isJdkOnly(c)) {
+ continue;
+ }
+ if ("custom".equals(scope) && isCoreConverter(c)) {
+ continue;
+ }
+ if ("core".equals(scope) && !isCoreConverter(c)) {
+ continue;
+ }
+ if (ft != null && !matchesFilter(c, ft)) {
+ continue;
+ }
+ result.add(c);
+ }
+ result.sort((a, b) -> {
+ int cmp = switch (sort) {
+ case "to" -> compareStr(a.to, b.to);
+ default -> compareStr(a.from, b.from);
+ };
+ return sortReversed ? -cmp : cmp;
+ });
+ return result;
+ }
+
+ private static boolean isJdkType(String name) {
+ if (name == null) {
+ return false;
+ }
+ return name.startsWith("java.") || name.startsWith("javax.") ||
name.startsWith("org.w3c.")
+ || name.startsWith("org.xml.")
+ || !name.contains(".");
+ }
+
+ private static boolean isJdkOnly(ConverterData c) {
+ return isJdkType(c.from) && isJdkType(c.to);
+ }
+
+ private static boolean isCoreConverter(ConverterData c) {
+ return c.converterClass != null &&
c.converterClass.startsWith("org.apache.camel");
+ }
+
+ private static boolean matchesFilter(ConverterData c, String filter) {
+ if (c.from != null && c.from.toLowerCase().contains(filter)) {
+ return true;
+ }
+ return c.to != null && c.to.toLowerCase().contains(filter);
+ }
+
+ private static String displayName(String fqcn) {
+ if (fqcn == null) {
+ return "";
+ }
+ if (fqcn.startsWith("java.lang.")) {
+ return fqcn.substring("java.lang.".length());
+ }
+ return fqcn;
+ }
+
+ private void loadConverters() {
+ if (ctx.selectedPid == null || ctx.runner == null) {
+ return;
+ }
+ if (!loading.compareAndSet(false, true)) {
+ return;
+ }
+ String pid = ctx.selectedPid;
+ ctx.backgroundExecutor.execute(() -> {
+ try {
+ loadConvertersInBackground(pid);
+ } finally {
+ loading.set(false);
+ }
+ });
+ }
+
+ private void loadConvertersInBackground(String pid) {
+ JsonObject root = new JsonObject();
+ root.put("action", "type-converters");
+
+ JsonObject jo = ctx.executeAction(pid, root, 5000);
+ if (jo == null) {
+ return;
+ }
+
+ JsonArray arr = jo.getCollection("converters");
+ if (arr == null) {
+ return;
+ }
+
+ List<ConverterData> result = new ArrayList<>();
+ for (int i = 0; i < arr.size(); i++) {
+ JsonObject cj = (JsonObject) arr.get(i);
+ ConverterData cd = new ConverterData();
+ cd.from = cj.getString("from");
+ cd.to = cj.getString("to");
+ cd.converterClass = cj.getString("converterClass");
+ result.add(cd);
+ }
+
+ if (ctx.runner != null) {
+ ctx.runner.runOnRenderThread(() -> {
+ allConverters = result;
+ lastPid = pid;
+ });
+ }
+ }
+
+ static class ConverterData {
+ String from;
+ String to;
+ String converterClass;
+ }
+
+ @Override
+ public String description() {
+ return "Registered type converters in the Camel context";
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # Type Converters
+
+ The Type Converters tab shows all type converters registered
in the
+ Camel type converter registry. Type converters are used to
automatically
+ convert message bodies, headers, and other values between Java
types
+ during routing.
+
+ Camel ships with ~230 built-in (core) type converters that
handle
+ common conversions like String to Integer, byte[] to
InputStream,
+ Document to String, etc. Components can also register
additional
+ converters for their own types.
+
+ ## Table Columns
+
+ - **FROM** — Source Java type
+ - **TO** — Target Java type
+ - **KIND** — `core` (shipped with Camel) or `custom` (added by
components or user code)
+
+ ## Scope Modes
+
+ Press `f` to cycle through scope modes:
+
+ - **all** — show all type converters
+ - **non-jdk** — hide converters where both types are JDK
classes (java.*, javax.*, org.w3c.*, org.xml.*)
+ - **custom** — only converters added by custom components or
user code
+ - **core** — only Camel core converters (shipped with Apache
Camel)
+
+ ## Keys
+
+ - `Up/Down` — select converter
+ - `s` — cycle sort column
+ - `S` — reverse sort order
+ - `f` — cycle scope filter
+ - `/` — text filter by class name
+ - `Esc` — clear filter / back
+ """;
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ List<ConverterData> converters = sortedConverters();
+ if (converters.isEmpty()) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "Type Converters");
+ JsonArray rows = new JsonArray();
+ for (ConverterData c : converters) {
+ JsonObject row = new JsonObject();
+ row.put("from", c.from);
+ row.put("to", c.to);
+ rows.add(row);
+ }
+ result.put("rows", rows);
+ result.put("totalRows", converters.size());
+ Integer sel = tableState.selected();
+ result.put("selectedIndex", sel != null ? sel : -1);
+ return result;
+ }
+}