提交 942b1359 authored 作者: 宋勇's avatar 宋勇

添加mqtt,opcua

上级 3474b595
......@@ -960,6 +960,27 @@
<pluginManagement>
<plugins>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.5.3.0</version>
<dependencies>
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
<version>4.6.0</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
<!-- java compiler (Start) -->
<!-- <plugin>-->
......
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.seatunnel</groupId>
<artifactId>seatunnel-datasource-plugins</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>datasource-mqtt</artifactId>
<properties>
<kafka.client.version>3.2.0</kafka.client.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.seatunnel</groupId>
<artifactId>datasource-plugins-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>common-lang3</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.auto.service/auto-service -->
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
</dependency>
<dependency>
<groupId>org.apache.seatunnel</groupId>
<artifactId>seatunnel-api</artifactId>
<scope>provided</scope>
</dependency>
<!--MQTT-->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
<version>6.1.3</version>
</dependency>
<!--MQTT-->
</dependencies>
</project>
package org.apache.seatunnel.datasource.plugin.mqtt;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* MQTT回调函数
*
* @author Mr.Qu
* @since 2020/11/18
*/
@Slf4j
public class InitCallback implements MqttCallback {
private List<MqttMessage> mqttMessageList;
public List<MqttMessage> getMqttMessageList() {
return mqttMessageList;
}
public void setMqttMessageList(List<MqttMessage> mqttMessageList) {
this.mqttMessageList = mqttMessageList;
}
/** MQTT 断开连接会执行此方法 */
@Override
public void connectionLost(Throwable cause) {
log.error(cause.getMessage(), cause);
}
/** publish发布成功后会执行到这里 */
@Override
public void deliveryComplete(IMqttDeliveryToken token) {}
/** subscribe订阅后得到的消息会执行到这里 回调 */
@Override
public void messageArrived(String topic, MqttMessage message) {
// 回调
log.info("[{}] : {}", topic, new String(message.getPayload()));
mqttMessageList.add(message);
// deviceService
// .updateDeviceStatus(new
// Device().setUsername("qbb").setTs(System.currentTimeMillis()));
/*try {
JSONObject jsonObject = JSON.parseObject(msg);
String clientId = String.valueOf(jsonObject.get("clientid"));
if (topic.endsWith("/disconnected")) {
log.info("客户端已掉线:{}", clientId);
} else {
log.info("客户端已上线:{}", clientId);
}
} catch (JSONException e) {
log.error("JSON Format Parsing Exception : {}", msg);
}*/
}
}
package org.apache.seatunnel.datasource.plugin.mqtt;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import lombok.Data;
/** @Author Heartsuit @Date 2022-12-11 */
@Data
public class MqttClientService {
private String HOST;
private Integer PORT;
private final String clientId = "Client" + (int) (Math.random() * 100000000);
private MqttClient mqttClient;
private String TOPIC;
public MqttClientService(
String host,
Integer port,
String username,
String password,
String topic,
MqttCallback mqttCallback)
throws MqttException {
this.HOST = host;
this.PORT = port;
setMqttClient(username, password, topic, mqttCallback);
}
public MqttClient getMqttClient(
String host,
Integer port,
String username,
String password,
String topic,
MqttCallback mqttCallback)
throws MqttException {
this.HOST = host;
this.PORT = port;
setMqttClient(username, password, topic, mqttCallback);
return mqttClient;
}
/**
* 客户端connect连接mqtt服务器
*
* @param username 用户名
* @param password 密码
* @param mqttCallback 回调函数
*/
public void setMqttClient(
String username, String password, String topic, MqttCallback mqttCallback)
throws MqttException {
MqttConnectOptions options = mqttConnectOptions(username, password);
this.TOPIC = topic;
/*if (mqttCallback == null) {
mqttClient.setCallback(new Callback());
} else {
}*/
mqttClient.setCallback(mqttCallback);
mqttClient.connect(options);
}
public void setHOST(String host) {
this.HOST = host;
}
public String getTopic() {
MqttTopic topic = mqttClient.getTopic(TOPIC);
return topic.getName();
}
/** MQTT连接参数设置 */
private MqttConnectOptions mqttConnectOptions(String userName, String passWord)
throws MqttException {
String url = "tcp://" + HOST + ":" + PORT;
mqttClient = new MqttClient(url, clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(userName);
options.setPassword(passWord.toCharArray());
options.setConnectionTimeout(10); // /默认:30
options.setAutomaticReconnect(true); // 默认:false
options.setCleanSession(false); // 默认:true
// options.setKeepAliveInterval(20);//默认:60
return options;
}
/** 关闭MQTT连接 */
public void close() throws MqttException {
mqttClient.close();
mqttClient.disconnect();
}
/** 向某个主题发布消息 默认qos:1 */
public void pub(String topic, String msg) throws MqttException {
MqttMessage mqttMessage = new MqttMessage();
// mqttMessage.setQos(2);
mqttMessage.setPayload(msg.getBytes());
MqttTopic mqttTopic = mqttClient.getTopic(topic);
MqttDeliveryToken token = mqttTopic.publish(mqttMessage);
token.waitForCompletion();
}
public void pub(String msg) throws MqttException {
MqttMessage mqttMessage = new MqttMessage();
// mqttMessage.setQos(2);
mqttMessage.setPayload(msg.getBytes());
MqttTopic mqttTopic = mqttClient.getTopic(TOPIC);
MqttDeliveryToken token = mqttTopic.publish(mqttMessage);
token.waitForCompletion();
}
/**
* 向某个主题发布消息
*
* @param topic: 发布的主题
* @param msg: 发布的消息
* @param qos: 消息质量 Qos:0、1、2
*/
public void pub(String topic, String msg, int qos) throws MqttException {
MqttMessage mqttMessage = new MqttMessage();
mqttMessage.setQos(qos);
mqttMessage.setPayload(msg.getBytes());
MqttTopic mqttTopic = mqttClient.getTopic(topic);
MqttDeliveryToken token = mqttTopic.publish(mqttMessage);
token.waitForCompletion();
}
public void pub(String msg, int qos) throws MqttException {
MqttMessage mqttMessage = new MqttMessage();
mqttMessage.setQos(qos);
mqttMessage.setPayload(msg.getBytes());
MqttTopic mqttTopic = mqttClient.getTopic(TOPIC);
MqttDeliveryToken token = mqttTopic.publish(mqttMessage);
token.waitForCompletion();
}
/**
* 订阅某一个主题 ,此方法默认的的Qos等级为:1
*
* @param topic 主题
*/
public void sub(String topic) throws MqttException {
mqttClient.subscribe(topic);
}
public void sub() throws MqttException {
mqttClient.subscribe(TOPIC);
}
/**
* 订阅某一个主题,可携带Qos
*
* @param topic 所要订阅的主题
* @param qos 消息质量:0、1、2
*/
public void sub(String topic, int qos) throws MqttException {
mqttClient.subscribe(topic, qos);
}
public void sub(int qos) throws MqttException {
mqttClient.subscribe(TOPIC, qos);
}
}
/*
* 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.seatunnel.datasource.plugin.mqtt;
import org.apache.seatunnel.api.configuration.util.OptionRule;
import org.apache.seatunnel.common.utils.SeaTunnelException;
import org.apache.seatunnel.datasource.plugin.api.DataSourceChannel;
import org.apache.seatunnel.datasource.plugin.api.DataSourcePluginException;
import org.apache.seatunnel.datasource.plugin.api.model.TableField;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import com.alibaba.fastjson2.JSONObject;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
@Slf4j
public class MqttDataSourceChannel implements DataSourceChannel {
private static final String DATABASE = "default";
private InitCallback initCallback;
@Override
public OptionRule getDataSourceOptions(@NonNull String pluginName) {
return MqttOptionRule.optionRule();
}
@Override
public OptionRule getDatasourceMetadataFieldsByDataSourceName(@NonNull String pluginName) {
return MqttOptionRule.metadataRule();
}
@Override
public List<String> getTables(
@NonNull String pluginName,
Map<String, String> requestParams,
String database,
Map<String, String> option) {
// checkArgument(StringUtils.equalsIgnoreCase(database, DATABASE), "database must be
// default");
String topic = requestParams.get("topic");
if (StringUtils.isNotBlank(topic)) {
return Arrays.asList(topic);
} else if (StringUtils.isNotBlank(database)) {
return Arrays.asList(database);
} else {
return new ArrayList<>();
}
}
@Override
public List<String> getDatabases(
@NonNull String pluginName, @NonNull Map<String, String> requestParams) {
String suffix = requestParams.get("topic");
if (StringUtils.isNotEmpty(suffix)) {
return Arrays.asList(suffix);
}
return DEFAULT_DATABASES;
}
@Override
public boolean checkDataSourceConnectivity(
@NonNull String pluginName, @NonNull Map<String, String> requestParams) {
if (requestParams.isEmpty()) {
throw new SeaTunnelException("requestParmas 为空!");
}
try {
MqttClientService mqttClient = createMqttClient(requestParams);
// just test the connection
mqttClient.getMqttClient().connect();
return StringUtils.isNotEmpty(mqttClient.getTopic());
} catch (Exception ex) {
throw new DataSourcePluginException(
"check mqtt connectivity failed, " + ex.getMessage(), ex);
}
}
@Override
public List<TableField> getTableFields(
@NonNull String pluginName,
@NonNull Map<String, String> requestParams,
@NonNull String database,
@NonNull String table) {
// checkArgument(StringUtils.equalsIgnoreCase(database, DATABASE), "database must be
// default");
try {
List<TableField> tableFields = new ArrayList<>();
MqttClientService mqttClient = createMqttClient(requestParams);
// just test the connection
mqttClient.getMqttClient().connect();
List<MqttMessage> mqttMessageList = initCallback.getMqttMessageList();
if (CollectionUtils.isNotEmpty(mqttMessageList)) {
MqttMessage mqttMessage = mqttMessageList.get(0);
String s = new String(mqttMessage.getPayload());
JSONObject jsonObject = JSONObject.parseObject(s);
// 获取所有key的迭代器
Iterator<String> keys = jsonObject.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
Object value = jsonObject.get(key);
TableField tableField = new TableField();
tableField.setName(key);
tableField.setType(value.getClass().getTypeName());
tableFields.add(tableField);
}
return tableFields;
}
return Collections.emptyList();
} catch (Exception ex) {
throw new DataSourcePluginException(
"check mqtt getTableFields failed, " + ex.getMessage(), ex);
}
}
@Override
public Map<String, List<TableField>> getTableFields(
@NonNull String pluginName,
@NonNull Map<String, String> requestParams,
@NonNull String database,
@NonNull List<String> tables) {
// checkArgument(StringUtils.equalsIgnoreCase(database, DATABASE), "database must be
// default");
if (CollectionUtils.isEmpty(tables)) {
return Collections.emptyMap();
}
Map<String, List<TableField>> map = new HashMap<>();
for (String table : tables) {
map.put(table, getTableFields(pluginName, requestParams, database, table));
}
return map;
}
private MqttClientService createMqttClient(Map<String, String> requestParams) {
String host = requestParams.get("host") + "";
String port = requestParams.get("port") + "";
String username = requestParams.get("username") + "";
String password = requestParams.get("password") + "";
String topic = requestParams.get("topic") + "";
initCallback = new InitCallback();
try {
MqttClientService mqttClientService =
new MqttClientService(
host, Integer.parseInt(port), username, password, topic, initCallback);
return mqttClientService;
} catch (Exception e) {
throw new SeaTunnelException("创建Mqtt客户端错误!");
}
}
}
/*
* 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.seatunnel.datasource.plugin.mqtt;
import org.apache.seatunnel.datasource.plugin.api.DataSourceChannel;
import org.apache.seatunnel.datasource.plugin.api.DataSourceFactory;
import org.apache.seatunnel.datasource.plugin.api.DataSourcePluginInfo;
import org.apache.seatunnel.datasource.plugin.api.DatasourcePluginTypeEnum;
import com.google.auto.service.AutoService;
import com.google.common.collect.Sets;
import java.util.Set;
@AutoService(DataSourceFactory.class)
public class MqttDataSourceFactory implements DataSourceFactory {
public static final String MQTT_PLUGIN_NAME = "Mqtt";
public static final String MQTT_PLUGIN_ICON = "mqtt";
public static final String MQTT_PLUGIN_VERSION = "1.0.0";
@Override
public String factoryIdentifier() {
return MQTT_PLUGIN_NAME;
}
@Override
public Set<DataSourcePluginInfo> supportedDataSources() {
return Sets.newHashSet(
DataSourcePluginInfo.builder()
.name(MQTT_PLUGIN_NAME)
.icon(MQTT_PLUGIN_ICON)
.version(MQTT_PLUGIN_VERSION)
.supportVirtualTables(false)
.type(DatasourcePluginTypeEnum.NO_STRUCTURED.getCode())
.build());
}
@Override
public DataSourceChannel createChannel() {
return new MqttDataSourceChannel();
}
}
/*
* 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.seatunnel.datasource.plugin.mqtt;
import org.apache.seatunnel.api.configuration.Option;
import org.apache.seatunnel.api.configuration.Options;
import org.apache.seatunnel.api.configuration.util.OptionRule;
public class MqttOptionRule {
public static final Option<String> HOST =
Options.key("host")
.stringType()
.noDefaultValue()
.withDescription("mqtt server address, separated by \",\".");
public static final Option<Integer> PORT =
Options.key("port")
.intType()
.noDefaultValue()
.withDescription("mqtt server port, separated by \",\".");
public static final Option<String> TOPIC =
Options.key("topic")
.stringType()
.noDefaultValue()
.withDescription(
"Kafka topic name. If there are multiple topics, use , to split, for example: \"tpc1,tpc2\".");
public static final Option<String> USERNAME =
Options.key("username")
.stringType()
.noDefaultValue()
.withDescription("mqtt server username");
public static final Option<String> PASSWORD =
Options.key("password")
.stringType()
.noDefaultValue()
.withDescription("mqtt server password");
public static OptionRule optionRule() {
return OptionRule.builder().required(HOST, PORT).optional(USERNAME, PASSWORD).build();
}
public static OptionRule metadataRule() {
return OptionRule.builder().required(TOPIC).build();
}
}
/*
* 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.seatunnel.datasource.plugin.mqtt;
import org.apache.seatunnel.shade.com.typesafe.config.Config;
import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory;
import java.util.Map;
import java.util.Properties;
import static com.google.common.base.Preconditions.checkArgument;
public class MqttRequestParamsUtils {
public static Properties parsePropertiesFromRequestParams(Map<String, String> requestParams) {
checkArgument(
requestParams.containsKey(MqttOptionRule.HOST.key()),
String.format("Missing %s in requestParams", MqttOptionRule.HOST.key()));
final Properties properties = new Properties();
properties.put(MqttOptionRule.HOST.key(), requestParams.get(MqttOptionRule.HOST.key()));
if (requestParams.containsKey(MqttOptionRule.PORT.key())) {
Config configObject =
ConfigFactory.parseString(requestParams.get(MqttOptionRule.PORT.key()));
configObject
.entrySet()
.forEach(
entry -> {
properties.put(
entry.getKey(), entry.getValue().unwrapped().toString());
});
}
return properties;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.seatunnel</groupId>
<artifactId>seatunnel-datasource-plugins</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>datasource-opcua</artifactId>
<properties>
<kafka.client.version>3.2.0</kafka.client.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.seatunnel</groupId>
<artifactId>datasource-plugins-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>common-lang3</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.auto.service/auto-service -->
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
</dependency>
<dependency>
<groupId>org.apache.seatunnel</groupId>
<artifactId>seatunnel-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-client</artifactId>
<version>0.6.8</version>
</dependency>
</dependencies>
</project>
package org.apache.seatunnel.datasource.plugin.opcua;
import org.apache.commons.collections4.CollectionUtils;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;
import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription;
import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscriptionManager;
import org.eclipse.milo.opcua.sdk.client.nodes.UaNode;
import org.eclipse.milo.opcua.sdk.client.subscriptions.ManagedDataItem;
import org.eclipse.milo.opcua.sdk.client.subscriptions.ManagedSubscription;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import lombok.Data;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/** @Author Heartsuit @Date 2022-12-11 */
@Data
public class OpcUaClientService {
// 批量订阅namespaceIndex默认为2
private int batchNamespaceIndex = 2;
// 批量订阅时的identifiers
private List<String> batchIdentifiers;
private OpcUaClient client;
private String host;
private Integer port;
private String suffix;
public OpcUaClientService(String host, Integer port, String suffix) throws Exception {
this.host = host;
this.port = port;
this.suffix = suffix;
connectOpcUaServer(host, port, suffix);
}
/**
* 创建OPC UA客户端
*
* @param host
* @param port
* @param suffix
* @return
* @throws Exception
*/
public OpcUaClient connectOpcUaServer(String host, Integer port, String suffix)
throws Exception {
String endPointUrl = "opc.tcp://" + host + ":" + port + suffix;
Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
Files.createDirectories(securityTempDir);
if (!Files.exists(securityTempDir)) {
throw new Exception("unable to create security dir: " + securityTempDir);
}
client =
OpcUaClient.create(
endPointUrl,
endpoints ->
endpoints.stream()
.filter(
e ->
e.getSecurityPolicyUri()
.equals(
SecurityPolicy.None
.getUri()))
.findFirst(),
configBuilder ->
configBuilder
.setApplicationName(
LocalizedText.english("eclipse milo opc-ua client"))
.setApplicationUri("urn:eclipse:milo:examples:client")
// 访问方式
.setIdentityProvider(new AnonymousProvider())
.setRequestTimeout(UInteger.valueOf(5000))
.build());
client.connect().get();
Thread.sleep(2000); // 线程休眠一下再返回对象,给创建过程一个时间。
return client;
}
/**
* 遍历树形节点
*
* @param uaNode 节点
* @throws Exception
*/
public List listNode(UaNode uaNode, List<UaNode> nodes) throws Exception {
List<? extends UaNode> nodesSub;
if (uaNode == null) {
nodesSub = client.getAddressSpace().browseNodes(Identifiers.ObjectsFolder);
} else {
nodesSub = client.getAddressSpace().browseNodes(uaNode);
}
if (CollectionUtils.isNotEmpty(nodesSub)) {
for (UaNode nd : nodesSub) {
nodes.add(nd);
// 排除系统行性节点,这些系统性节点名称一般都是以"_"开头
if (Objects.requireNonNull(nd.getBrowseName().getName()).contains("_")) {
continue;
}
System.out.println(
"Node= " + nd.getBrowseName().getName() + ",id=" + nd.getNodeId());
listNode(nd, nodes);
}
}
return nodes;
}
/**
* 读取节点数据
*
* <p>namespaceIndex可以通过UaExpert客户端去查询,一般来说这个值是2。
* identifier也可以通过UaExpert客户端去查询,这个值=通道名称.设备名称.标记名称
*
* @param namespaceIndex
* @param identifier
* @throws Exception
*/
public Object[] readNodeValue(int namespaceIndex, String identifier) throws Exception {
// 节点
NodeId nodeId = new NodeId(namespaceIndex, identifier);
// 读取节点数据
DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
// 状态
System.out.println("Status: " + value.getStatusCode());
// 标识符
String id = String.valueOf(nodeId.getIdentifier());
System.out.println(id + ": " + value.getValue().getValue());
return (Object[]) value.getValue().getValue();
}
public Object[] readNodeValue(int namespaceIndex, Integer identifier) throws Exception {
// 节点
NodeId nodeId = new NodeId(namespaceIndex, identifier);
// 读取节点数据
DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
// 状态
System.out.println("Status: " + value.getStatusCode());
// 标识符
String id = String.valueOf(nodeId.getIdentifier());
System.out.println(id + ": " + value.getValue().getValue());
Object[] value1 = (Object[]) value.getValue().getValue();
return value1;
}
/**
* 读取指定节点的值的重载方法
*
* @param nodeId
* @throws Exception
*/
public Object[] readNodeValue(NodeId nodeId) throws Exception {
// 读取节点数据
DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
// 状态
System.out.println("Status: " + value.getStatusCode());
// 标识符
String id = String.valueOf(nodeId.getIdentifier());
System.out.println(nodeId);
System.out.println(id + ": " + value.getValue().getValue());
return (Object[]) value.getValue().getValue();
}
/**
* 读取节点数据
*
* <p>namespaceIndex可以通过UaExpert客户端去查询,一般来说这个值是2。
* identifier也可以通过UaExpert客户端去查询,这个值=通道名称.设备名称.标记名称
*
* @param namespaceIndex
* @param identifier
* @throws Exception
*/
public Object readNodeObject(int namespaceIndex, String identifier) throws Exception {
// 节点
NodeId nodeId = new NodeId(namespaceIndex, identifier);
// 读取节点数据
DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
// 状态
System.out.println("Status: " + value.getStatusCode());
// 标识符
String id = String.valueOf(nodeId.getIdentifier());
System.out.println(id + ": " + value.getValue().getValue());
return value.getValue().getValue();
}
public Object readNodeObject(int namespaceIndex, Integer identifier) throws Exception {
// 节点
NodeId nodeId = new NodeId(namespaceIndex, identifier);
// 读取节点数据
DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
// 状态
System.out.println("Status: " + value.getStatusCode());
// 标识符
String id = String.valueOf(nodeId.getIdentifier());
System.out.println(id + ": " + value.getValue().getValue());
Object value1 = value.getValue().getValue();
return value1;
}
/**
* 读取指定节点的值的重载方法
*
* @param nodeId
* @throws Exception
*/
public Object readNodeObject(NodeId nodeId) throws Exception {
// 读取节点数据
DataValue value = client.readValue(0.0, TimestampsToReturn.Neither, nodeId).get();
// 状态
System.out.println("Status: " + value.getStatusCode());
// 标识符
String id = String.valueOf(nodeId.getIdentifier());
System.out.println(nodeId);
System.out.println(id + ": " + value.getValue().getValue());
return value.getValue().getValue();
}
/**
* 写入节点数据
*
* @param namespaceIndex
* @param identifier
* @param value
* @throws Exception
*/
public boolean writeNodeValue(int namespaceIndex, String identifier, Float value)
throws Exception {
// 节点
NodeId nodeId = new NodeId(namespaceIndex, identifier);
// 创建数据对象,此处的数据对象一定要定义类型,不然会出现类型错误,导致无法写入
DataValue newValue = new DataValue(new Variant(value), null, null);
// 写入节点数据
StatusCode statusCode = client.writeValue(nodeId, newValue).join();
System.out.println("结果:" + statusCode.isGood());
return true;
}
/**
* 订阅(单个)
*
* @param namespaceIndex
* @param identifier
* @throws Exception
*/
private static final AtomicInteger atomic = new AtomicInteger();
public void subscribe(int namespaceIndex, String identifier) throws Exception {
// 创建发布间隔1000ms的订阅对象
client.getSubscriptionManager()
.createSubscription(1000.0)
.thenAccept(
t -> {
// 节点
NodeId nodeId = new NodeId(namespaceIndex, identifier);
ReadValueId readValueId =
new ReadValueId(nodeId, AttributeId.Value.uid(), null, null);
// 创建监控的参数
MonitoringParameters parameters =
new MonitoringParameters(
UInteger.valueOf(atomic.getAndIncrement()),
1000.0,
null,
UInteger.valueOf(10),
true);
// 创建监控项请求
// 该请求最后用于创建订阅。
MonitoredItemCreateRequest request =
new MonitoredItemCreateRequest(
readValueId, MonitoringMode.Reporting, parameters);
List<MonitoredItemCreateRequest> requests = new ArrayList<>();
requests.add(request);
// 创建监控项,并且注册变量值改变时候的回调函数。
t.createMonitoredItems(
TimestampsToReturn.Both,
requests,
(item, id) ->
item.setValueConsumer(
(it, val) -> {
System.out.println(
"nodeid :"
+ it.getReadValueId()
.getNodeId());
System.out.println(
"value :"
+ val.getValue()
.getValue());
}));
})
.get();
// 持续订阅
Thread.sleep(Long.MAX_VALUE);
}
/**
* 订阅单个节点的重载方法
*
* @param nodeId
* @throws Exception
*/
public void subscribe(NodeId nodeId) throws Exception {
// 创建发布间隔1000ms的订阅对象
client.getSubscriptionManager()
.createSubscription(1000.0)
.thenAccept(
t -> {
ReadValueId readValueId =
new ReadValueId(nodeId, AttributeId.Value.uid(), null, null);
// 创建监控的参数
MonitoringParameters parameters =
new MonitoringParameters(
UInteger.valueOf(atomic.getAndIncrement()),
1000.0,
null,
UInteger.valueOf(10),
true);
// 创建监控项请求
// 该请求最后用于创建订阅。
MonitoredItemCreateRequest request =
new MonitoredItemCreateRequest(
readValueId, MonitoringMode.Reporting, parameters);
List<MonitoredItemCreateRequest> requests = new ArrayList<>();
requests.add(request);
// 创建监控项,并且注册变量值改变时候的回调函数。
t.createMonitoredItems(
TimestampsToReturn.Both,
requests,
(item, id) ->
item.setValueConsumer(
(it, val) -> {
System.out.println(
"nodeid :"
+ it.getReadValueId()
.getNodeId());
System.out.println(
"value :"
+ val.getValue()
.getValue());
}));
})
.get();
// 持续订阅
Thread.sleep(Long.MAX_VALUE);
}
/**
* 批量订阅
*
* @throws Exception
*/
public void subscribeBatch() throws Exception {
final CountDownLatch eventLatch = new CountDownLatch(1);
// 处理订阅业务
handlerMultipleNode();
// 持续监听
eventLatch.await();
}
/** 处理订阅业务 */
private void handlerMultipleNode() {
try {
// 创建订阅
ManagedSubscription subscription = ManagedSubscription.create(client);
List<NodeId> nodeIdList = new ArrayList<>();
for (String id : batchIdentifiers) {
nodeIdList.add(new NodeId(batchNamespaceIndex, id));
}
// 监听
List<ManagedDataItem> dataItemList = subscription.createDataItems(nodeIdList);
for (ManagedDataItem managedDataItem : dataItemList) {
managedDataItem.addDataValueListener(
(t) -> {
System.out.println(
managedDataItem.getNodeId().getIdentifier().toString()
+ ":"
+ t.getValue().getValue().toString());
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 批量订阅
*
* @throws Exception
*/
public void subscribeBatchWithReconnect() throws Exception {
final CountDownLatch eventLatch = new CountDownLatch(1);
// 添加订阅监听器,用于处理断线重连后的订阅问题
client.getSubscriptionManager()
.addSubscriptionListener(new CustomSubscriptionListener(client));
// 处理订阅业务
handlerMultipleNode();
// 持续监听
eventLatch.await();
}
/** 自定义订阅监听 */
private class CustomSubscriptionListener implements UaSubscriptionManager.SubscriptionListener {
private final OpcUaClient client;
CustomSubscriptionListener(OpcUaClient client) {
this.client = client;
}
public void onKeepAlive(UaSubscription subscription, DateTime publishTime) {
System.out.println("onKeepAlive");
}
public void onStatusChanged(UaSubscription subscription, StatusCode status) {
System.out.println("onStatusChanged");
}
public void onPublishFailure(UaException exception) {
System.out.println("onPublishFailure");
}
public void onNotificationDataLost(UaSubscription subscription) {
System.out.println("onNotificationDataLost");
}
/**
* 重连时,尝试恢复之前的订阅失败时,会调用此方法
*
* @param uaSubscription 订阅
* @param statusCode 状态
*/
public void onSubscriptionTransferFailed(
UaSubscription uaSubscription, StatusCode statusCode) {
System.out.println("恢复订阅失败 需要重新订阅");
// 在回调方法中重新订阅
handlerMultipleNode();
}
}
public void close() {
if (null != client) {
client.disconnect();
}
}
}
/*
* 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.seatunnel.datasource.plugin.opcua;
import org.apache.seatunnel.api.configuration.util.OptionRule;
import org.apache.seatunnel.datasource.plugin.api.DataSourceChannel;
import org.apache.seatunnel.datasource.plugin.api.DataSourcePluginException;
import org.apache.seatunnel.datasource.plugin.api.model.TableField;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.milo.opcua.stack.core.NamespaceTable;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
import com.alibaba.fastjson2.JSONObject;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
@Slf4j
public class OpcuaDataSourceChannel implements DataSourceChannel {
private static final String DATABASE = "default";
@Override
public OptionRule getDataSourceOptions(@NonNull String pluginName) {
return OpcuaOptionRule.optionRule();
}
@Override
public OptionRule getDatasourceMetadataFieldsByDataSourceName(@NonNull String pluginName) {
return OpcuaOptionRule.metadataRule();
}
@Override
public List<String> getTables(
@NonNull String pluginName,
Map<String, String> requestParams,
String database,
Map<String, String> option) {
// checkArgument(StringUtils.equalsIgnoreCase(database, DATABASE), "database must be
// default");
try {
if (requestParams.isEmpty()) {
return new ArrayList<>();
}
OpcUaClientService opcClient = createOpcClient(requestParams);
if (Objects.nonNull(opcClient)) {
String ns = requestParams.get("ns") + "";
String id = requestParams.get("id") + "";
NodeId nodeId = new NodeId(UShort.valueOf(ns), id);
return Arrays.asList(nodeId.toString());
}
return new ArrayList<>();
} catch (Exception ex) {
throw new DataSourcePluginException(
"check kafka connectivity failed, " + ex.getMessage(), ex);
}
}
@Override
public List<String> getDatabases(
@NonNull String pluginName, @NonNull Map<String, String> requestParams) {
String suffix = requestParams.get("suffix");
if (StringUtils.isNotEmpty(suffix)) {
return Arrays.asList(suffix);
}
return DEFAULT_DATABASES;
}
@Override
public boolean checkDataSourceConnectivity(
@NonNull String pluginName, @NonNull Map<String, String> requestParams) {
try {
OpcUaClientService opcClient = createOpcClient(requestParams);
// just test the connection
NamespaceTable namespaceTable = opcClient.getClient().getNamespaceTable();
return Objects.nonNull(namespaceTable);
} catch (Exception ex) {
throw new DataSourcePluginException(
"check kafka connectivity failed, " + ex.getMessage(), ex);
}
}
@Override
public List<TableField> getTableFields(
@NonNull String pluginName,
@NonNull Map<String, String> requestParams,
@NonNull String database,
@NonNull String table) {
// checkArgument(StringUtils.equalsIgnoreCase(database, DATABASE), "database must be
// default");
// checkArgument(StringUtils.equalsIgnoreCase(database, DATABASE), "database must be
// default");
try {
List<TableField> tableFields = new ArrayList<>();
OpcUaClientService opcClient = createOpcClient(requestParams);
NodeId nodeId = null;
// just test the connection
if (StringUtils.isNotEmpty(table)) {
try {
JSONObject jsonObject = JSONObject.parseObject(table);
UShort uShort = UShort.valueOf(jsonObject.getString("ns"));
Object id = jsonObject.get("id");
if (id instanceof Integer) {
nodeId = new NodeId(uShort, (Integer) id);
} else {
nodeId = new NodeId(uShort, id + "");
}
} catch (Exception e) {
String[] split =
table.replace("NodeId{", "")
.replace("{", "")
.replace("}", "")
.split(",");
UShort uShort = null;
String id = "";
for (String s : split) {
String[] split1 = s.split(":|=");
if (split1.length == 2) {
if (split1[0].toLowerCase().indexOf("ns") > -1) {
uShort = UShort.valueOf(split1[1]);
}
if (split1[0].toLowerCase().indexOf("id") > -1) {
id = split1[1];
}
}
}
if (uShort != null && StringUtils.isNotEmpty(id)) {
try {
// 如果是int
Integer idn = Integer.parseInt(id);
nodeId = new NodeId(uShort, idn);
} catch (Exception ex) {
nodeId = new NodeId(uShort, id);
}
}
}
}
if (Objects.isNull(nodeId)) {
return new ArrayList<>();
}
Object objects = opcClient.readNodeObject(nodeId);
try {
JSONObject jsonObject = JSONObject.parseObject(objects.toString());
// 获取所有key的迭代器
Iterator<String> keys = jsonObject.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
Object value = jsonObject.get(key);
TableField tableField = new TableField();
tableField.setName(key);
tableField.setType(value.getClass().getTypeName());
tableFields.add(tableField);
}
return tableFields;
} catch (Exception e) {
//不是json
TableField tableField = new TableField();
tableField.setName(nodeId.getIdentifier() + "");
tableField.setType("String");
tableFields.add(tableField);
return tableFields;
}
} catch (Exception ex) {
throw new DataSourcePluginException(
"check mqtt getTableFields failed, " + ex.getMessage(), ex);
}
}
@Override
public Map<String, List<TableField>> getTableFields(
@NonNull String pluginName,
@NonNull Map<String, String> requestParams,
@NonNull String database,
@NonNull List<String> tables) {
// checkArgument(StringUtils.equalsIgnoreCase(database, DATABASE), "database must be
// default");
Map<String, List<TableField>> map = new HashMap<>();
if (CollectionUtils.isNotEmpty(tables)) {
for (String table : tables) {
map.put(table, getTableFields(pluginName, requestParams, database, table));
}
}
return map;
}
private OpcUaClientService createOpcClient(Map<String, String> requestParams) {
String host = requestParams.get("host") + "";
String port = requestParams.get("port") + "";
String suffix = requestParams.get("suffix") + "";
try {
OpcUaClientService opcUaClientService =
new OpcUaClientService(host, Integer.parseInt(port), suffix);
return opcUaClientService;
} catch (Exception e) {
throw new DataSourcePluginException("OpcUa插件 创建链接错误!", e);
}
}
}
/*
* 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.seatunnel.datasource.plugin.opcua;
import org.apache.seatunnel.datasource.plugin.api.DataSourceChannel;
import org.apache.seatunnel.datasource.plugin.api.DataSourceFactory;
import org.apache.seatunnel.datasource.plugin.api.DataSourcePluginInfo;
import org.apache.seatunnel.datasource.plugin.api.DatasourcePluginTypeEnum;
import com.google.auto.service.AutoService;
import com.google.common.collect.Sets;
import java.util.Set;
@AutoService(DataSourceFactory.class)
public class OpcuaDataSourceFactory implements DataSourceFactory {
public static final String OPCUA_PLUGIN_NAME = "Opcua";
public static final String OPCUA_PLUGIN_ICON = "opcua";
public static final String OPCUA_PLUGIN_VERSION = "1.0.0";
@Override
public String factoryIdentifier() {
return OPCUA_PLUGIN_NAME;
}
@Override
public Set<DataSourcePluginInfo> supportedDataSources() {
return Sets.newHashSet(
DataSourcePluginInfo.builder()
.name(OPCUA_PLUGIN_NAME)
.icon(OPCUA_PLUGIN_ICON)
.version(OPCUA_PLUGIN_VERSION)
.supportVirtualTables(false)
.type(DatasourcePluginTypeEnum.NO_STRUCTURED.getCode())
.build());
}
@Override
public DataSourceChannel createChannel() {
return new OpcuaDataSourceChannel();
}
}
/*
* 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.seatunnel.datasource.plugin.opcua;
import org.apache.seatunnel.api.configuration.Option;
import org.apache.seatunnel.api.configuration.Options;
import org.apache.seatunnel.api.configuration.util.OptionRule;
public class OpcuaOptionRule {
public static final Option<String> HOST =
Options.key("host").stringType().noDefaultValue().withDescription("socket host");
public static final Option<Integer> PORT =
Options.key("port").intType().noDefaultValue().withDescription("socket port");
public static final Option<String> SUFFIX =
Options.key("suffix").stringType().noDefaultValue().withDescription("suffix");
public static final Option<Integer> NS =
Options.key("ns").intType().noDefaultValue().withDescription("ns");
public static final Option<String> ID =
Options.key("id").stringType().noDefaultValue().withDescription("id");
public static final Option<String> TYPE =
Options.key("type").stringType().defaultValue("int").withDescription("type");
public static OptionRule optionRule() {
return OptionRule.builder().required(HOST, PORT, SUFFIX).optional(TYPE).build();
}
public static OptionRule metadataRule() {
return OptionRule.builder().required(NS, ID).optional(TYPE).build();
}
}
/*
* 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.seatunnel.datasource.plugin.opcua;
import org.apache.seatunnel.shade.com.typesafe.config.Config;
import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory;
import java.util.Map;
import java.util.Properties;
import static com.google.common.base.Preconditions.checkArgument;
public class OpcuaRequestParamsUtils {
public static Properties parsePropertiesFromRequestParams(Map<String, String> requestParams) {
checkArgument(
requestParams.containsKey(OpcuaOptionRule.HOST.key()),
String.format("Missing %s in requestParams", OpcuaOptionRule.HOST.key()));
final Properties properties = new Properties();
properties.put(OpcuaOptionRule.HOST.key(), requestParams.get(OpcuaOptionRule.HOST.key()));
if (requestParams.containsKey(OpcuaOptionRule.PORT.key())) {
Config configObject =
ConfigFactory.parseString(requestParams.get(OpcuaOptionRule.PORT.key()));
configObject
.entrySet()
.forEach(
entry -> {
properties.put(
entry.getKey(), entry.getValue().unwrapped().toString());
});
}
return properties;
}
}
......@@ -45,6 +45,8 @@
<module>datasource-s3</module>
<module>datasource-sqlserver-cdc</module>
<module>datasource-jdbc-tidb</module>
<module>datasource-mqtt</module>
<module>datasource-opcua</module>
</modules>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论