This is an automated email from the ASF dual-hosted git repository.
oscerd 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 f038136730cb CAMEL-23600: camel-milo - expose the underlying milo
OpcUaClient for custom DataType encoding/decoding (#23456)
f038136730cb is described below
commit f038136730cb625d8ea4daa3e59be3f745cd07a4
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri May 22 18:00:39 2026 +0200
CAMEL-23600: camel-milo - expose the underlying milo OpcUaClient for custom
DataType encoding/decoding (#23456)
Values of custom (server-defined) OPC UA data types are returned as
ExtensionObject and cannot be decoded/encoded without an EncodingContext
from the milo OpcUaClient, which was held privately inside
SubscriptionManager.Connected and unreachable from outside.
Add MiloClientConnection.getOpcUaClient() (delegating through
SubscriptionManager) so users can obtain the client and its
static/dynamic encoding contexts to encode/decode ExtensionObject values
themselves. The accessor is purely additive and returns null while the
connection is not established (lazy/async, re-created on reconnect); the
client lifecycle stays owned by Camel.
Includes a test exercising an encode/decode round-trip via the exposed
context, and a documentation section on the client component page.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
.../src/main/docs/milo-client-component.adoc | 34 +++++++++
.../milo/client/MiloClientConnection.java | 24 ++++++
.../milo/client/internal/SubscriptionManager.java | 14 ++++
.../milo/MiloClientOpcUaClientAccessTest.java | 88 ++++++++++++++++++++++
4 files changed, 160 insertions(+)
diff --git a/components/camel-milo/src/main/docs/milo-client-component.adoc
b/components/camel-milo/src/main/docs/milo-client-component.adoc
index 7833d9da2502..6b4ac33047dd 100644
--- a/components/camel-milo/src/main/docs/milo-client-component.adoc
+++ b/components/camel-milo/src/main/docs/milo-client-component.adoc
@@ -146,6 +146,40 @@ from("direct:start")
}).to("mock:test1");
----
+=== Custom data types: accessing the underlying milo OpcUaClient
+
+Built-in OPC UA data types are read and written transparently. Values whose
node has a *custom* (server-defined)
+data type are instead returned as an
`org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject`, which can
only
+be decoded (or, for writes, encoded) with an `EncodingContext` obtained from
the underlying milo client.
+
+The component does not decode these values automatically, because it cannot
reliably determine which encoding context
+applies. Instead, the active `OpcUaClient` is exposed through
`MiloClientConnection.getOpcUaClient()`, so you can obtain
+its encoding contexts and perform the encode/decode yourself:
+
+[source,java]
+----
+MiloClientEndpoint endpoint =
context.getEndpoint("milo-client:opc.tcp://localhost:4334",
MiloClientEndpoint.class);
+MiloClientConnection connection = endpoint.createConnection();
+try {
+ OpcUaClient client = connection.getOpcUaClient();
+ // dynamic context resolves custom, server-defined types; static context
covers built-in/generated types
+ EncodingContext encodingContext = client.getDynamicEncodingContext();
+
+ // decode a value read as ExtensionObject
+ UaStructuredType decoded = extensionObject.decode(encodingContext);
+
+ // or encode a structure before writing it
+ ExtensionObject toWrite = ExtensionObject.encode(encodingContext,
myStructure);
+} finally {
+ endpoint.releaseConnection(connection);
+}
+----
+
+NOTE: The connection is established lazily and asynchronously, so
`getOpcUaClient()` returns `null` until the client is
+connected, and again briefly while a connection is being re-established. The
client is owned and managed by Camel: do
+not connect, disconnect or otherwise change its lifecycle, and fetch it on
demand rather than caching the reference, as
+a reconnect replaces the instance.
+
=== Security policies
When setting the allowing security policies is it possible to use the well
known OPC UA URIs (e.g.
`\http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15`)
diff --git
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
index e3766528336e..146158217690 100644
---
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
+++
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
@@ -25,6 +25,7 @@ import java.util.function.Consumer;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.milo.client.internal.SubscriptionManager;
+import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.Stack;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
@@ -58,6 +59,29 @@ public class MiloClientConnection implements AutoCloseable {
return configuration;
}
+ /**
+ * Returns the underlying Eclipse Milo {@link OpcUaClient} backing this
connection.
+ * <p>
+ * This is intended for advanced scenarios that the component does not
cover directly, most notably encoding and
+ * decoding of custom (server-defined) OPC UA data types: the returned
client exposes the
+ * {@link OpcUaClient#getStaticEncodingContext() static} and {@link
OpcUaClient#getDynamicEncodingContext() dynamic}
+ * encoding contexts required to encode/decode {@code ExtensionObject}
values.
+ * <p>
+ * Notes:
+ * <ul>
+ * <li>The connection is established lazily and asynchronously, so this
method may return {@code null} until the
+ * client is connected, and again briefly while a connection is being
re-established.</li>
+ * <li>The client is owned and managed by Camel. Callers must not connect,
disconnect or otherwise alter its
+ * lifecycle. A reconnect replaces the instance, so fetch it on demand
rather than caching the reference.</li>
+ * </ul>
+ *
+ * @return the active milo client, or {@code null} if the connection is
not currently established
+ */
+ public OpcUaClient getOpcUaClient() {
+ checkInit();
+ return this.manager.getOpcUaClient();
+ }
+
protected void init() {
this.manager = new SubscriptionManager(this.configuration,
Stack.sharedScheduledExecutor(), 10_000);
}
diff --git
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
index bce5c27401ce..b5b14bbcddb2 100644
---
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
+++
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
@@ -601,6 +601,20 @@ public class SubscriptionManager {
connect();
}
+ /**
+ * Returns the milo {@link OpcUaClient} of the currently established
connection, if any.
+ *
+ * @return the active client, or {@code null} if there is no current
connection
+ */
+ public OpcUaClient getOpcUaClient() {
+ lock.lock();
+ try {
+ return this.connected != null ? this.connected.client : null;
+ } finally {
+ lock.unlock();
+ }
+ }
+
private void handleConnectionFailure(final Throwable e) {
lock.lock();
try {
diff --git
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MiloClientOpcUaClientAccessTest.java
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MiloClientOpcUaClientAccessTest.java
new file mode 100644
index 000000000000..d44461d7c075
--- /dev/null
+++
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MiloClientOpcUaClientAccessTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.component.milo;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.milo.client.MiloClientConnection;
+import org.apache.camel.component.milo.client.MiloClientEndpoint;
+import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
+import org.eclipse.milo.opcua.stack.core.AttributeId;
+import org.eclipse.milo.opcua.stack.core.encoding.EncodingContext;
+import org.eclipse.milo.opcua.stack.core.types.UaStructuredType;
+import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
+import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
+import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Verifies that the underlying milo {@link OpcUaClient} can be obtained from
a {@link MiloClientConnection}, so that
+ * its encoding contexts can be used to encode/decode custom OPC UA data types
(returned as {@code ExtensionObject}).
+ */
+public class MiloClientOpcUaClientAccessTest extends AbstractMiloServerTest {
+
+ private static final String MILO_SERVER_ITEM = "milo-server:myitem1";
+
+ private static final String MILO_CLIENT
+ =
"milo-client:opc.tcp://foo:bar@localhost:@@port@@?allowedSecurityPolicies=None&overrideHost=true";
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ // ensures the embedded OPC UA server is up with a registered
item
+ from("direct:start").to(MILO_SERVER_ITEM);
+ }
+ };
+ }
+
+ @Test
+ void shouldExposeOpcUaClientAndItsEncodingContext() throws Exception {
+ final MiloClientEndpoint endpoint =
context.getEndpoint(resolve(MILO_CLIENT), MiloClientEndpoint.class);
+ final MiloClientConnection connection = endpoint.createConnection();
+ try {
+ // the connection is established lazily and asynchronously
+ await().atMost(30, TimeUnit.SECONDS)
+ .untilAsserted(() ->
assertNotNull(connection.getOpcUaClient(), "OpcUaClient should be exposed"));
+
+ final OpcUaClient client = connection.getOpcUaClient();
+ assertNotNull(client);
+
+ // the whole point of the accessor: reaching the encoding context
to (de)serialize ExtensionObject values,
+ // here exercised with a built-in structured type to avoid
depending on a server-defined custom type
+ final EncodingContext encodingContext =
client.getStaticEncodingContext();
+ assertNotNull(encodingContext);
+
+ final ReadValueId original
+ = new ReadValueId(NodeId.NULL_VALUE,
AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE);
+ final ExtensionObject encoded =
ExtensionObject.encode(encodingContext, original);
+ final UaStructuredType decoded = encoded.decode(encodingContext);
+
+ assertInstanceOf(ReadValueId.class, decoded);
+ } finally {
+ endpoint.releaseConnection(connection);
+ }
+ }
+}