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 a8f3de30554f chore: camel-jbang - About has the links of the runtime:
a queue between the routes of an integration is a call or a hand-off, and no In
or Out (#29600)
a8f3de30554f is described below
commit a8f3de30554f0d8fb86dbfe04bbdcddc06e5a2de
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 13:14:50 2026 +0100
chore: camel-jbang - About has the links of the runtime: a queue between
the routes of an integration is a call or a hand-off, and no In or Out (#29600)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-observe.adoc | 4 +-
.../dsl/jbang/core/commands/tui/AboutView.java | 47 +++++-
.../dsl/jbang/core/commands/tui/RuntimeLinks.java | 181 +++++++++++++++++++++
.../src/main/resources/tui/help/overview.md | 6 +-
.../jbang/core/commands/tui/RuntimeLinksTest.java | 152 +++++++++++++++++
5 files changed, 381 insertions(+), 9 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index b4f00f5c1ec4..8450a29f878e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -30,7 +30,9 @@ README, else the descriptions of the routes, else the
overview an AI wrote in `c
and *s* the summary of the project.
* *In*, *Routes* and *Out* say what starts it (a timer or a cron with the
period or schedule of its properties, its
profile applied; the operations of an OpenAPI contract the REST DSL serves,
from the contract), its routes (a route called with `direct` sits under its
caller with `↳`,
-one handed off with `seda` or a `wireTap` with `⇢`) and what it talks to, with
the exchanges since it started and a sparkline of the
+one handed off with `seda` or a `wireTap` with `⇢`; for a running integration
as its route topology has it, so a
+queue or topic of a broker between its own routes is `↳` for a request-reply,
`⇢` for a hand-off and `on failure` for a
+dead letter queue, and no In or Out unless messages came from outside too) and
what it talks to, with the exchanges since it started and a sparkline of the
last 5 minutes. *Up* and *Down* select a line, and *Enter* opens a route
drilled down on the Diagram, or an endpoint with
its details on the Endpoint tab.
* *Last 5m* has the exchanges and how they ended, with the cause that happened
most, and *What happened* a strip per route
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
index 05bb6aba74dd..25399325e8c7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
@@ -90,6 +90,8 @@ final class AboutView {
private final MonitorContext ctx;
private volatile ProjectOverview.Overview overview;
private volatile About.Model model;
+ // how the routes of the running integration reach each other; null as the
sources have it
+ private volatile RuntimeLinks links;
private volatile String purpose;
private volatile String purposeFrom;
private volatile boolean loading;
@@ -130,6 +132,30 @@ final class AboutView {
this.ctx = ctx;
}
+ /**
+ * The links of the running integration (its route topology, as the
Diagram has it); null when it does not run, or
+ * does not answer, and the model is as the sources have it.
+ */
+ private RuntimeLinks runtimeLinks(IntegrationInfo info) {
+ if (info.phantom || info.pid == null || !alive(info.pid)) {
+ return null;
+ }
+ try {
+ JsonObject topology = ctx.diagramTopology.load(ctx, info.pid);
+ return topology != null ? RuntimeLinks.of(topology) : null;
+ } catch (RuntimeException e) {
+ return null;
+ }
+ }
+
+ private static boolean alive(String pid) {
+ try {
+ return
ProcessHandle.of(Long.parseLong(pid)).map(ProcessHandle::isAlive).orElse(false);
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
/** Reads the sources of the integration in the background, again when
they may have changed. */
private void load(IntegrationInfo info, Consumer<Runnable> onRenderThread)
{
Path d = info.directory != null ? Path.of(info.directory) : null;
@@ -152,9 +178,12 @@ final class AboutView {
Thread t = new Thread(() -> {
try {
ProjectOverview.Overview o = ProjectOverview.analyze(d,
ArchitectureView.catalog());
- About.Model m = About.of(d, o);
+ // how the routes reach each other as the runtime has it, a
hop over a broker included
+ RuntimeLinks rl = runtimeLinks(info);
+ About.Model m = rl != null ? rl.apply(About.of(d, o), info) :
About.of(d, o);
Path r = About.readme(d);
onRenderThread.accept(() -> {
+ links = rl;
readme = r;
overview = o;
model = m;
@@ -1034,7 +1063,8 @@ final class AboutView {
String how = r.how() == null ? "" : "call".equals(r.how()) ? "↳ "
: "⇢ ";
spans.add(Span.raw(" " + " ".repeat(r.depth()) + how));
spans.add(Span.styled(r.routeId(), Style.EMPTY.bold()));
- spans.add(Span.styled(" " + r.from() + " → " + r.steps() +
(r.steps() == 1 ? " step" : " steps"),
+ spans.add(Span.styled(" " + r.from() + " → " + r.steps() +
(r.steps() == 1 ? " step" : " steps")
+ + ("error".equals(r.how()) ? ", on failure"
: ""),
Theme.muted()));
Long total = totals.get(r.routeId());
if (total != null) {
@@ -1046,7 +1076,9 @@ final class AboutView {
// and the ones the integration sent to that its routes do not name (a
toD, a recipient list)
List<About.Out> outs = new ArrayList<>(m.out());
- outs.addAll(ObservedOuts.of(info, m.out()));
+ RuntimeLinks rl = links;
+ // a destination a route of the integration consumes is no system
outside
+ ObservedOuts.of(info, m.out()).stream().filter(s -> rl == null ||
!rl.consumedHere(s)).forEach(outs::add);
for (About.Out s : outs) {
if (spaced && !out.isEmpty()) {
out.add(Line.from(Span.raw("")));
@@ -1402,11 +1434,12 @@ final class AboutView {
? " (the last exchange " + ChaosTab.shortTime((end -
lastActive) * 1000) + " ago)"
: " (quiet)", Theme.muted()));
}
- // what the routes that something starts did since the integration
started, as their counters say
+ // what the routes that something from outside starts did since the
integration started, as their counters
+ // say: not a route a queue of the integration itself feeds
Set<String> entry = new LinkedHashSet<>();
- ProjectOverview.Overview o = overview;
- if (o != null) {
- o.entryPoints().forEach(e -> entry.add(e.route()));
+ About.Model shown = model;
+ if (shown != null) {
+ shown.in().forEach(e -> entry.add(e.route()));
}
long total = 0;
long totalFailed = 0;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinks.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinks.java
new file mode 100644
index 000000000000..8f4b50688f84
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinks.java
@@ -0,0 +1,181 @@
+/*
+ * 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.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.dsl.jbang.core.commands.ai.About;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * How the routes of a running integration reach each other, from its route
topology (CAMEL-26823), whatever the
+ * component: a route the sender waits for (direct, a request-reply over a JMS
queue) is a call, one it hands the
+ * message to (seda, a topic, a wire tap) a hand-off, and one it sends to only
when it handles a failure (a dead letter
+ * queue) an error path. The sources of the integration name the links by
their scheme only, and took a hop over a
+ * broker for a message from outside: jms-request-reply had its queues as In
and Out, and 14 exchanges for 3 orders.
+ */
+final class RuntimeLinks {
+
+ /** How a route reaches another: call, async (a hand-off) or error (only
when it handles a failure). */
+ record Link(String from, String to, String endpoint, String how) {
+ }
+
+ private static final int MAX_DEPTH = 6;
+
+ private final List<Link> links;
+
+ private RuntimeLinks(List<Link> links) {
+ this.links = links;
+ }
+
+ List<Link> links() {
+ return links;
+ }
+
+ /** The links between different routes in the topology of the
route-topology console. */
+ static RuntimeLinks of(JsonObject topology) {
+ List<Link> answer = new ArrayList<>();
+ Collection<?> edges = topology != null ?
topology.getCollection("edges") : null;
+ if (edges != null) {
+ for (Object o : edges) {
+ if (!(o instanceof JsonObject e)) {
+ continue;
+ }
+ String from = e.getString("fromRouteId");
+ String to = e.getString("toRouteId");
+ String endpoint = e.getString("endpoint");
+ if (from == null || to == null || endpoint == null ||
from.equals(to)) {
+ continue;
+ }
+ // a hand-off unless the route waits for the answer; an edge
the TUI adds for the sends the runtime saw
+ // (a recipient list) waits, as a call
+ String how = "error".equals(e.getString("kind")) ? "error"
+ : Boolean.TRUE.equals(e.get("reply")) ||
Boolean.TRUE.equals(e.get("observed")) ? "call" : "async";
+ answer.add(new Link(from, to, endpoint, how));
+ }
+ }
+ return new RuntimeLinks(answer);
+ }
+
+ /**
+ * The model of the sources with the links of the runtime: the routes
under the route that reaches them, In without
+ * an endpoint only the integration sends to (the rest of its messages,
from outside, as +N from outside), and Out
+ * without one only the integration consumes.
+ */
+ About.Model apply(About.Model m, IntegrationInfo info) {
+ if (links.isEmpty()) {
+ return m;
+ }
+ return new About.Model(m.purpose(), m.purposeFrom(), in(m.in(), info),
routes(m.routes()), out(m.out()));
+ }
+
+ private List<About.In> in(List<About.In> in, IntegrationInfo info) {
+ List<About.In> answer = new ArrayList<>();
+ for (About.In e : in) {
+ String key = DiagramTopology.endpointKey(e.label());
+ Link fed = links.stream()
+ .filter(l -> l.to().equals(e.route()) &&
key.equals(DiagramTopology.endpointKey(l.endpoint())))
+ .findFirst().orElse(null);
+ if (fed == null) {
+ answer.add(e);
+ continue;
+ }
+ // what came in beyond what the routes of the integration sent
there came from outside
+ long outside = hits(info, key, "in", e.route()) - hits(info, key,
"out", null);
+ if (outside > 0) {
+ answer.add(new About.In(e.kind(), e.label() + " +" + outside
+ " from outside", e.route()));
+ }
+ }
+ return answer;
+ }
+
+ private static long hits(IntegrationInfo info, String key, String
direction, String route) {
+ long answer = 0;
+ for (EndpointInfo ep : info.endpoints) {
+ if (direction.equals(ep.direction) && ep.uri != null && (route ==
null || route.equals(ep.routeId))
+ && key.equals(DiagramTopology.endpointKey(ep.uri))) {
+ answer += ep.hits;
+ }
+ }
+ return answer;
+ }
+
+ private List<About.Out> out(List<About.Out> out) {
+ return out.stream().filter(o -> !consumedHere(o)).toList();
+ }
+
+ /** Whether the destination of an out is one a route of the integration
consumes from: a hop, no system outside. */
+ boolean consumedHere(About.Out o) {
+ String key = DiagramTopology.endpointKey(o.uri());
+ return links.stream()
+ .anyMatch(l -> l.from().equals(o.route()) &&
key.equals(DiagramTopology.endpointKey(l.endpoint())));
+ }
+
+ private List<About.RouteLine> routes(List<About.RouteLine> lines) {
+ Map<String, About.RouteLine> byId = new LinkedHashMap<>();
+ lines.forEach(r -> byId.putIfAbsent(r.routeId(), r));
+ Map<String, List<Link>> from = new HashMap<>();
+ Set<String> reached = new LinkedHashSet<>();
+ for (Link l : links) {
+ if (byId.containsKey(l.from()) && byId.containsKey(l.to())) {
+ from.computeIfAbsent(l.from(), k -> new ArrayList<>()).add(l);
+ reached.add(l.to());
+ }
+ }
+ List<About.RouteLine> answer = new ArrayList<>();
+ Set<String> shown = new LinkedHashSet<>();
+ for (String id : byId.keySet()) {
+ if (!reached.contains(id)) {
+ add(byId.get(id), 0, null, byId, from, shown, answer);
+ }
+ }
+ // what is left: routes reached only from a cycle
+ for (String id : byId.keySet()) {
+ add(byId.get(id), 0, null, byId, from, shown, answer);
+ }
+ return answer;
+ }
+
+ private static void add(
+ About.RouteLine r, int depth, String how, Map<String,
About.RouteLine> byId, Map<String, List<Link>> from,
+ Set<String> shown, List<About.RouteLine> answer) {
+ if (!shown.add(r.routeId()) || depth > MAX_DEPTH) {
+ return;
+ }
+ answer.add(new About.RouteLine(r.routeId(), depth, how, r.from(),
r.steps()));
+ for (Link l : from.getOrDefault(r.routeId(), List.of())) {
+ add(byId.get(l.to()), depth + 1, l.how(), byId, from, shown,
answer);
+ }
+ }
+
+ /** How each route that another one reaches is reached: call, async or
error; none for the others. */
+ Map<String, String> howReached() {
+ Map<String, String> answer = new HashMap<>();
+ for (Link l : links) {
+ // a route called by one and handed to by another: the call keeps
it inside the exchange of its caller
+ answer.merge(l.to(), l.how(), (a, b) -> "call".equals(a) ||
"call".equals(b) ? "call" : a);
+ }
+ return answer;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index 7ca5cef11bf8..4ed570918470 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -25,7 +25,11 @@ happened to it, with no AI:
- **In, Routes, Out** — what starts it (timers, HTTP, consumers, the
operations of an OpenAPI contract the REST DSL serves), its
routes (a route called with `direct:` sits under its caller with `↳`,
- one handed off with `seda:` or a `wireTap` with `⇢`) and what it talks to,
from its
+ one handed off with `seda:` or a `wireTap` with `⇢`; for a running
+ integration as its route topology has it, so a queue or topic between
+ its own routes is `↳` for a request-reply, `⇢` for a hand-off, and
+ `on failure` for a dead letter queue, and is no In or Out, unless
+ messages came from outside too: `+5 from outside`) and what it talks to,
from its
sources. Each line has the exchanges since it started (for an
endpoint, the sends to it), and In and Out a sparkline of the last 5
minutes with the rate now, or `idle`. Out also has the systems the
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinksTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinksTest.java
new file mode 100644
index 000000000000..db541c299065
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RuntimeLinksTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.List;
+
+import org.apache.camel.dsl.jbang.core.commands.ai.About;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * connect-service/jms-request-reply: order-intake puts each order on a queue,
reserve-order asks stock-service over JMS
+ * request-reply, publishes the reserved order on a topic for shipping and
invoicing, and its dead letter channel moves
+ * the order it cannot reserve to a dead letter queue. The sources name the
links by their scheme only: every queue was
+ * an In and an Out of the integration.
+ */
+class RuntimeLinksTest {
+
+ private static About.Model sources() {
+ List<About.In> in = List.of(
+ new About.In("file", "file:orders", "order-intake"),
+ new About.In("jms", "jms:queue:orders", "reserve-order"),
+ new About.In("jms", "jms:queue:stock", "stock-service"),
+ new About.In("jms", "jms:queue:orders.dead", "dead-orders"),
+ new About.In("jms", "jms:topic:orders.reserved", "shipping"),
+ new About.In("jms", "jms:topic:orders.reserved", "invoicing"));
+ List<About.RouteLine> routes = List.of(
+ new About.RouteLine("order-intake", 0, null, "file", 4),
+ new About.RouteLine("reserve-order", 0, null, "jms", 6),
+ new About.RouteLine("stock-service", 0, null, "jms", 5),
+ new About.RouteLine("dead-orders", 0, null, "jms", 1),
+ new About.RouteLine("shipping", 0, null, "jms", 1),
+ new About.RouteLine("invoicing", 0, null, "jms", 1));
+ List<About.Out> out = List.of(
+ new About.Out("messaging", "jms:queue:orders", "out",
"order-intake"),
+ new About.Out("messaging", "jms:queue:stock", "in/out",
"reserve-order"),
+ new About.Out("messaging", "jms:topic:orders.reserved", "out",
"reserve-order"),
+ new About.Out("messaging", "jms:queue:orders.dead", "out",
"reserve-order"));
+ return new About.Model("Each order asks a stock service", "from the
comment", in, routes, out);
+ }
+
+ private static JsonObject topology() {
+ JsonArray edges = new JsonArray();
+ edges.add(edge("order-intake", "reserve-order", "jms:queue:orders",
"call", false));
+ edges.add(edge("reserve-order", "stock-service", "jms:queue:stock",
"call", true));
+ edges.add(edge("reserve-order", "shipping",
"jms:topic:orders.reserved", "call", false));
+ edges.add(edge("reserve-order", "invoicing",
"jms:topic:orders.reserved", "call", false));
+ edges.add(edge("reserve-order", "dead-orders",
"jms:queue:orders.dead", "error", false));
+ JsonObject topology = new JsonObject();
+ topology.put("edges", edges);
+ return topology;
+ }
+
+ private static JsonObject edge(String from, String to, String endpoint,
String kind, boolean reply) {
+ JsonObject e = new JsonObject();
+ e.put("fromRouteId", from);
+ e.put("toRouteId", to);
+ e.put("endpoint", endpoint);
+ e.put("kind", kind);
+ // the runtime writes reply on every edge
+ e.put("reply", reply);
+ return e;
+ }
+
+ @Test
+ void theRoutesAreUnderTheRouteThatReachesThem() {
+ About.Model m = RuntimeLinks.of(topology()).apply(sources(), new
IntegrationInfo());
+
+ assertThat(m.routes()).extracting(r -> " ".repeat(r.depth()) +
(r.how() != null ? r.how() + " " : "") + r.routeId())
+ .containsExactly(
+ "order-intake",
+ " async reserve-order",
+ " call stock-service",
+ " async shipping",
+ " async invoicing",
+ " error dead-orders");
+ }
+
+ @Test
+ void inAndOutAreWhatCrossesTheIntegration() {
+ About.Model m = RuntimeLinks.of(topology()).apply(sources(), new
IntegrationInfo());
+
+
assertThat(m.in()).extracting(About.In::label).containsExactly("file:orders");
+ assertThat(m.out()).isEmpty();
+ }
+
+ @Test
+ void anOutTheRuntimeSawOnAQueueOfTheIntegrationIsNoSystemOutside() {
+ RuntimeLinks links = RuntimeLinks.of(topology());
+
+ // as the runtime names it, with // after the scheme
+ assertThat(links.consumedHere(new About.Out("messaging",
"jms://queue:orders", "out", "order-intake"))).isTrue();
+ assertThat(links.consumedHere(new About.Out("messaging",
"jms://queue:billing", "out", "order-intake"))).isFalse();
+ }
+
+ @Test
+ void aQueueOthersSendToTooStaysAnInWithWhatTheySent() {
+ // 3 orders from order-intake, and 5 more another application put on
the queue
+ IntegrationInfo info = new IntegrationInfo();
+ info.endpoints.add(endpoint("order-intake", "out",
"jms://queue:orders", 3));
+ info.endpoints.add(endpoint("reserve-order", "in",
"jms://queue:orders", 8));
+
+ About.Model m = RuntimeLinks.of(topology()).apply(sources(), info);
+
+
assertThat(m.in()).extracting(About.In::label).containsExactly("file:orders",
"jms:queue:orders +5 from outside");
+ }
+
+ @Test
+ void anEdgeWithoutAnEndpointIsLeftOut() {
+ JsonObject topology = topology();
+ JsonArray edges = (JsonArray) topology.get("edges");
+ edges.add(edge("shipping", "invoicing", null, "call", true));
+
+
assertThat(RuntimeLinks.of(topology).howReached()).doesNotContainEntry("invoicing",
"call");
+ }
+
+ @Test
+ void withoutATopologyTheSourcesStay() {
+ About.Model m = sources();
+
+ assertThat(RuntimeLinks.of(null).apply(m, new
IntegrationInfo())).isSameAs(m);
+
assertThat(RuntimeLinks.of(topology()).howReached()).containsEntry("stock-service",
"call")
+ .containsEntry("shipping",
"async").containsEntry("dead-orders", "error")
+ .doesNotContainKey("order-intake");
+ }
+
+ private static EndpointInfo endpoint(String route, String direction,
String uri, long hits) {
+ EndpointInfo ep = new EndpointInfo();
+ ep.routeId = route;
+ ep.direction = direction;
+ ep.uri = uri;
+ ep.hits = hits;
+ return ep;
+ }
+}