From fad16b2e471a14939f2f54c4965d53fdc0e239ec Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Mon, 30 May 2022 11:25:23 +0300 Subject: [PATCH 01/65] Connection type 'Hono' introduced Signed-off-by: Andrey Balarev --- .../eclipse/ditto/connectivity/model/ConnectionType.java | 7 ++++++- .../service/messaging/DefaultClientActorPropsFactory.java | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java index 279a45bac0..c338acfc5f 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java @@ -50,7 +50,12 @@ public enum ConnectionType implements CharSequence { /** * Indicates a MQTT 5 connection. */ - MQTT_5("mqtt-5"); + MQTT_5("mqtt-5"), + + /** + * Indicates a connection to Eclipse Hono. + */ + HONO("hono"); private final String name; diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index 5840bb2505..74c150aa8d 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -80,7 +80,7 @@ public Props getActorPropsForType(final Connection connection, final ActorRef pr result = HiveMqtt5ClientActor.props(connection, proxyActor, connectionActor, dittoHeaders, connectivityConfigOverwrites); break; - case KAFKA: + case KAFKA, HONO: result = KafkaClientActor.props(connection, proxyActor, connectionActor, dittoHeaders, connectivityConfigOverwrites); break; From 9af8a4f21a25c072f3beaf9cdd4d1acc3eba2a10 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Mon, 30 May 2022 11:27:35 +0300 Subject: [PATCH 02/65] HonoConfig introduced to provide properties for the new Hono-connection type Signed-off-by: Andrey Balarev --- .../ditto/connectivity/api/HonoConfig.java | 68 +++++++++++++++++++ .../connectivity/api/HonoConfigDefault.java | 29 ++++++++ .../src/main/resources/connectivity.conf | 7 ++ .../service/src/main/resources/gateway.conf | 7 ++ 4 files changed, 111 insertions(+) create mode 100644 connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java create mode 100644 connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfigDefault.java diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java new file mode 100644 index 0000000000..9ca185ad5e --- /dev/null +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.api; +import java.util.List; +import org.eclipse.ditto.internal.utils.akka.AkkaClassLoader; +import org.eclipse.ditto.internal.utils.config.KnownConfigValue; +import akka.actor.AbstractExtensionId; +import akka.actor.ActorSystem; +import akka.actor.ExtendedActorSystem; +import akka.actor.Extension; +public interface HonoConfig extends Extension { + + String PREFIX = "hono-connection"; + + String getBaseUri(); + enum ConfigValues implements KnownConfigValue { + + BASE_URI("base-uri", "my.hono.host:1234"); + private final String path; + private final Object defaultValue; + ConfigValues(final String thePath, final Object theDefaultValue) { + path = thePath; + defaultValue = theDefaultValue; + } + + @Override + public Object getDefaultValue() { + return defaultValue; + } + + @Override + public String getConfigPath() { + return path; + } + + } + + static HonoConfig get(final ActorSystem actorSystem) { + return HonoConfig.ExtensionId.INSTANCE.get(actorSystem); + } + + /** + * ID of the actor system extension to provide a Hono-connections configuration. + */ + final class ExtensionId extends AbstractExtensionId { + + private static final String CONFIG_PATH = PREFIX + ".config-provider"; + private static final ExtensionId INSTANCE = new ExtensionId(); + @Override + public HonoConfig createExtension(final ExtendedActorSystem system) { + + final String implementation = system.settings().config().getString(CONFIG_PATH); + return AkkaClassLoader.instantiate(system, HonoConfig.class, + implementation, + List.of(ActorSystem.class), + List.of(system)); + } + } +} \ No newline at end of file diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfigDefault.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfigDefault.java new file mode 100644 index 0000000000..2cba2f6f19 --- /dev/null +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfigDefault.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.api; +import com.typesafe.config.Config; +import akka.actor.ActorSystem; +public final class HonoConfigDefault implements HonoConfig { + + private final String baseUri; + + public HonoConfigDefault(final ActorSystem actorSystem) { + final Config config = actorSystem.settings().config().getConfig(PREFIX); + this.baseUri = config.getString(ConfigValues.BASE_URI.getConfigPath()); + } + + @Override + public String getBaseUri() { + return baseUri; + } + +} \ No newline at end of file diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index bd6e8c35c0..ad48656a4b 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -1,3 +1,10 @@ +hono-connection { + config-provider = "org.eclipse.ditto.connectivity.api.HonoConfigDefault", + config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} + base-uri = "localhost:9990" + base-uri = ${?HONO_CONNECTION_URI} +} + ditto { service-name = "connectivity" diff --git a/gateway/service/src/main/resources/gateway.conf b/gateway/service/src/main/resources/gateway.conf index 4654da828f..44b5f916d5 100755 --- a/gateway/service/src/main/resources/gateway.conf +++ b/gateway/service/src/main/resources/gateway.conf @@ -1,3 +1,10 @@ +hono-connection { + config-provider = "org.eclipse.ditto.connectivity.api.HonoConfigDefault", + config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} + base-uri = "localhost:9990" + base-uri = ${?HONO_CONNECTION_URI} +} + ditto { service-name = "gateway" mapping-strategy.implementation = "org.eclipse.ditto.gateway.service.util.GatewayMappingStrategies" From 448d5a25df3637fd5ac3f95cbbfd0155f694e0ea Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Tue, 7 Jun 2022 11:12:18 +0300 Subject: [PATCH 03/65] DefaultHonoConfig and conf files update Signed-off-by: Andrey Balarev --- .../connectivity/api/DefaultHonoConfig.java | 66 +++++++++++++++++++ .../src/main/resources/connectivity.conf | 14 +++- .../service/src/main/resources/gateway.conf | 14 +++- 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java new file mode 100644 index 0000000000..e2db67ce5e --- /dev/null +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.api; +import com.typesafe.config.Config; +import akka.actor.ActorSystem; +public final class DefaultHonoConfig implements HonoConfig { + + private final String baseUri; + + private final String telemetryAddress; + private final String eventAddress; + private final String commandAndControlAddress; + private final String commandResponseAddress; + + private final String username; + private final String password; + + public DefaultHonoConfig(final ActorSystem actorSystem) { + final Config config = actorSystem.settings().config().getConfig(PREFIX); + this.baseUri = config.getString(ConfigValues.BASE_URI.getConfigPath()); + this.telemetryAddress = config.getString(ConfigValues.TELEMETRY_ADDRESS.getConfigPath()); + this.eventAddress = config.getString(ConfigValues.EVENT_ADDRESS.getConfigPath()); + this.commandAndControlAddress = config.getString(ConfigValues.COMMAND_AND_CONTROL_ADDRESS.getConfigPath()); + this.commandResponseAddress = config.getString(ConfigValues.COMMAND_RESPONSE_ADDRESS.getConfigPath()); + this.username = config.getString(ConfigValues.USERNAME.getConfigPath()); + this.password = config.getString(ConfigValues.PASSWORD.getConfigPath()); + } + + @Override + public String getBaseUri() { + return baseUri; + } + + public String getTelemetryAddress() { + return telemetryAddress; + } + + public String getEventAddress() { + return eventAddress; + } + + public String getCommandAndControlAddress() { + return commandAndControlAddress; + } + + public String getCommandResponseAddress() { + return commandResponseAddress; + } + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} \ No newline at end of file diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index ad48656a4b..56298ab8f8 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -1,8 +1,18 @@ hono-connection { - config-provider = "org.eclipse.ditto.connectivity.api.HonoConfigDefault", + config-provider = "org.eclipse.ditto.connectivity.api.DefaultHonoConfig", config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} - base-uri = "localhost:9990" + + base-uri = "tcp://localhost:9990" base-uri = ${?HONO_CONNECTION_URI} + telemetry = "telemetry/..." + telemetry = ${?HONO_CONNECTION_TELEMETRY_ADDRESS} + event = "event/..." + event = ${?HONO_CONNECTION_EVENT_ADDRESS} + commandAndControl = "command/..." + commandAndControl = ${?HONO_CONNECTION_COMMAND_ADDRESS} + commandResponse = "response/..." + commandResponse = ${?HONO_CONNECTION_RESPONSE_ADDRESS} + } ditto { diff --git a/gateway/service/src/main/resources/gateway.conf b/gateway/service/src/main/resources/gateway.conf index 44b5f916d5..40e72c6015 100755 --- a/gateway/service/src/main/resources/gateway.conf +++ b/gateway/service/src/main/resources/gateway.conf @@ -1,8 +1,18 @@ hono-connection { - config-provider = "org.eclipse.ditto.connectivity.api.HonoConfigDefault", + config-provider = "org.eclipse.ditto.connectivity.api.DefaultHonoConfig", config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} - base-uri = "localhost:9990" + + base-uri = "tcp://localhost:9990" base-uri = ${?HONO_CONNECTION_URI} + telemetry = "telemetry/..." + telemetry = ${?HONO_CONNECTION_TELEMETRY_ADDRESS} + event = "event/..." + event = ${?HONO_CONNECTION_EVENT_ADDRESS} + commandAndControl = "command/..." + commandAndControl = ${?HONO_CONNECTION_COMMAND_ADDRESS} + commandResponse = "response/..." + commandResponse = ${?HONO_CONNECTION_RESPONSE_ADDRESS} + } ditto { From 70b47d1162d4c53fe5783f1802c6c437e464eeeb Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Tue, 7 Jun 2022 11:17:07 +0300 Subject: [PATCH 04/65] Config values added Signed-off-by: Andrey Balarev --- .../org/eclipse/ditto/connectivity/api/HonoConfig.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index 9ca185ad5e..a7601ad9f7 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -24,7 +24,14 @@ public interface HonoConfig extends Extension { String getBaseUri(); enum ConfigValues implements KnownConfigValue { - BASE_URI("base-uri", "my.hono.host:1234"); + BASE_URI("base-uri", "my.hono.host:1234"), + TELEMETRY_ADDRESS("telemetry", "telemetry/..."), + EVENT_ADDRESS("event", "event/..."), + COMMAND_AND_CONTROL_ADDRESS("commandAndControl", "command/..."), + COMMAND_RESPONSE_ADDRESS("commandResponse", "response/..."), + USERNAME("username", ""), + PASSWORD("password", ""); + private final String path; private final Object defaultValue; ConfigValues(final String thePath, final Object theDefaultValue) { From 102937783313cc4b7c2d5544f6e6a4f7e06831e5 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 8 Jun 2022 10:47:14 +0300 Subject: [PATCH 05/65] Redundant file removed Signed-off-by: Andrey Balarev --- .../connectivity/api/HonoConfigDefault.java | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfigDefault.java diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfigDefault.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfigDefault.java deleted file mode 100644 index 2cba2f6f19..0000000000 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfigDefault.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2022 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.connectivity.api; -import com.typesafe.config.Config; -import akka.actor.ActorSystem; -public final class HonoConfigDefault implements HonoConfig { - - private final String baseUri; - - public HonoConfigDefault(final ActorSystem actorSystem) { - final Config config = actorSystem.settings().config().getConfig(PREFIX); - this.baseUri = config.getString(ConfigValues.BASE_URI.getConfigPath()); - } - - @Override - public String getBaseUri() { - return baseUri; - } - -} \ No newline at end of file From 16f3da62f2b24c74b8ac6319e11fb76853b9bd00 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Mon, 13 Jun 2022 19:22:05 +0300 Subject: [PATCH 06/65] HonoCredentials taken from static configuration Signed-off-by: Andrey Balarev --- .../connectivity/api/DefaultHonoConfig.java | 31 +++++-- .../ditto/connectivity/api/HonoConfig.java | 90 +++++++++++++++++++ 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java index e2db67ce5e..7395a8f5df 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java @@ -11,7 +11,14 @@ * SPDX-License-Identifier: EPL-2.0 */ package org.eclipse.ditto.connectivity.api; + +import org.eclipse.ditto.base.model.common.ConditionChecker; +import org.eclipse.ditto.connectivity.model.ConnectionId; +import org.eclipse.ditto.connectivity.model.Credentials; +import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; + import com.typesafe.config.Config; + import akka.actor.ActorSystem; public final class DefaultHonoConfig implements HonoConfig { @@ -22,18 +29,21 @@ public final class DefaultHonoConfig implements HonoConfig { private final String commandAndControlAddress; private final String commandResponseAddress; - private final String username; - private final String password; + private final Credentials credentials; public DefaultHonoConfig(final ActorSystem actorSystem) { + ConditionChecker.checkNotNull(actorSystem, "actorSystem"); final Config config = actorSystem.settings().config().getConfig(PREFIX); this.baseUri = config.getString(ConfigValues.BASE_URI.getConfigPath()); + this.telemetryAddress = config.getString(ConfigValues.TELEMETRY_ADDRESS.getConfigPath()); this.eventAddress = config.getString(ConfigValues.EVENT_ADDRESS.getConfigPath()); this.commandAndControlAddress = config.getString(ConfigValues.COMMAND_AND_CONTROL_ADDRESS.getConfigPath()); this.commandResponseAddress = config.getString(ConfigValues.COMMAND_RESPONSE_ADDRESS.getConfigPath()); - this.username = config.getString(ConfigValues.USERNAME.getConfigPath()); - this.password = config.getString(ConfigValues.PASSWORD.getConfigPath()); + + this.credentials = UserPasswordCredentials.newInstance( + config.getString(ConfigValues.USERNAME.getConfigPath()), + config.getString(ConfigValues.PASSWORD.getConfigPath())); } @Override @@ -41,26 +51,29 @@ public String getBaseUri() { return baseUri; } + @Override public String getTelemetryAddress() { return telemetryAddress; } + @Override public String getEventAddress() { return eventAddress; } + @Override public String getCommandAndControlAddress() { return commandAndControlAddress; } + @Override public String getCommandResponseAddress() { return commandResponseAddress; } - public String getUsername() { - return username; - } - public String getPassword() { - return password; + @Override + public Credentials getCredentials(final ConnectionId connectionId) { + return credentials; } + } \ No newline at end of file diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index a7601ad9f7..5ee3d3008c 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -11,29 +11,111 @@ */ package org.eclipse.ditto.connectivity.api; import java.util.List; + +import org.eclipse.ditto.connectivity.model.ConnectionId; +import org.eclipse.ditto.connectivity.model.Credentials; import org.eclipse.ditto.internal.utils.akka.AkkaClassLoader; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; import akka.actor.AbstractExtensionId; import akka.actor.ActorSystem; import akka.actor.ExtendedActorSystem; import akka.actor.Extension; + +/** + * Configuration interface for connection type 'Hono' parameters + * Via actor system extension, it enables different implementations per service type (Ditto/Things) + */ public interface HonoConfig extends Extension { + /** + * Prefix in .conf files + */ String PREFIX = "hono-connection"; + /** + * Gets the Base URI configuration value + * + * @return the connection URI + */ String getBaseUri(); + + /** + * Gets the 'telemetry' alias configuration value + * + * @return The telemetry address + */ + String getTelemetryAddress(); + + /** + * Gets the 'event' alias configuration value + * + * @return The event address + */ + String getEventAddress(); + + /** + * Gets the 'commandAndControl' alias configuration value + * + * @return The command&control address + */ + + String getCommandAndControlAddress(); + + /** + * Gets the 'commandResponse' alias configuration value + * + * @return The commandResponse address + */ + String getCommandResponseAddress(); + + /** + * Gets the credentials for specified Hono-connection + * + * @param connectionId The connection ID of the connection to get credentials + * @return The credentials of the connection + */ + Credentials getCredentials(final ConnectionId connectionId); + enum ConfigValues implements KnownConfigValue { + /** + * Base URI, including port number (without protocol prefix) + */ BASE_URI("base-uri", "my.hono.host:1234"), + + /** + * Telemetry address alias + */ TELEMETRY_ADDRESS("telemetry", "telemetry/..."), + + /** + * Event address alias + */ EVENT_ADDRESS("event", "event/..."), + + /** + * Command and control address alias + */ COMMAND_AND_CONTROL_ADDRESS("commandAndControl", "command/..."), + + /** + * Command response address alias + */ COMMAND_RESPONSE_ADDRESS("commandResponse", "response/..."), + + /** + * Username + */ USERNAME("username", ""), + + /** + * Password + */ PASSWORD("password", ""); private final String path; private final Object defaultValue; + ConfigValues(final String thePath, final Object theDefaultValue) { path = thePath; defaultValue = theDefaultValue; @@ -51,6 +133,12 @@ public String getConfigPath() { } + /** + * Load the {@code HonoConfig} extension. + * + * @param actorSystem The actor system in which to load the configuration. + * @return the {@code HonoConfig}. + */ static HonoConfig get(final ActorSystem actorSystem) { return HonoConfig.ExtensionId.INSTANCE.get(actorSystem); } @@ -61,7 +149,9 @@ static HonoConfig get(final ActorSystem actorSystem) { final class ExtensionId extends AbstractExtensionId { private static final String CONFIG_PATH = PREFIX + ".config-provider"; + private static final ExtensionId INSTANCE = new ExtensionId(); + @Override public HonoConfig createExtension(final ExtendedActorSystem system) { From 000b33326877acb9244b4f4b2252b2fabb8eafca Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Fri, 17 Jun 2022 14:30:24 +0300 Subject: [PATCH 07/65] Config refactored Signed-off-by: Andrey Balarev --- .../connectivity/api/DefaultHonoConfig.java | 41 +++--- .../ditto/connectivity/api/HonoConfig.java | 63 +++++--- .../connectivity/model/HonoAddressAlias.java | 32 ++++ .../model/HonoAddressAliasValues.java | 138 ++++++++++++++++++ .../DefaultClientActorPropsFactory.java | 24 ++- .../src/main/resources/connectivity.conf | 43 +++--- .../service/src/main/resources/gateway.conf | 17 --- 7 files changed, 280 insertions(+), 78 deletions(-) create mode 100644 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java create mode 100644 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java index 7395a8f5df..3ddc1e87c3 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java @@ -13,6 +13,7 @@ package org.eclipse.ditto.connectivity.api; import org.eclipse.ditto.base.model.common.ConditionChecker; +import org.eclipse.ditto.connectivity.model.HonoAddressAliasValues; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.Credentials; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; @@ -20,26 +21,33 @@ import com.typesafe.config.Config; import akka.actor.ActorSystem; + +/** + * Configuration class providing parameters for connection type 'Hono' in Ditto from static configuration + */ public final class DefaultHonoConfig implements HonoConfig { private final String baseUri; + private final SaslMechanism saslMechanism; + private final String bootstrapServers; - private final String telemetryAddress; - private final String eventAddress; - private final String commandAndControlAddress; - private final String commandResponseAddress; + private final HonoAddressAliasValues honoAddressAliasValues; private final Credentials credentials; public DefaultHonoConfig(final ActorSystem actorSystem) { ConditionChecker.checkNotNull(actorSystem, "actorSystem"); final Config config = actorSystem.settings().config().getConfig(PREFIX); + this.baseUri = config.getString(ConfigValues.BASE_URI.getConfigPath()); + this.saslMechanism = config.getEnum(SaslMechanism.class, ConfigValues.SASL_MECHANISM.getConfigPath()); + this.bootstrapServers = config.getString(ConfigValues.BOOTSTRAP_SERVERS.getConfigPath()); - this.telemetryAddress = config.getString(ConfigValues.TELEMETRY_ADDRESS.getConfigPath()); - this.eventAddress = config.getString(ConfigValues.EVENT_ADDRESS.getConfigPath()); - this.commandAndControlAddress = config.getString(ConfigValues.COMMAND_AND_CONTROL_ADDRESS.getConfigPath()); - this.commandResponseAddress = config.getString(ConfigValues.COMMAND_RESPONSE_ADDRESS.getConfigPath()); + honoAddressAliasValues = HonoAddressAliasValues.newInstance( + config.getString(ConfigValues.TELEMETRY_ADDRESS.getConfigPath()), + config.getString(ConfigValues.EVENT_ADDRESS.getConfigPath()), + config.getString(ConfigValues.COMMAND_AND_CONTROL_ADDRESS.getConfigPath()), + config.getString(ConfigValues.COMMAND_RESPONSE_ADDRESS.getConfigPath())); this.credentials = UserPasswordCredentials.newInstance( config.getString(ConfigValues.USERNAME.getConfigPath()), @@ -52,23 +60,18 @@ public String getBaseUri() { } @Override - public String getTelemetryAddress() { - return telemetryAddress; - } - - @Override - public String getEventAddress() { - return eventAddress; + public SaslMechanism getSaslMechanism() { + return saslMechanism; } @Override - public String getCommandAndControlAddress() { - return commandAndControlAddress; + public String getBootstrapServers() { + return bootstrapServers; } @Override - public String getCommandResponseAddress() { - return commandResponseAddress; + public HonoAddressAliasValues getAddressAliases(final ConnectionId connectionId) { + return honoAddressAliasValues; } @Override diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index 5ee3d3008c..70e3b5c537 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -12,6 +12,7 @@ package org.eclipse.ditto.connectivity.api; import java.util.List; +import org.eclipse.ditto.connectivity.model.HonoAddressAliasValues; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.Credentials; import org.eclipse.ditto.internal.utils.akka.AkkaClassLoader; @@ -30,7 +31,7 @@ public interface HonoConfig extends Extension { /** * Prefix in .conf files */ - String PREFIX = "hono-connection"; + String PREFIX = "ditto/connectivity/hono-connection"; /** * Gets the Base URI configuration value @@ -40,33 +41,26 @@ public interface HonoConfig extends Extension { String getBaseUri(); /** - * Gets the 'telemetry' alias configuration value + * Gets the SASL mechanism of Hono-connection (Kafka specific property) * - * @return The telemetry address + * @return {@link SaslMechanism} */ - String getTelemetryAddress(); + SaslMechanism getSaslMechanism(); /** - * Gets the 'event' alias configuration value + * Gets bootstrap servers * - * @return The event address + * @return {@link String} containing comma separated bootstrap server list */ - String getEventAddress(); + String getBootstrapServers(); /** - * Gets the 'commandAndControl' alias configuration value + * Gets the connection address aliases * - * @return The command&control address + * @param connectionId The connection ID of the connection to get aliases + * @return {@link org.eclipse.ditto.connectivity.model.HonoAddressAliasValues} */ - - String getCommandAndControlAddress(); - - /** - * Gets the 'commandResponse' alias configuration value - * - * @return The commandResponse address - */ - String getCommandResponseAddress(); + HonoAddressAliasValues getAddressAliases(ConnectionId connectionId); /** * Gets the credentials for specified Hono-connection @@ -74,34 +68,44 @@ public interface HonoConfig extends Extension { * @param connectionId The connection ID of the connection to get credentials * @return The credentials of the connection */ - Credentials getCredentials(final ConnectionId connectionId); + Credentials getCredentials(ConnectionId connectionId); enum ConfigValues implements KnownConfigValue { /** * Base URI, including port number (without protocol prefix) */ - BASE_URI("base-uri", "my.hono.host:1234"), + BASE_URI("base-uri", ""), + + /** + * SASL mechanism for connections of type Hono + */ + SASL_MECHANISM("sasl-mechanism", "plain"), + + /** + * Bootstrap servers, comma separated + */ + BOOTSTRAP_SERVERS("bootstrap-servers", ""), /** * Telemetry address alias */ - TELEMETRY_ADDRESS("telemetry", "telemetry/..."), + TELEMETRY_ADDRESS("telemetry", ""), /** * Event address alias */ - EVENT_ADDRESS("event", "event/..."), + EVENT_ADDRESS("event", ""), /** * Command and control address alias */ - COMMAND_AND_CONTROL_ADDRESS("commandAndControl", "command/..."), + COMMAND_AND_CONTROL_ADDRESS("commandAndControl", ""), /** * Command response address alias */ - COMMAND_RESPONSE_ADDRESS("commandResponse", "response/..."), + COMMAND_RESPONSE_ADDRESS("commandResponse", ""), /** * Username @@ -162,4 +166,15 @@ public HonoConfig createExtension(final ExtendedActorSystem system) { List.of(system)); } } + + enum SaslMechanism { + + PLAIN("plain"); + + final String value; + + SaslMechanism(String value) { + this.value = value; + } + } } \ No newline at end of file diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java new file mode 100644 index 0000000000..2bd23e85d9 --- /dev/null +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.ditto.connectivity.model; + +public enum HonoAddressAlias { + TELEMETRY("telemetry"), + EVENT("event"), + COMMAND("commandAndControl"), + COMMAND_RESPONSE("commandResponse"); + + private final String name; + + HonoAddressAlias(String name) { + this.name = name; + } + + public String getName() { + return name; + } + +} diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java new file mode 100644 index 0000000000..014fbd02cb --- /dev/null +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2021 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonFieldDefinition; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; + +/** + * Address aliases used by connections of type 'Hono' + * + * @since 2.5.0 + */ +@Immutable +public final class HonoAddressAliasValues { + + private final String telemetry; + private final String event; + private final String commandAndControl; + private final String commandResponse; + + private HonoAddressAliasValues(final String telemetry, final String event, final String commandAndControl, + final String commandResponse) { + this.telemetry = telemetry; + this.event = event; + this.commandAndControl = commandAndControl; + this.commandResponse = commandResponse; + } + + /** + * Gets the 'telemetry' alias configuration value + * + * @return The telemetry address + */ + public String getTelemetryAddress() { + return telemetry; + } + + /** + * Gets the 'event' alias configuration value + * + * @return The event address + */ + public String getEventAddress() { + return event; + } + + /** + * Gets the 'commandAndControl' alias value + * + * @return The command&control address + */ + + public String getCommandAndControlAddress() { + return commandAndControl; + } + + /** + * Gets the 'commandResponse' alias value + * + * @return The commandResponse address + */ + public String getCommandResponseAddress() { + return commandResponse; + } + + public JsonObject toJson() { + final JsonObjectBuilder jsonObjectBuilder = JsonFactory.newObjectBuilder(); + jsonObjectBuilder.set(JsonFields.TELEMETRY, telemetry); + jsonObjectBuilder.set(JsonFields.EVENT, event); + jsonObjectBuilder.set(JsonFields.COMMAND_AND_CONTROL, commandAndControl); + jsonObjectBuilder.set(JsonFields.COMMAND_RESPONSE, commandResponse); + return jsonObjectBuilder.build(); + } + + static HonoAddressAliasValues fromJson(final JsonObject jsonObject) { + final String telemetry = jsonObject.getValueOrThrow(JsonFields.TELEMETRY); + final String event = jsonObject.getValueOrThrow(JsonFields.EVENT); + final String commandAndControl = jsonObject.getValueOrThrow(JsonFields.COMMAND_AND_CONTROL); + final String commandResponse = jsonObject.getValueOrThrow(JsonFields.COMMAND_RESPONSE); + return new HonoAddressAliasValues(telemetry, event, commandAndControl, commandResponse); + } + + /** + * Create credentials with username and password. + * + * @return credentials. + */ + public static HonoAddressAliasValues newInstance(final String telemetry, final String event, + final String commandAndControl, final String commandResponse) { + return new HonoAddressAliasValues(telemetry, event, commandAndControl, commandResponse); + } + + /** + * JSON field definitions. + */ + public static final class JsonFields extends Credentials.JsonFields { + + private static final ConcurrentMap> DESERIALIZER_MAP = + new ConcurrentHashMap<>(); + /** + * JSON field containing the telemetry + */ + public static final JsonFieldDefinition TELEMETRY = JsonFieldDefinition.ofString("telemetry"); + + /** + * JSON field containing the event + */ + public static final JsonFieldDefinition EVENT = JsonFieldDefinition.ofString("event"); + + /** + * JSON field containing the event + */ + public static final JsonFieldDefinition COMMAND_AND_CONTROL = JsonFieldDefinition.ofString("command"); + + /** + * JSON field containing the event + */ + public static final JsonFieldDefinition COMMAND_RESPONSE = JsonFieldDefinition.ofString("command_response"); + } +} diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index 74c150aa8d..4a2fec3370 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -16,8 +16,10 @@ import javax.annotation.concurrent.Immutable; import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.connectivity.api.HonoConfig; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionType; +import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpClientActor; import org.eclipse.ditto.connectivity.service.messaging.httppush.HttpPushClientActor; import org.eclipse.ditto.connectivity.service.messaging.kafka.KafkaClientActor; @@ -80,10 +82,14 @@ public Props getActorPropsForType(final Connection connection, final ActorRef pr result = HiveMqtt5ClientActor.props(connection, proxyActor, connectionActor, dittoHeaders, connectivityConfigOverwrites); break; - case KAFKA, HONO: + case KAFKA: result = KafkaClientActor.props(connection, proxyActor, connectionActor, dittoHeaders, connectivityConfigOverwrites); break; + case HONO: + result = KafkaClientActor.props(getEnrichedConnection(actorSystem, connection), + proxyActor, connectionActor, dittoHeaders, connectivityConfigOverwrites); + break; case HTTP_PUSH: result = HttpPushClientActor.props(connection, proxyActor, connectionActor, dittoHeaders, connectivityConfigOverwrites); @@ -94,4 +100,20 @@ public Props getActorPropsForType(final Connection connection, final ActorRef pr return result; } + private Connection getEnrichedConnection(final ActorSystem actorSystem, final Connection connection) { + var honoConfig = HonoConfig.get(actorSystem); + return ConnectivityModelFactory.newConnectionBuilder( + connection.getId(), + connection.getConnectionType(), + connection.getConnectionStatus(), + honoConfig.getBaseUri()) + .credentials(honoConfig.getCredentials(connection.getId())) +// .sources(connection.getSources() +// .stream() +// .map(source -> new Source( +// source.getAddresses().map(replaceAlias).collect(Collectors.toList()); +// return source + .build(); + } + } diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index 56298ab8f8..006f84cf41 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -1,20 +1,3 @@ -hono-connection { - config-provider = "org.eclipse.ditto.connectivity.api.DefaultHonoConfig", - config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} - - base-uri = "tcp://localhost:9990" - base-uri = ${?HONO_CONNECTION_URI} - telemetry = "telemetry/..." - telemetry = ${?HONO_CONNECTION_TELEMETRY_ADDRESS} - event = "event/..." - event = ${?HONO_CONNECTION_EVENT_ADDRESS} - commandAndControl = "command/..." - commandAndControl = ${?HONO_CONNECTION_COMMAND_ADDRESS} - commandResponse = "response/..." - commandResponse = ${?HONO_CONNECTION_RESPONSE_ADDRESS} - -} - ditto { service-name = "connectivity" @@ -33,6 +16,32 @@ ditto { connectivity { + hono-connection { + config-provider = "org.eclipse.ditto.connectivity.api.DefaultHonoConfig", + config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} + + base-uri = "tcp://localhost:9990" + base-uri = ${?HONO_CONNECTION_URI} + + sasl-mechanism = "PLAIN" + sasl-mechanism = ${?HONO_CONNECTION_SASL_MECHANISM} + + bootstrap-servers = "bootstrap.server:9999" + bootstrap-servers = ${?HONO_CONNECTION_BOOTSTRAP_SERVERS} + + telemetry = "telemetry/..." + telemetry = ${?HONO_CONNECTION_TELEMETRY_ADDRESS} + + event = "event/..." + event = ${?HONO_CONNECTION_EVENT_ADDRESS} + + commandAndControl = "command/..." + commandAndControl = ${?HONO_CONNECTION_COMMAND_ADDRESS} + + commandResponse = "response/..." + commandResponse = ${?HONO_CONNECTION_RESPONSE_ADDRESS} + } + user-indicated-errors-base = [ # Kafka {exceptionName: "org.apache.kafka.common.errors.SaslAuthenticationException", messagePattern: ".*"} diff --git a/gateway/service/src/main/resources/gateway.conf b/gateway/service/src/main/resources/gateway.conf index 40e72c6015..4654da828f 100755 --- a/gateway/service/src/main/resources/gateway.conf +++ b/gateway/service/src/main/resources/gateway.conf @@ -1,20 +1,3 @@ -hono-connection { - config-provider = "org.eclipse.ditto.connectivity.api.DefaultHonoConfig", - config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} - - base-uri = "tcp://localhost:9990" - base-uri = ${?HONO_CONNECTION_URI} - telemetry = "telemetry/..." - telemetry = ${?HONO_CONNECTION_TELEMETRY_ADDRESS} - event = "event/..." - event = ${?HONO_CONNECTION_EVENT_ADDRESS} - commandAndControl = "command/..." - commandAndControl = ${?HONO_CONNECTION_COMMAND_ADDRESS} - commandResponse = "response/..." - commandResponse = ${?HONO_CONNECTION_RESPONSE_ADDRESS} - -} - ditto { service-name = "gateway" mapping-strategy.implementation = "org.eclipse.ditto.gateway.service.util.GatewayMappingStrategies" From 845d7b897b65e9d52ef4dc99c812ebc4bc3b57e1 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 22 Jun 2022 14:51:10 +0300 Subject: [PATCH 08/65] Config refactored Signed-off-by: Andrey Balarev --- .../ditto/connectivity/api/HonoConfig.java | 7 +++--- .../connectivity/model/HonoAddressAlias.java | 23 +++++++++++++++++++ .../model/HonoAddressAliasValues.java | 4 ++-- .../model/UserPasswordCredentials.java | 2 ++ .../RetrieveConnectionStatusResponse.java | 2 ++ 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index 70e3b5c537..bc6d2d3d51 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -6,15 +6,16 @@ * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 * + * http://www.eclipse.org/legal/epl-2.0 + * * SPDX-License-Identifier: EPL-2.0 */ package org.eclipse.ditto.connectivity.api; import java.util.List; -import org.eclipse.ditto.connectivity.model.HonoAddressAliasValues; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.Credentials; +import org.eclipse.ditto.connectivity.model.HonoAddressAliasValues; import org.eclipse.ditto.internal.utils.akka.AkkaClassLoader; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; import akka.actor.AbstractExtensionId; @@ -31,7 +32,7 @@ public interface HonoConfig extends Extension { /** * Prefix in .conf files */ - String PREFIX = "ditto/connectivity/hono-connection"; + String PREFIX = "ditto.connectivity.hono-connection"; /** * Gets the Base URI configuration value diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 2bd23e85d9..cacb8d41f7 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -13,10 +13,28 @@ package org.eclipse.ditto.connectivity.model; +/** + * Possible Aliases for Address used by connections of type 'Hono' + */ public enum HonoAddressAlias { + /** + * telemetry address alias + */ TELEMETRY("telemetry"), + + /** + * event address alias + */ EVENT("event"), + + /** + * command&control address alias + */ COMMAND("commandAndControl"), + + /** + * command response address alias + */ COMMAND_RESPONSE("commandResponse"); private final String name; @@ -25,6 +43,11 @@ public enum HonoAddressAlias { this.name = name; } + /** + * Gets the name of the alias + * + * @return The name of the alias + */ public String getName() { return name; } diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java index 014fbd02cb..3a4d99c001 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java @@ -24,7 +24,7 @@ import org.eclipse.ditto.json.JsonObjectBuilder; /** - * Address aliases used by connections of type 'Hono' + * Address values for each alias used by connections of type 'Hono' * * @since 2.5.0 */ @@ -65,7 +65,7 @@ public String getEventAddress() { /** * Gets the 'commandAndControl' alias value * - * @return The command&control address + * @return The commandAndControl address */ public String getCommandAndControlAddress() { diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java index 6d22882291..a2654e3ad7 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java @@ -102,6 +102,8 @@ static UserPasswordCredentials fromJson(final JsonObject jsonObject) { /** * Create credentials with username and password. * + * @param username the username + * @param password the password * @return credentials. */ public static UserPasswordCredentials newInstance(final String username, final String password) { diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveConnectionStatusResponse.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveConnectionStatusResponse.java index de68f2dd13..0663e6a575 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveConnectionStatusResponse.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveConnectionStatusResponse.java @@ -444,6 +444,8 @@ public Builder withAddressStatus(final ResourceStatus resourceStatus) { case SSH_TUNNEL: sshTunnelStatus = addToList(sshTunnelStatus, resourceStatus); break; + default: + // Do nothing } return this; } From 70386d06980370ae35de3a280cec41b74fb719dd Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Fri, 24 Jun 2022 17:29:38 +0300 Subject: [PATCH 09/65] Connection "enrichment" Signed-off-by: Andrey Balarev --- .../connectivity/api/DefaultHonoConfig.java | 24 ++- .../ditto/connectivity/api/HonoConfig.java | 38 +++-- .../connectivity/model/HonoAddressAlias.java | 13 ++ .../model/HonoAddressAliasValues.java | 138 ------------------ .../DefaultClientActorPropsFactory.java | 48 +++++- .../src/main/resources/connectivity.conf | 3 + 6 files changed, 99 insertions(+), 165 deletions(-) delete mode 100644 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java index 3ddc1e87c3..458a204814 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java @@ -13,7 +13,6 @@ package org.eclipse.ditto.connectivity.api; import org.eclipse.ditto.base.model.common.ConditionChecker; -import org.eclipse.ditto.connectivity.model.HonoAddressAliasValues; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.Credentials; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; @@ -28,11 +27,10 @@ public final class DefaultHonoConfig implements HonoConfig { private final String baseUri; + private final Boolean validateCertificates; private final SaslMechanism saslMechanism; private final String bootstrapServers; - private final HonoAddressAliasValues honoAddressAliasValues; - private final Credentials credentials; public DefaultHonoConfig(final ActorSystem actorSystem) { @@ -40,15 +38,10 @@ public DefaultHonoConfig(final ActorSystem actorSystem) { final Config config = actorSystem.settings().config().getConfig(PREFIX); this.baseUri = config.getString(ConfigValues.BASE_URI.getConfigPath()); + this.validateCertificates = config.getBoolean(ConfigValues.VALIDATE_CERTIFICATES.getConfigPath()); this.saslMechanism = config.getEnum(SaslMechanism.class, ConfigValues.SASL_MECHANISM.getConfigPath()); this.bootstrapServers = config.getString(ConfigValues.BOOTSTRAP_SERVERS.getConfigPath()); - honoAddressAliasValues = HonoAddressAliasValues.newInstance( - config.getString(ConfigValues.TELEMETRY_ADDRESS.getConfigPath()), - config.getString(ConfigValues.EVENT_ADDRESS.getConfigPath()), - config.getString(ConfigValues.COMMAND_AND_CONTROL_ADDRESS.getConfigPath()), - config.getString(ConfigValues.COMMAND_RESPONSE_ADDRESS.getConfigPath())); - this.credentials = UserPasswordCredentials.newInstance( config.getString(ConfigValues.USERNAME.getConfigPath()), config.getString(ConfigValues.PASSWORD.getConfigPath())); @@ -59,6 +52,11 @@ public String getBaseUri() { return baseUri; } + @Override + public Boolean getValidateCertificates() { + return validateCertificates; + } + @Override public SaslMechanism getSaslMechanism() { return saslMechanism; @@ -70,13 +68,13 @@ public String getBootstrapServers() { } @Override - public HonoAddressAliasValues getAddressAliases(final ConnectionId connectionId) { - return honoAddressAliasValues; + public Credentials getCredentials(final ConnectionId connectionId) { + return credentials; } @Override - public Credentials getCredentials(final ConnectionId connectionId) { - return credentials; + public String getTenantId(ConnectionId connectionId) { + return connectionId.toString(); } } \ No newline at end of file diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index bc6d2d3d51..baaf855eaf 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -11,13 +11,14 @@ * SPDX-License-Identifier: EPL-2.0 */ package org.eclipse.ditto.connectivity.api; + import java.util.List; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.Credentials; -import org.eclipse.ditto.connectivity.model.HonoAddressAliasValues; import org.eclipse.ditto.internal.utils.akka.AkkaClassLoader; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; + import akka.actor.AbstractExtensionId; import akka.actor.ActorSystem; import akka.actor.ExtendedActorSystem; @@ -41,6 +42,11 @@ public interface HonoConfig extends Extension { */ String getBaseUri(); + /* + + + */ + Boolean getValidateCertificates(); /** * Gets the SASL mechanism of Hono-connection (Kafka specific property) * @@ -56,20 +62,20 @@ public interface HonoConfig extends Extension { String getBootstrapServers(); /** - * Gets the connection address aliases + * Gets the credentials for specified Hono-connection * - * @param connectionId The connection ID of the connection to get aliases - * @return {@link org.eclipse.ditto.connectivity.model.HonoAddressAliasValues} + * @param connectionId The connection ID of the connection + * @return The credentials of the connection */ - HonoAddressAliasValues getAddressAliases(ConnectionId connectionId); + Credentials getCredentials(ConnectionId connectionId); /** - * Gets the credentials for specified Hono-connection + * Gets the Kafka GroupId property * - * @param connectionId The connection ID of the connection to get credentials - * @return The credentials of the connection + * @param connectionId The connection ID of the connection + * @return GroupId */ - Credentials getCredentials(ConnectionId connectionId); + String getTenantId(ConnectionId connectionId); enum ConfigValues implements KnownConfigValue { @@ -78,11 +84,21 @@ enum ConfigValues implements KnownConfigValue { */ BASE_URI("base-uri", ""), + /** + * validateCertificates boolean property + */ + VALIDATE_CERTIFICATES("validateCertificates", false), + /** * SASL mechanism for connections of type Hono */ SASL_MECHANISM("sasl-mechanism", "plain"), + /** + * Kafka Group ID property + */ + GROUP_ID("groupId", ""), + /** * Bootstrap servers, comma separated */ @@ -177,5 +193,9 @@ enum SaslMechanism { SaslMechanism(String value) { this.value = value; } + + public String getValue() { + return value; + } } } \ No newline at end of file diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index cacb8d41f7..72053a24e1 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -13,6 +13,8 @@ package org.eclipse.ditto.connectivity.model; +import java.util.Arrays; + /** * Possible Aliases for Address used by connections of type 'Hono' */ @@ -52,4 +54,15 @@ public String getName() { return name; } + /** + * @return the Enum representation for the given string. + * @throws IllegalArgumentException if unknown string. + */ + public static HonoAddressAlias fromName(String s) throws IllegalArgumentException { + return Arrays.stream(HonoAddressAlias.values()) + .filter(v -> v.name.equals(s)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown Address Alias value: " + s)); + } + } diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java deleted file mode 100644 index 3a4d99c001..0000000000 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasValues.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2021 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.connectivity.model; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; - -import javax.annotation.concurrent.Immutable; - -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonFieldDefinition; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonObjectBuilder; - -/** - * Address values for each alias used by connections of type 'Hono' - * - * @since 2.5.0 - */ -@Immutable -public final class HonoAddressAliasValues { - - private final String telemetry; - private final String event; - private final String commandAndControl; - private final String commandResponse; - - private HonoAddressAliasValues(final String telemetry, final String event, final String commandAndControl, - final String commandResponse) { - this.telemetry = telemetry; - this.event = event; - this.commandAndControl = commandAndControl; - this.commandResponse = commandResponse; - } - - /** - * Gets the 'telemetry' alias configuration value - * - * @return The telemetry address - */ - public String getTelemetryAddress() { - return telemetry; - } - - /** - * Gets the 'event' alias configuration value - * - * @return The event address - */ - public String getEventAddress() { - return event; - } - - /** - * Gets the 'commandAndControl' alias value - * - * @return The commandAndControl address - */ - - public String getCommandAndControlAddress() { - return commandAndControl; - } - - /** - * Gets the 'commandResponse' alias value - * - * @return The commandResponse address - */ - public String getCommandResponseAddress() { - return commandResponse; - } - - public JsonObject toJson() { - final JsonObjectBuilder jsonObjectBuilder = JsonFactory.newObjectBuilder(); - jsonObjectBuilder.set(JsonFields.TELEMETRY, telemetry); - jsonObjectBuilder.set(JsonFields.EVENT, event); - jsonObjectBuilder.set(JsonFields.COMMAND_AND_CONTROL, commandAndControl); - jsonObjectBuilder.set(JsonFields.COMMAND_RESPONSE, commandResponse); - return jsonObjectBuilder.build(); - } - - static HonoAddressAliasValues fromJson(final JsonObject jsonObject) { - final String telemetry = jsonObject.getValueOrThrow(JsonFields.TELEMETRY); - final String event = jsonObject.getValueOrThrow(JsonFields.EVENT); - final String commandAndControl = jsonObject.getValueOrThrow(JsonFields.COMMAND_AND_CONTROL); - final String commandResponse = jsonObject.getValueOrThrow(JsonFields.COMMAND_RESPONSE); - return new HonoAddressAliasValues(telemetry, event, commandAndControl, commandResponse); - } - - /** - * Create credentials with username and password. - * - * @return credentials. - */ - public static HonoAddressAliasValues newInstance(final String telemetry, final String event, - final String commandAndControl, final String commandResponse) { - return new HonoAddressAliasValues(telemetry, event, commandAndControl, commandResponse); - } - - /** - * JSON field definitions. - */ - public static final class JsonFields extends Credentials.JsonFields { - - private static final ConcurrentMap> DESERIALIZER_MAP = - new ConcurrentHashMap<>(); - /** - * JSON field containing the telemetry - */ - public static final JsonFieldDefinition TELEMETRY = JsonFieldDefinition.ofString("telemetry"); - - /** - * JSON field containing the event - */ - public static final JsonFieldDefinition EVENT = JsonFieldDefinition.ofString("event"); - - /** - * JSON field containing the event - */ - public static final JsonFieldDefinition COMMAND_AND_CONTROL = JsonFieldDefinition.ofString("command"); - - /** - * JSON field containing the event - */ - public static final JsonFieldDefinition COMMAND_RESPONSE = JsonFieldDefinition.ofString("command_response"); - } -} diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index 4a2fec3370..42f98b5aa6 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -12,6 +12,9 @@ */ package org.eclipse.ditto.connectivity.service.messaging; +import java.util.Map; +import java.util.Optional; + import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -20,12 +23,19 @@ import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; +import org.eclipse.ditto.connectivity.model.HonoAddressAlias; +import org.eclipse.ditto.connectivity.model.Source; +import org.eclipse.ditto.connectivity.model.Target; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpClientActor; import org.eclipse.ditto.connectivity.service.messaging.httppush.HttpPushClientActor; import org.eclipse.ditto.connectivity.service.messaging.kafka.KafkaClientActor; import org.eclipse.ditto.connectivity.service.messaging.mqtt.hivemq.HiveMqtt3ClientActor; import org.eclipse.ditto.connectivity.service.messaging.mqtt.hivemq.HiveMqtt5ClientActor; import org.eclipse.ditto.connectivity.service.messaging.rabbitmq.RabbitMQClientActor; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonValue; import com.typesafe.config.Config; @@ -107,12 +117,40 @@ private Connection getEnrichedConnection(final ActorSystem actorSystem, final Co connection.getConnectionType(), connection.getConnectionStatus(), honoConfig.getBaseUri()) + .validateCertificate(honoConfig.getValidateCertificates()) + .specificConfig(Map.of( + "saslMechanism", honoConfig.getSaslMechanism().getValue(), + "bootstrapServers", honoConfig.getBootstrapServers())) .credentials(honoConfig.getCredentials(connection.getId())) -// .sources(connection.getSources() -// .stream() -// .map(source -> new Source( -// source.getAddresses().map(replaceAlias).collect(Collectors.toList()); -// return source + .sources(connection.getSources() + .stream() + .map(source -> ConnectivityModelFactory.sourceFromJson( + resolveSourceAliases(source, honoConfig.getTenantId(connection.getId())), 1)) + .toList()) + .targets(connection.getTargets() + .stream() + .map(target -> ConnectivityModelFactory.targetFromJson( + resolveTargetAlias(target, honoConfig.getTenantId(connection.getId())))) + .toList()) + .build(); + } + + private JsonObject resolveSourceAliases(final Source source, String tenantId) { + return JsonFactory.newObjectBuilder(source.toJson()) + .set(Source.JsonFields.REPLY_TARGET, ) + .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() + .map(a -> HonoAddressAlias.fromName(a) + "/" + tenantId) + .map(JsonValue::of) + .toList())) + .build(); + } + + private JsonObject resolveTargetAlias(final Target target, String tenantId) { + return JsonFactory.newObjectBuilder(target.toJson()) + .set(Target.JsonFields.ADDRESS, Optional.of(target.getAddress()) + .map(a -> HonoAddressAlias.fromName(a) + "/" + tenantId) + .orElse("") + ) .build(); } diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index 006f84cf41..3a4d0df313 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -23,6 +23,9 @@ ditto { base-uri = "tcp://localhost:9990" base-uri = ${?HONO_CONNECTION_URI} + validate-certificates = false + validate-certificates = ${?HONO_CONNECTION_VALIDATE_CERTIFICATE} + sasl-mechanism = "PLAIN" sasl-mechanism = ${?HONO_CONNECTION_SASL_MECHANISM} From c70c88bcd13374ea79fa3aa467d00320027ba4b7 Mon Sep 17 00:00:00 2001 From: "Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM)" Date: Wed, 29 Jun 2022 14:40:20 +0300 Subject: [PATCH 10/65] CR-11462 Add HonoValidator impl and Junit tests --- ...nnectionConfigurationInvalidException.java | 2 +- .../connectivity/model/HonoAddressAlias.java | 46 ++++- .../service/messaging/hono/HonoValidator.java | 167 ++++++++++++++++++ .../service/messaging/hono/package-info.java | 14 ++ .../ConnectionPersistenceActor.java | 2 + .../messaging/hono/HonoValidatorTest.java | 162 +++++++++++++++++ 6 files changed, 389 insertions(+), 4 deletions(-) create mode 100644 connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java create mode 100644 connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/package-info.java create mode 100644 connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionConfigurationInvalidException.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionConfigurationInvalidException.java index 3578d459b9..13b40f0962 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionConfigurationInvalidException.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionConfigurationInvalidException.java @@ -26,7 +26,7 @@ import org.eclipse.ditto.base.model.json.JsonParsableException; /** - * Thrown if the the configuration of a {@link Connection} was invalid. + * Thrown if the configuration of a {@link Connection} was invalid. */ @Immutable @JsonParsableException(errorCode = ConnectionConfigurationInvalidException.ERROR_CODE) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index cacb8d41f7..3df8e0d48a 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -13,6 +13,13 @@ package org.eclipse.ditto.connectivity.model; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + + /** * Possible Aliases for Address used by connections of type 'Hono' */ @@ -30,15 +37,17 @@ public enum HonoAddressAlias { /** * command&control address alias */ - COMMAND("commandAndControl"), + COMMAND("command"), /** * command response address alias */ - COMMAND_RESPONSE("commandResponse"); + COMMAND_RESPONSE("command_response"); private final String name; + private static final Map HONO_ADDRESS_ALIAS_MAP; + HonoAddressAlias(String name) { this.name = name; } @@ -49,7 +58,38 @@ public enum HonoAddressAlias { * @return The name of the alias */ public String getName() { - return name; + return name; + } + + static { + Map map = new ConcurrentHashMap<>(); + for (HonoAddressAlias alias : HonoAddressAlias.values()) { + map.put(alias.getName(), alias); + } + HONO_ADDRESS_ALIAS_MAP = Collections.unmodifiableMap(map); + } + + /** + * Returns all defined HonoAddressAlias names + * + * @return A list with HonoAddressAlias names + */ + public static List names() { + return List.copyOf(HONO_ADDRESS_ALIAS_MAP.keySet()); + } + + /** + * Returns the HonoAddressAlias to which the given name is mapped + * + * @param name of HonoAddressAlias + * @return the HonoAddressAlias to which the given name is mapped + */ + public static Optional fromName(String name) { + try { + return Optional.of(HONO_ADDRESS_ALIAS_MAP.get(name)); + } catch (NullPointerException | ClassCastException ex) { + return Optional.empty(); + } } } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java new file mode 100644 index 0000000000..bdefac6622 --- /dev/null +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2021 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.service.messaging.hono; + +import static org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders.newEntityPlaceholder; +import static org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders.newFeaturePlaceholder; +import static org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders.newPolicyPlaceholder; +import static org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders.newThingPlaceholder; + +import java.text.MessageFormat; +import java.util.function.Supplier; + +import javax.annotation.concurrent.Immutable; +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectionConfigurationInvalidException; +import org.eclipse.ditto.connectivity.model.ConnectionType; +import org.eclipse.ditto.connectivity.model.HonoAddressAlias; +import org.eclipse.ditto.connectivity.model.Source; +import org.eclipse.ditto.connectivity.model.Target; +import org.eclipse.ditto.connectivity.service.config.ConnectivityConfig; +import org.eclipse.ditto.connectivity.service.messaging.validation.AbstractProtocolValidator; +import org.eclipse.ditto.placeholders.PlaceholderFactory; + +import akka.actor.ActorSystem; +@Immutable +public final class HonoValidator extends AbstractProtocolValidator { + + private static final String INVALID_SOURCE_ADDRESS_ALIAS_FORMAT = "The provided source address is not valid: {0}." + + " It should be one of the defined {1} aliases."; + private static final String INVALID_TARGET_ADDRESS_ALIAS_FORMAT = "The provided target address is not" + + "valid: {0}. It should be 'command' alias."; + private static final String NOT_EMPTY_FORMAT = "The provided {0} in your target address may not be empty."; + + @Nullable private static HonoValidator instance; + + private HonoValidator() { + super(); + } + + /** + * Returns an instance of the Hono validator. + * + * @return the instance. + */ + public static HonoValidator getInstance() { + HonoValidator result = instance; + if (null == result) { + result = new HonoValidator(); + instance = result; + } + return result; + } + + @Override + public ConnectionType type() { + return ConnectionType.HONO; + } + + @Override + public void validate(final Connection connection, + final DittoHeaders dittoHeaders, + final ActorSystem actorSystem, + final ConnectivityConfig connectivityConfig) { + validateSourceConfigs(connection, dittoHeaders); + validateTargetConfigs(connection, dittoHeaders); + validatePayloadMappings(connection, actorSystem, connectivityConfig, dittoHeaders); + } + + @Override + protected void validateSource(final Source source, final DittoHeaders dittoHeaders, + final Supplier sourceDescription) { + source.getEnforcement().ifPresent(enforcement -> { + validateTemplate(enforcement.getInput(), dittoHeaders, PlaceholderFactory.newHeadersPlaceholder()); + enforcement.getFilters().forEach(filterTemplate -> + validateTemplate(filterTemplate, dittoHeaders, newThingPlaceholder(), newPolicyPlaceholder(), + newEntityPlaceholder(), newFeaturePlaceholder())); + }); + source.getAddresses().forEach( + address -> validateSourceAddress(address, dittoHeaders)); + validateSourceQos(source, dittoHeaders); + } + + @Override + protected void validateTarget(final Target target, final DittoHeaders dittoHeaders, + final Supplier targetDescription) { + validateTargetAddress(target.getAddress(), dittoHeaders); + validateExtraFields(target); + } + + private static void validateSourceQos(final Source source, final DittoHeaders dittoHeaders) { + source.getQos().ifPresent(qos -> { + if (qos < 0 || qos > 1) { + throw ConnectionConfigurationInvalidException + .newBuilder("Invalid 'qos' value for Kafka source, supported are: <0> or <1>. " + + "Configured 'qos' value was: <" + qos + ">" + ) + .dittoHeaders(dittoHeaders) + .build(); + } + }); + } + + private static void validateTargetAddress(final String address, final DittoHeaders dittoHeaders) { + if (address.isEmpty()) { + throwEmptyException(dittoHeaders); + } + + HonoAddressAlias.fromName(address).filter(alias -> alias == HonoAddressAlias.COMMAND) + .orElseThrow(() -> buildInvalidTargetAddressException(address, dittoHeaders)); + } + + private static void validateSourceAddress(final String address, final DittoHeaders dittoHeaders) { + if (address.isEmpty()) { + throwEmptyException(dittoHeaders); + } + + var honoAddressAlias = HonoAddressAlias.fromName(address); + honoAddressAlias.filter(alias -> alias != HonoAddressAlias.COMMAND) + .orElseThrow(() -> { + String aliases = HonoAddressAlias.names() + .stream() + .filter(item -> !item.equalsIgnoreCase(HonoAddressAlias.COMMAND.getName())) + .toList() + .toString(); + return buildInvalidSourceAddressException(address, dittoHeaders, aliases); + }); + + } + + private static void throwEmptyException(final DittoHeaders dittoHeaders) { + final String message = MessageFormat.format(NOT_EMPTY_FORMAT, "address"); + throw ConnectionConfigurationInvalidException.newBuilder(message) + .dittoHeaders(dittoHeaders) + .build(); + } + + private static ConnectionConfigurationInvalidException buildInvalidTargetAddressException(String address, + final DittoHeaders dittoHeaders) { + final String message = MessageFormat.format(INVALID_TARGET_ADDRESS_ALIAS_FORMAT, address); + throw ConnectionConfigurationInvalidException.newBuilder(message) + .dittoHeaders(dittoHeaders) + .build(); + } + + private static ConnectionConfigurationInvalidException buildInvalidSourceAddressException(String address, + final DittoHeaders dittoHeaders, String definedAliases) { + final String message = MessageFormat.format(INVALID_SOURCE_ADDRESS_ALIAS_FORMAT, address, + definedAliases); + throw ConnectionConfigurationInvalidException.newBuilder(message) + .dittoHeaders(dittoHeaders) + .build(); + } + +} diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/package-info.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/package-info.java new file mode 100644 index 0000000000..8665cb6659 --- /dev/null +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2017 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +@org.eclipse.ditto.utils.jsr305.annotations.AllParametersAndReturnValuesAreNonnullByDefault +package org.eclipse.ditto.connectivity.service.messaging.hono; diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index 3d396f2a9f..ac46f2deae 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -79,6 +79,7 @@ import org.eclipse.ditto.connectivity.service.messaging.ClientActorPropsFactory; import org.eclipse.ditto.connectivity.service.messaging.ClientActorRefs; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpValidator; +import org.eclipse.ditto.connectivity.service.messaging.hono.HonoValidator; import org.eclipse.ditto.connectivity.service.messaging.httppush.HttpPushValidator; import org.eclipse.ditto.connectivity.service.messaging.kafka.KafkaValidator; import org.eclipse.ditto.connectivity.service.messaging.monitoring.logs.ConnectionLogger; @@ -1133,6 +1134,7 @@ private ConnectivityCommandInterceptor getCommandValidator( AmqpValidator.newInstance(), Mqtt3Validator.newInstance(mqttConfig), Mqtt5Validator.newInstance(mqttConfig), + HonoValidator.getInstance(), KafkaValidator.getInstance(connectivityConfig.getConnectionConfig().doubleDecodingEnabled()), HttpPushValidator.newInstance(connectivityConfig.getConnectionConfig().getHttpPushConfig())); diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java new file mode 100644 index 0000000000..d0ee825d6f --- /dev/null +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2017 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.service.messaging.hono; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.eclipse.ditto.connectivity.service.messaging.TestConstants.Authorization.AUTHORIZATION_CONTEXT; + +import java.util.concurrent.TimeUnit; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectionConfigurationInvalidException; +import org.eclipse.ditto.connectivity.model.ConnectionId; +import org.eclipse.ditto.connectivity.model.ConnectionType; +import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; +import org.eclipse.ditto.connectivity.model.ConnectivityStatus; +import org.eclipse.ditto.connectivity.model.Topic; +import org.eclipse.ditto.connectivity.service.config.ConnectivityConfig; +import org.eclipse.ditto.connectivity.service.messaging.TestConstants; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import akka.actor.ActorSystem; +import akka.testkit.javadsl.TestKit; + +/** + * Unit test for {@link org.eclipse.ditto.connectivity.service.messaging.hono.HonoValidatorTest}. + */ +public final class HonoValidatorTest { + + private static final ConnectionId CONNECTION_ID = TestConstants.createRandomConnectionId(); + private static ActorSystem actorSystem; + private static ConnectivityConfig connectivityConfig; + + private HonoValidator underTest; + + @BeforeClass + public static void initTestFixture() { + actorSystem = ActorSystem.create("AkkaTestSystem", TestConstants.CONFIG); + connectivityConfig = TestConstants.CONNECTIVITY_CONFIG; + } + + @AfterClass + public static void tearDown() { + if (actorSystem != null) { + TestKit.shutdownActorSystem(actorSystem, scala.concurrent.duration.Duration.apply(5, TimeUnit.SECONDS), + false); + } + } + + @Before + public void setUp() { + underTest = HonoValidator.getInstance(); + } + + @Test + public void testValidSourceAddress() { + final DittoHeaders emptyDittoHeaders = DittoHeaders.empty(); + underTest.validate(getConnectionWithSource("event"), emptyDittoHeaders, actorSystem, + connectivityConfig); + underTest.validate(getConnectionWithSource("telemetry"), emptyDittoHeaders, actorSystem, + connectivityConfig); + underTest.validate(getConnectionWithSource("command_response"), emptyDittoHeaders, actorSystem, + connectivityConfig); + } + + @Test + public void testInvalidSourceAddress() { + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithSource(""), "empty"); + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithSource("command"), + "[command_response, telemetry, event]"); + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithSource("events/"), + "[command_response, telemetry, event]"); + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithSource("hono.telemetry" + + ".c4bc9a62-8516-4232-bb81-dbbfe4d0fa8c_hub"), "[command_response, telemetry, event]"); + verifyConnectionConfigurationInvalidExceptionIsThrown(getConnectionWithSource("ditto*a")); + + } + + @Test + public void testInvalidSourceQos() { + verifyConnectionConfigurationInvalidExceptionIsThrown( + ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, ConnectionType.HONO, + ConnectivityStatus.OPEN, "tcp://localhost:999999") + .sources(singletonList(ConnectivityModelFactory.newSourceBuilder() + .address("event") + .authorizationContext(AUTHORIZATION_CONTEXT) + .qos(3) + .build())) + .build()); + } + + @Test + public void testValidTargetAddress() { + final DittoHeaders emptyDittoHeaders = DittoHeaders.empty(); + underTest.validate(getConnectionWithTarget("command"), emptyDittoHeaders, actorSystem, connectivityConfig); + } + + @Test + public void testInvalidTargetAddress() { + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget(""), "empty"); + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("event"), "command"); + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("telemtry"), "command"); + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("command_response"), + "command"); + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage( + getConnectionWithTarget("hono.command.c4bc9a62-8516-4232-bb81-dbbfe4d0fa8c_hub/{{thing:id}}"), + "command"); + } + + private static Connection getConnectionWithTarget(final String target) { + return ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, ConnectionType.HONO, + ConnectivityStatus.OPEN, "tcp://localhost:1883") + .targets(singletonList(ConnectivityModelFactory.newTargetBuilder() + .address(target) + .authorizationContext(AUTHORIZATION_CONTEXT) + .qos(1) + .topics(Topic.LIVE_EVENTS) + .build())) + .build(); + } + + private static Connection getConnectionWithSource(final String sourceAddress) { + return ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, ConnectionType.HONO, + ConnectivityStatus.OPEN, "tcp://localhost:99999") + .sources(singletonList(ConnectivityModelFactory.newSourceBuilder() + .address(sourceAddress) + .authorizationContext(AUTHORIZATION_CONTEXT) + .qos(1) + .build())) + .build(); + } + + private void verifyConnectionConfigurationInvalidExceptionIsThrown(final Connection connection) { + assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) + .isThrownBy( + () -> underTest.validate(connection, DittoHeaders.empty(), actorSystem, connectivityConfig)); + } + + private void verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(final Connection connection, + String messages) { + assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) + .isThrownBy( + () -> underTest.validate(connection, DittoHeaders.empty(), actorSystem, connectivityConfig)) + .withMessageContaining(messages); + } + +} + From 455891a9a3309350a792e00e3da90ba62034d3af Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Tue, 5 Jul 2022 12:58:56 +0300 Subject: [PATCH 11/65] Connection "enrichment" Signed-off-by: Andrey Balarev --- .../ditto/connectivity/api/HonoConfig.java | 27 +------ .../connectivity/model/HonoAddressAlias.java | 49 +++++++++++-- .../DefaultClientActorPropsFactory.java | 72 +++++++++++++++---- .../src/main/resources/connectivity.conf | 12 ---- .../main/resources/ditto-cluster-downing.conf | 2 +- 5 files changed, 102 insertions(+), 60 deletions(-) diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index baaf855eaf..de989f3934 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -87,43 +87,18 @@ enum ConfigValues implements KnownConfigValue { /** * validateCertificates boolean property */ - VALIDATE_CERTIFICATES("validateCertificates", false), + VALIDATE_CERTIFICATES("validate-certificates", false), /** * SASL mechanism for connections of type Hono */ SASL_MECHANISM("sasl-mechanism", "plain"), - /** - * Kafka Group ID property - */ - GROUP_ID("groupId", ""), - /** * Bootstrap servers, comma separated */ BOOTSTRAP_SERVERS("bootstrap-servers", ""), - /** - * Telemetry address alias - */ - TELEMETRY_ADDRESS("telemetry", ""), - - /** - * Event address alias - */ - EVENT_ADDRESS("event", ""), - - /** - * Command and control address alias - */ - COMMAND_AND_CONTROL_ADDRESS("commandAndControl", ""), - - /** - * Command response address alias - */ - COMMAND_RESPONSE_ADDRESS("commandResponse", ""), - /** * Username */ diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 72053a24e1..2bb28181dc 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -32,12 +32,17 @@ public enum HonoAddressAlias { /** * command&control address alias */ - COMMAND("commandAndControl"), + COMMAND("command"), /** * command response address alias */ - COMMAND_RESPONSE("commandResponse"); + COMMAND_RESPONSE("command_response"), + + /** + * unknown alias + */ + UNKNOWN(""); private final String name; @@ -55,14 +60,44 @@ public String getName() { } /** - * @return the Enum representation for the given string. - * @throws IllegalArgumentException if unknown string. + * Gets the alias enum that have the given name. + * + * @param alias the alias name to get + * @return {@link HonoAddressAlias} + */ + public static HonoAddressAlias fromName(String alias) { + return Arrays.stream(HonoAddressAlias.values()) + .filter(v -> v.name.equals(alias)) + .findFirst() + .orElse(UNKNOWN); + } + + /** + * Resolves the input as a potential address alias or returns the same value if not an existing alias. + * + * @param alias the alias name to resolve + * @param tenantId the tenantId - used in the resolve pattern + * @param thingSuffix if true, adds '/{{thing:id}}' suffix on resolve - needed for replyTarget addresses + * if false - does not add suffix + * @return the resolved alias or the same value if not an alias */ - public static HonoAddressAlias fromName(String s) throws IllegalArgumentException { + public static String resolve(String alias, String tenantId, boolean thingSuffix) throws IllegalArgumentException { return Arrays.stream(HonoAddressAlias.values()) - .filter(v -> v.name.equals(s)) + .filter(v -> v.name.equals(alias)) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Unknown Address Alias value: " + s)); + .map(found -> "hono." + found + "." + tenantId + (thingSuffix ? "/{{thing:id}}" : "")) + .orElse(alias); + } + + /** + * Resolves the input as a potential address alias or returns the same value if not an existing alias. + * + * @param alias the alias name to resolve + * @param tenantId the tenantId - used in the resolve pattern + * @return the resolved alias or the same value if not an alias + */ + public static String resolve(String alias, String tenantId) throws IllegalArgumentException { + return resolve(alias, tenantId, false); } } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index 42f98b5aa6..417de99f3d 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -21,9 +21,11 @@ import org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.connectivity.api.HonoConfig; import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.HonoAddressAlias; +import org.eclipse.ditto.connectivity.model.ReplyTarget; import org.eclipse.ditto.connectivity.model.Source; import org.eclipse.ditto.connectivity.model.Target; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpClientActor; @@ -35,6 +37,7 @@ import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; import org.eclipse.ditto.json.JsonValue; import com.typesafe.config.Config; @@ -112,6 +115,7 @@ public Props getActorPropsForType(final Connection connection, final ActorRef pr private Connection getEnrichedConnection(final ActorSystem actorSystem, final Connection connection) { var honoConfig = HonoConfig.get(actorSystem); + final ConnectionId connectionId = connection.getId(); return ConnectivityModelFactory.newConnectionBuilder( connection.getId(), connection.getConnectionType(), @@ -120,38 +124,78 @@ private Connection getEnrichedConnection(final ActorSystem actorSystem, final Co .validateCertificate(honoConfig.getValidateCertificates()) .specificConfig(Map.of( "saslMechanism", honoConfig.getSaslMechanism().getValue(), - "bootstrapServers", honoConfig.getBootstrapServers())) - .credentials(honoConfig.getCredentials(connection.getId())) + "bootstrapServers", honoConfig.getBootstrapServers(), + "groupId", honoConfig.getTenantId(connectionId) + "_" + connectionId)) + .credentials(honoConfig.getCredentials(connectionId)) .sources(connection.getSources() .stream() .map(source -> ConnectivityModelFactory.sourceFromJson( - resolveSourceAliases(source, honoConfig.getTenantId(connection.getId())), 1)) + resolveSourceAliases2(source, honoConfig.getTenantId(connectionId)), 1)) .toList()) .targets(connection.getTargets() .stream() .map(target -> ConnectivityModelFactory.targetFromJson( - resolveTargetAlias(target, honoConfig.getTenantId(connection.getId())))) + resolveTargetAlias(target, honoConfig.getTenantId(connectionId)))) .toList()) .build(); } private JsonObject resolveSourceAliases(final Source source, String tenantId) { - return JsonFactory.newObjectBuilder(source.toJson()) - .set(Source.JsonFields.REPLY_TARGET, ) + JsonObjectBuilder sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() - .map(a -> HonoAddressAlias.fromName(a) + "/" + tenantId) + .map(address -> HonoAddressAlias.resolve(address, tenantId)) .map(JsonValue::of) - .toList())) - .build(); + .toList())); + source.getReplyTarget().ifPresent(replyTarget -> + Optional.of(replyTarget.getAddress()).ifPresent(address -> { + final JsonObjectBuilder replyTargetBuilder = JsonFactory.newObjectBuilder(replyTarget.toJson()) + .set(ReplyTarget.JsonFields.ADDRESS, HonoAddressAlias.resolve(address, tenantId, true)); + Optional.of(replyTarget.getHeaderMapping()).ifPresent(mapping -> { + Map newMapping = mapping.getMapping(); + switch (HonoAddressAlias.fromName(address)) { + case COMMAND -> { + newMapping.put("device_id", "{{ thing:id }}"); + newMapping.put("subject", + "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); + } + case COMMAND_RESPONSE -> newMapping.put("status", "{{ header:status }}"); + } + newMapping.put("correlation-id", "{{ header:correlation-id }}"); + replyTargetBuilder.set(ReplyTarget.JsonFields.HEADER_MAPPING, + ConnectivityModelFactory.newHeaderMapping(newMapping).toJson()); + }); + sourceBuilder.set(Source.JsonFields.REPLY_TARGET, replyTargetBuilder.build()); + })); + return sourceBuilder.build(); + } + + private JsonObject resolveSourceAliases2(final Source source, String tenantId) { + JsonObjectBuilder sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) + .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() + .map(address -> HonoAddressAlias.resolve(address, tenantId)) + .map(JsonValue::of) + .toList())); + source.getReplyTarget().ifPresent(replyTarget -> { + sourceBuilder.set("replyTarget/address", HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)); + switch (HonoAddressAlias.fromName(replyTarget.getAddress())) { + case COMMAND -> { + sourceBuilder.set("replyTarget/headerMapping/device_id", "{{ thing:id }}"); + sourceBuilder.set("replyTarget/headerMapping/subject", + "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); + } + case COMMAND_RESPONSE -> sourceBuilder.set("replyTarget/headerMapping/status", "{{ header:status }}"); + } + sourceBuilder.set("replyTarget/headerMapping/correlation-id", "{{ header:correlation-id }}"); + }); + return sourceBuilder.build(); } private JsonObject resolveTargetAlias(final Target target, String tenantId) { - return JsonFactory.newObjectBuilder(target.toJson()) + JsonObjectBuilder targetBuilder = JsonFactory.newObjectBuilder(target.toJson()) .set(Target.JsonFields.ADDRESS, Optional.of(target.getAddress()) - .map(a -> HonoAddressAlias.fromName(a) + "/" + tenantId) - .orElse("") - ) - .build(); + .map(address -> HonoAddressAlias.resolve(address, tenantId, true)) + .orElse(null), jsonField -> !target.getAddress().isEmpty()); + return targetBuilder.build(); } } diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index 3a4d0df313..83cc72975a 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -31,18 +31,6 @@ ditto { bootstrap-servers = "bootstrap.server:9999" bootstrap-servers = ${?HONO_CONNECTION_BOOTSTRAP_SERVERS} - - telemetry = "telemetry/..." - telemetry = ${?HONO_CONNECTION_TELEMETRY_ADDRESS} - - event = "event/..." - event = ${?HONO_CONNECTION_EVENT_ADDRESS} - - commandAndControl = "command/..." - commandAndControl = ${?HONO_CONNECTION_COMMAND_ADDRESS} - - commandResponse = "response/..." - commandResponse = ${?HONO_CONNECTION_RESPONSE_ADDRESS} } user-indicated-errors-base = [ diff --git a/internal/utils/config/src/main/resources/ditto-cluster-downing.conf b/internal/utils/config/src/main/resources/ditto-cluster-downing.conf index aa828f8caa..f1faf8ab5b 100644 --- a/internal/utils/config/src/main/resources/ditto-cluster-downing.conf +++ b/internal/utils/config/src/main/resources/ditto-cluster-downing.conf @@ -1,6 +1,6 @@ ditto.cluster { sbr { - auto-enable-after=1h + auto-enable-after=2d auto-enable-after=${?DITTO_CLUSTER_SBR_AUTO_ENABLE_AFTER} } } From 7b72beed89ad9f80140733d619ac381371acb70a Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Tue, 5 Jul 2022 13:10:56 +0300 Subject: [PATCH 12/65] Fixed HonoValidator merge issues Signed-off-by: Andrey Balarev --- .../connectivity/model/HonoAddressAlias.java | 58 ++++++++++++------- .../DefaultClientActorPropsFactory.java | 34 +---------- .../service/messaging/hono/HonoValidator.java | 8 +-- .../main/resources/ditto-cluster-downing.conf | 2 +- 4 files changed, 43 insertions(+), 59 deletions(-) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 1d12e661f1..2db3e53b0e 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -13,8 +13,7 @@ package org.eclipse.ditto.connectivity.model; -import java.util.Arrays; - +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -47,7 +46,7 @@ public enum HonoAddressAlias { COMMAND_RESPONSE("command_response"), /** - * unknown alias + * unrecognized address alias */ UNKNOWN(""); @@ -68,43 +67,58 @@ public String getName() { return name; } - + static { + Map map = new ConcurrentHashMap<>(); + for (HonoAddressAlias alias : HonoAddressAlias.values()) { + map.put(alias.getName(), alias); + } + HONO_ADDRESS_ALIAS_MAP = Collections.unmodifiableMap(map); + } + + /** + * Returns all defined HonoAddressAlias names + * + * @return A list with HonoAddressAlias names + */ + public static List names() { + return new ArrayList<>(HONO_ADDRESS_ALIAS_MAP.keySet()); + } + /** - * Gets the alias enum that have the given name. + * Returns the HonoAddressAlias to which the given name is mapped * - * @param alias the alias name to get - * @return {@link HonoAddressAlias} + * @param name of HonoAddressAlias + * @return the HonoAddressAlias to which the given name is mapped */ - public static HonoAddressAlias fromName(String alias) { - return Arrays.stream(HonoAddressAlias.values()) - .filter(v -> v.name.equals(alias)) - .findFirst() - .orElse(UNKNOWN); + public static Optional fromName(String name) { + try { + return Optional.of(HONO_ADDRESS_ALIAS_MAP.get(name)); + } catch (NullPointerException | ClassCastException ex) { + return Optional.empty(); + } } /** - * Resolves the input as a potential address alias or returns the same value if not an existing alias. + * Resolves the input as a potential address alias or returns empty string if not an existing alias. * * @param alias the alias name to resolve * @param tenantId the tenantId - used in the resolve pattern * @param thingSuffix if true, adds '/{{thing:id}}' suffix on resolve - needed for replyTarget addresses - * if false - does not add suffix - * @return the resolved alias or the same value if not an alias + * if false - does not add suffix + * @return the resolved alias or empty if not an alias */ - public static String resolve(String alias, String tenantId, boolean thingSuffix) throws IllegalArgumentException { - return Arrays.stream(HonoAddressAlias.values()) - .filter(v -> v.name.equals(alias)) - .findFirst() + public static String resolve(String alias, String tenantId, boolean thingSuffix) { + return fromName(alias) .map(found -> "hono." + found + "." + tenantId + (thingSuffix ? "/{{thing:id}}" : "")) - .orElse(alias); + .orElse(""); } /** - * Resolves the input as a potential address alias or returns the same value if not an existing alias. + * Resolves the input as a potential address alias or returns empty string if not an existing aliass. * * @param alias the alias name to resolve * @param tenantId the tenantId - used in the resolve pattern - * @return the resolved alias or the same value if not an alias + * @return the resolved alias or empty if not an alias */ public static String resolve(String alias, String tenantId) throws IllegalArgumentException { return resolve(alias, tenantId, false); diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index 417de99f3d..9862e547bc 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -25,7 +25,6 @@ import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.HonoAddressAlias; -import org.eclipse.ditto.connectivity.model.ReplyTarget; import org.eclipse.ditto.connectivity.model.Source; import org.eclipse.ditto.connectivity.model.Target; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpClientActor; @@ -130,7 +129,7 @@ private Connection getEnrichedConnection(final ActorSystem actorSystem, final Co .sources(connection.getSources() .stream() .map(source -> ConnectivityModelFactory.sourceFromJson( - resolveSourceAliases2(source, honoConfig.getTenantId(connectionId)), 1)) + resolveSourceAliases(source, honoConfig.getTenantId(connectionId)), 1)) .toList()) .targets(connection.getTargets() .stream() @@ -141,35 +140,6 @@ private Connection getEnrichedConnection(final ActorSystem actorSystem, final Co } private JsonObject resolveSourceAliases(final Source source, String tenantId) { - JsonObjectBuilder sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) - .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() - .map(address -> HonoAddressAlias.resolve(address, tenantId)) - .map(JsonValue::of) - .toList())); - source.getReplyTarget().ifPresent(replyTarget -> - Optional.of(replyTarget.getAddress()).ifPresent(address -> { - final JsonObjectBuilder replyTargetBuilder = JsonFactory.newObjectBuilder(replyTarget.toJson()) - .set(ReplyTarget.JsonFields.ADDRESS, HonoAddressAlias.resolve(address, tenantId, true)); - Optional.of(replyTarget.getHeaderMapping()).ifPresent(mapping -> { - Map newMapping = mapping.getMapping(); - switch (HonoAddressAlias.fromName(address)) { - case COMMAND -> { - newMapping.put("device_id", "{{ thing:id }}"); - newMapping.put("subject", - "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); - } - case COMMAND_RESPONSE -> newMapping.put("status", "{{ header:status }}"); - } - newMapping.put("correlation-id", "{{ header:correlation-id }}"); - replyTargetBuilder.set(ReplyTarget.JsonFields.HEADER_MAPPING, - ConnectivityModelFactory.newHeaderMapping(newMapping).toJson()); - }); - sourceBuilder.set(Source.JsonFields.REPLY_TARGET, replyTargetBuilder.build()); - })); - return sourceBuilder.build(); - } - - private JsonObject resolveSourceAliases2(final Source source, String tenantId) { JsonObjectBuilder sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() .map(address -> HonoAddressAlias.resolve(address, tenantId)) @@ -177,7 +147,7 @@ private JsonObject resolveSourceAliases2(final Source source, String tenantId) { .toList())); source.getReplyTarget().ifPresent(replyTarget -> { sourceBuilder.set("replyTarget/address", HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)); - switch (HonoAddressAlias.fromName(replyTarget.getAddress())) { + switch (HonoAddressAlias.fromName(replyTarget.getAddress()).orElse(HonoAddressAlias.UNKNOWN)) { case COMMAND -> { sourceBuilder.set("replyTarget/headerMapping/device_id", "{{ thing:id }}"); sourceBuilder.set("replyTarget/headerMapping/subject", diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java index bdefac6622..5bc7f76ea5 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java @@ -20,8 +20,8 @@ import java.text.MessageFormat; import java.util.function.Supplier; -import javax.annotation.concurrent.Immutable; import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; import org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.connectivity.model.Connection; @@ -42,7 +42,7 @@ public final class HonoValidator extends AbstractProtocolValidator { " It should be one of the defined {1} aliases."; private static final String INVALID_TARGET_ADDRESS_ALIAS_FORMAT = "The provided target address is not" + "valid: {0}. It should be 'command' alias."; - private static final String NOT_EMPTY_FORMAT = "The provided {0} in your target address may not be empty."; + private static final String NOT_EMPTY_FORMAT = "The provided {0} in your target address may not be empty."; @Nullable private static HonoValidator instance; @@ -118,8 +118,8 @@ private static void validateTargetAddress(final String address, final DittoHeade throwEmptyException(dittoHeaders); } - HonoAddressAlias.fromName(address).filter(alias -> alias == HonoAddressAlias.COMMAND) - .orElseThrow(() -> buildInvalidTargetAddressException(address, dittoHeaders)); + HonoAddressAlias.fromName(address).filter(alias -> alias == HonoAddressAlias.COMMAND) + .orElseThrow(() -> buildInvalidTargetAddressException(address, dittoHeaders)); } private static void validateSourceAddress(final String address, final DittoHeaders dittoHeaders) { diff --git a/internal/utils/config/src/main/resources/ditto-cluster-downing.conf b/internal/utils/config/src/main/resources/ditto-cluster-downing.conf index f1faf8ab5b..aa828f8caa 100644 --- a/internal/utils/config/src/main/resources/ditto-cluster-downing.conf +++ b/internal/utils/config/src/main/resources/ditto-cluster-downing.conf @@ -1,6 +1,6 @@ ditto.cluster { sbr { - auto-enable-after=2d + auto-enable-after=1h auto-enable-after=${?DITTO_CLUSTER_SBR_AUTO_ENABLE_AFTER} } } From 9426aecd763c93368769d383d8927e66fc69ab06 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 6 Jul 2022 14:16:19 +0300 Subject: [PATCH 13/65] Hono-connection enrichment fixed Signed-off-by: Andrey Balarev --- .../ditto/connectivity/api/HonoConfig.java | 11 +++--- .../connectivity/model/HonoAddressAlias.java | 28 ++++++-------- .../model/ImmutableHeaderMapping.java | 6 +-- .../DefaultClientActorPropsFactory.java | 38 ++++++++++++++----- .../service/messaging/hono/HonoValidator.java | 4 +- .../src/main/resources/connectivity.conf | 2 +- 6 files changed, 51 insertions(+), 38 deletions(-) diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index de989f3934..adcf05aa09 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -42,9 +42,10 @@ public interface HonoConfig extends Extension { */ String getBaseUri(); - /* - - + /** + * Gets validateCertificates boolean property + * + * @return validateCertificates boolean property */ Boolean getValidateCertificates(); /** @@ -70,10 +71,10 @@ public interface HonoConfig extends Extension { Credentials getCredentials(ConnectionId connectionId); /** - * Gets the Kafka GroupId property + * Gets Hub tenant_id property * * @param connectionId The connection ID of the connection - * @return GroupId + * @return hubTenantId */ String getTenantId(ConnectionId connectionId); diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 2db3e53b0e..03a67f9f8a 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -20,7 +20,6 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; - /** * Possible Aliases for Address used by connections of type 'Hono' */ @@ -43,17 +42,20 @@ public enum HonoAddressAlias { /** * command response address alias */ - COMMAND_RESPONSE("command_response"), - - /** - * unrecognized address alias - */ - UNKNOWN(""); + COMMAND_RESPONSE("command_response"); private final String name; private static final Map HONO_ADDRESS_ALIAS_MAP; + static { + Map map = new ConcurrentHashMap<>(); + for (HonoAddressAlias alias : HonoAddressAlias.values()) { + map.put(alias.getName(), alias); + } + HONO_ADDRESS_ALIAS_MAP = Collections.unmodifiableMap(map); + } + HonoAddressAlias(String name) { this.name = name; } @@ -67,14 +69,6 @@ public String getName() { return name; } - static { - Map map = new ConcurrentHashMap<>(); - for (HonoAddressAlias alias : HonoAddressAlias.values()) { - map.put(alias.getName(), alias); - } - HONO_ADDRESS_ALIAS_MAP = Collections.unmodifiableMap(map); - } - /** * Returns all defined HonoAddressAlias names * @@ -109,8 +103,8 @@ public static Optional fromName(String name) { */ public static String resolve(String alias, String tenantId, boolean thingSuffix) { return fromName(alias) - .map(found -> "hono." + found + "." + tenantId + (thingSuffix ? "/{{thing:id}}" : "")) - .orElse(""); + .map(found -> "hono." + found.getName() + "." + tenantId + (thingSuffix ? "/{{thing:id}}" : "")) + .orElse(alias); } /** diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableHeaderMapping.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableHeaderMapping.java index e6c6bc6f0b..b789da0f1a 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableHeaderMapping.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableHeaderMapping.java @@ -22,16 +22,16 @@ import javax.annotation.concurrent.Immutable; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonField; import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; /** * Immutable implementation of a {@link HeaderMapping}. */ @Immutable -final class ImmutableHeaderMapping implements HeaderMapping { +public final class ImmutableHeaderMapping implements HeaderMapping { private final Map mapping; @@ -52,7 +52,7 @@ public Map getMapping() { * @throws NullPointerException if {@code jsonObject} is {@code null}. * @throws org.eclipse.ditto.json.JsonParseException if {@code jsonObject} is not an appropriate JSON object. */ - static HeaderMapping fromJson(final JsonObject jsonObject) { + public static HeaderMapping fromJson(final JsonObject jsonObject) { return new ImmutableHeaderMapping(jsonObject.stream() .filter(f -> f.getValue().isString()) .collect(Collectors.toMap(JsonField::getKeyName, jsonField -> jsonField.getValue().asString()))); diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index 9862e547bc..fd41f7003f 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -25,8 +25,10 @@ import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.HonoAddressAlias; +import org.eclipse.ditto.connectivity.model.ImmutableHeaderMapping; import org.eclipse.ditto.connectivity.model.Source; import org.eclipse.ditto.connectivity.model.Target; +import org.eclipse.ditto.connectivity.model.Topic; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpClientActor; import org.eclipse.ditto.connectivity.service.messaging.httppush.HttpPushClientActor; import org.eclipse.ditto.connectivity.service.messaging.kafka.KafkaClientActor; @@ -146,17 +148,24 @@ private JsonObject resolveSourceAliases(final Source source, String tenantId) { .map(JsonValue::of) .toList())); source.getReplyTarget().ifPresent(replyTarget -> { - sourceBuilder.set("replyTarget/address", HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)); - switch (HonoAddressAlias.fromName(replyTarget.getAddress()).orElse(HonoAddressAlias.UNKNOWN)) { - case COMMAND -> { - sourceBuilder.set("replyTarget/headerMapping/device_id", "{{ thing:id }}"); - sourceBuilder.set("replyTarget/headerMapping/subject", - "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); - } - case COMMAND_RESPONSE -> sourceBuilder.set("replyTarget/headerMapping/status", "{{ header:status }}"); + var headerMapping = replyTarget.getHeaderMapping().toJson() + .setValue("correlation-id", "{{ header:correlation-id }}"); + if (HonoAddressAlias.COMMAND.getName().equals(replyTarget.getAddress())) { + headerMapping = headerMapping + .setValue("device_id", "{{ thing:id }}") + .setValue("subject", + "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); } - sourceBuilder.set("replyTarget/headerMapping/correlation-id", "{{ header:correlation-id }}"); + sourceBuilder.set("replyTarget", replyTarget.toBuilder() + .address(HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)) + .headerMapping(ImmutableHeaderMapping.fromJson(headerMapping)) + .build().toJson()); }); + if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getName())) { + sourceBuilder.set("headerMapping", source.getHeaderMapping().toJson() + .setValue("correlation-id", "{{ header:correlation-id }}") + .setValue("status", "{{ header:status }}")); + } return sourceBuilder.build(); } @@ -164,7 +173,16 @@ private JsonObject resolveTargetAlias(final Target target, String tenantId) { JsonObjectBuilder targetBuilder = JsonFactory.newObjectBuilder(target.toJson()) .set(Target.JsonFields.ADDRESS, Optional.of(target.getAddress()) .map(address -> HonoAddressAlias.resolve(address, tenantId, true)) - .orElse(null), jsonField -> !target.getAddress().isEmpty()); + .orElse(null), jsonField -> !jsonField.getValue().asString().isEmpty()); + var headerMapping = target.getHeaderMapping().toJson() + .setValue("device_id", "{{ thing:id }}") + .setValue("correlation-id", "{{ header:correlation-id }}") + .setValue("subject", "{{ header:subject | fn:default(topic:action-subject) }}"); + if (target.getTopics().stream() + .anyMatch(topic -> topic.getTopic() == Topic.LIVE_MESSAGES || topic.getTopic() == Topic.LIVE_COMMANDS)) { + headerMapping.setValue("response-required", "{{ header:response-required }}"); + } + targetBuilder.set("headerMapping", headerMapping); return targetBuilder.build(); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java index 5bc7f76ea5..45896a06d4 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java @@ -35,6 +35,7 @@ import org.eclipse.ditto.placeholders.PlaceholderFactory; import akka.actor.ActorSystem; + @Immutable public final class HonoValidator extends AbstractProtocolValidator { @@ -137,7 +138,6 @@ private static void validateSourceAddress(final String address, final DittoHeade .toString(); return buildInvalidSourceAddressException(address, dittoHeaders, aliases); }); - } private static void throwEmptyException(final DittoHeaders dittoHeaders) { @@ -149,7 +149,7 @@ private static void throwEmptyException(final DittoHeaders dittoHeaders) { private static ConnectionConfigurationInvalidException buildInvalidTargetAddressException(String address, final DittoHeaders dittoHeaders) { - final String message = MessageFormat.format(INVALID_TARGET_ADDRESS_ALIAS_FORMAT, address); + final String message = MessageFormat.format(INVALID_TARGET_ADDRESS_ALIAS_FORMAT, address); throw ConnectionConfigurationInvalidException.newBuilder(message) .dittoHeaders(dittoHeaders) .build(); diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index 83cc72975a..d3a7119d4d 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -20,7 +20,7 @@ ditto { config-provider = "org.eclipse.ditto.connectivity.api.DefaultHonoConfig", config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} - base-uri = "tcp://localhost:9990" + base-uri = "tcp://localhost:30092" base-uri = ${?HONO_CONNECTION_URI} validate-certificates = false From fba66b9a2a14d11de75287826e456b1a106fe641 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 6 Jul 2022 22:33:26 +0300 Subject: [PATCH 14/65] ConfigValues renamed to HonoConfigValue, minor fixes Signed-off-by: Andrey Balarev --- .../connectivity/api/DefaultHonoConfig.java | 65 +++++++++++++++---- .../ditto/connectivity/api/HonoConfig.java | 14 ++-- .../connectivity/model/HonoAddressAlias.java | 2 +- .../messaging/hono/HonoValidatorTest.java | 2 +- 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java index 458a204814..a73ea7f6e8 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java @@ -12,9 +12,10 @@ */ package org.eclipse.ditto.connectivity.api; +import java.util.Objects; + import org.eclipse.ditto.base.model.common.ConditionChecker; import org.eclipse.ditto.connectivity.model.ConnectionId; -import org.eclipse.ditto.connectivity.model.Credentials; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import com.typesafe.config.Config; @@ -31,50 +32,92 @@ public final class DefaultHonoConfig implements HonoConfig { private final SaslMechanism saslMechanism; private final String bootstrapServers; - private final Credentials credentials; + private final UserPasswordCredentials credentials; public DefaultHonoConfig(final ActorSystem actorSystem) { ConditionChecker.checkNotNull(actorSystem, "actorSystem"); final Config config = actorSystem.settings().config().getConfig(PREFIX); - this.baseUri = config.getString(ConfigValues.BASE_URI.getConfigPath()); - this.validateCertificates = config.getBoolean(ConfigValues.VALIDATE_CERTIFICATES.getConfigPath()); - this.saslMechanism = config.getEnum(SaslMechanism.class, ConfigValues.SASL_MECHANISM.getConfigPath()); - this.bootstrapServers = config.getString(ConfigValues.BOOTSTRAP_SERVERS.getConfigPath()); + this.baseUri = config.getString(HonoConfigValue.BASE_URI.getConfigPath()); + this.validateCertificates = config.getBoolean(HonoConfigValue.VALIDATE_CERTIFICATES.getConfigPath()); + this.saslMechanism = config.getEnum(SaslMechanism.class, HonoConfigValue.SASL_MECHANISM.getConfigPath()); + this.bootstrapServers = config.getString(HonoConfigValue.BOOTSTRAP_SERVERS.getConfigPath()); this.credentials = UserPasswordCredentials.newInstance( - config.getString(ConfigValues.USERNAME.getConfigPath()), - config.getString(ConfigValues.PASSWORD.getConfigPath())); + config.getString(HonoConfigValue.USERNAME.getConfigPath()), + config.getString(HonoConfigValue.PASSWORD.getConfigPath())); } + /** + * @return Base URI, including port number + */ @Override public String getBaseUri() { return baseUri; } + /** + * @return validateCertificates boolean property + */ @Override public Boolean getValidateCertificates() { return validateCertificates; } + /** + * @return SASL mechanism property + */ @Override public SaslMechanism getSaslMechanism() { return saslMechanism; } + /** + * @return Bootstrap servers address(es) + */ @Override public String getBootstrapServers() { return bootstrapServers; } + /** + * Gets credentials for hono messaging + * @param connectionId The connection ID of the connection + * @return {@link org.eclipse.ditto.connectivity.model.UserPasswordCredentials} for hub messaging + */ @Override - public Credentials getCredentials(final ConnectionId connectionId) { + public UserPasswordCredentials getCredentials(final ConnectionId connectionId) { return credentials; } @Override - public String getTenantId(ConnectionId connectionId) { - return connectionId.toString(); + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final DefaultHonoConfig that = (DefaultHonoConfig) o; + return Objects.equals(baseUri, that.baseUri) + && Objects.equals(validateCertificates, that.validateCertificates) + && Objects.equals(saslMechanism, that.saslMechanism) + && Objects.equals(bootstrapServers, that.bootstrapServers); + + } + @Override + public int hashCode() { + return Objects.hash(baseUri, validateCertificates, saslMechanism, bootstrapServers); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "baseUri=" + baseUri + + ", validateCertificates=" + validateCertificates + + ", saslMechanism=" + saslMechanism + + ", bootstrapServers=" + bootstrapServers + + "]"; } } \ No newline at end of file diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index adcf05aa09..9a373d8867 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -15,7 +15,7 @@ import java.util.List; import org.eclipse.ditto.connectivity.model.ConnectionId; -import org.eclipse.ditto.connectivity.model.Credentials; +import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.internal.utils.akka.AkkaClassLoader; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; @@ -68,7 +68,7 @@ public interface HonoConfig extends Extension { * @param connectionId The connection ID of the connection * @return The credentials of the connection */ - Credentials getCredentials(ConnectionId connectionId); + UserPasswordCredentials getCredentials(ConnectionId connectionId); /** * Gets Hub tenant_id property @@ -76,12 +76,14 @@ public interface HonoConfig extends Extension { * @param connectionId The connection ID of the connection * @return hubTenantId */ - String getTenantId(ConnectionId connectionId); + default String getTenantId(ConnectionId connectionId) { + return ""; + } - enum ConfigValues implements KnownConfigValue { + enum HonoConfigValue implements KnownConfigValue { /** - * Base URI, including port number (without protocol prefix) + * Base URI, including port number */ BASE_URI("base-uri", ""), @@ -113,7 +115,7 @@ enum ConfigValues implements KnownConfigValue { private final String path; private final Object defaultValue; - ConfigValues(final String thePath, final Object theDefaultValue) { + HonoConfigValue(final String thePath, final Object theDefaultValue) { path = thePath; defaultValue = theDefaultValue; } diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 03a67f9f8a..3a5a2edc2f 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -21,7 +21,7 @@ import java.util.concurrent.ConcurrentHashMap; /** - * Possible Aliases for Address used by connections of type 'Hono' + * Possible address aliases used by connections of type 'Hono' */ public enum HonoAddressAlias { /** diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java index d0ee825d6f..273b8f3d24 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java @@ -113,7 +113,7 @@ public void testValidTargetAddress() { public void testInvalidTargetAddress() { verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget(""), "empty"); verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("event"), "command"); - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("telemtry"), "command"); + verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("telemetry"), "command"); verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("command_response"), "command"); verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage( From ba5b81adbd97d1cde4aec8a4d78ff37a9e200c69 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Thu, 7 Jul 2022 17:36:11 +0300 Subject: [PATCH 15/65] Review issues fixes, unit tests fixed Signed-off-by: Andrey Balarev --- .../connectivity/api/DefaultHonoConfig.java | 19 ++++++---- .../ditto/connectivity/api/HonoConfig.java | 35 +++++++++++++++---- .../connectivity/model/HonoAddressAlias.java | 2 +- .../DefaultClientActorPropsFactory.java | 4 +-- .../src/main/resources/connectivity.conf | 2 +- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java index a73ea7f6e8..36376acb38 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java @@ -12,6 +12,8 @@ */ package org.eclipse.ditto.connectivity.api; +import java.net.URI; +import java.util.Arrays; import java.util.Objects; import org.eclipse.ditto.base.model.common.ConditionChecker; @@ -27,8 +29,8 @@ */ public final class DefaultHonoConfig implements HonoConfig { - private final String baseUri; - private final Boolean validateCertificates; + private final URI baseUri; + private final boolean validateCertificates; private final SaslMechanism saslMechanism; private final String bootstrapServers; @@ -38,10 +40,12 @@ public DefaultHonoConfig(final ActorSystem actorSystem) { ConditionChecker.checkNotNull(actorSystem, "actorSystem"); final Config config = actorSystem.settings().config().getConfig(PREFIX); - this.baseUri = config.getString(HonoConfigValue.BASE_URI.getConfigPath()); + this.baseUri = HonoConfig.getUri(HonoConfigValue.BASE_URI.getConfigPath()); this.validateCertificates = config.getBoolean(HonoConfigValue.VALIDATE_CERTIFICATES.getConfigPath()); this.saslMechanism = config.getEnum(SaslMechanism.class, HonoConfigValue.SASL_MECHANISM.getConfigPath()); this.bootstrapServers = config.getString(HonoConfigValue.BOOTSTRAP_SERVERS.getConfigPath()); + // Validate bootstrap servers + Arrays.stream(this.bootstrapServers.split(",")).forEach(HonoConfig::getUri); this.credentials = UserPasswordCredentials.newInstance( config.getString(HonoConfigValue.USERNAME.getConfigPath()), @@ -52,7 +56,7 @@ public DefaultHonoConfig(final ActorSystem actorSystem) { * @return Base URI, including port number */ @Override - public String getBaseUri() { + public URI getBaseUri() { return baseUri; } @@ -60,7 +64,7 @@ public String getBaseUri() { * @return validateCertificates boolean property */ @Override - public Boolean getValidateCertificates() { + public boolean isValidateCertificates() { return validateCertificates; } @@ -102,12 +106,13 @@ public boolean equals(final Object o) { return Objects.equals(baseUri, that.baseUri) && Objects.equals(validateCertificates, that.validateCertificates) && Objects.equals(saslMechanism, that.saslMechanism) - && Objects.equals(bootstrapServers, that.bootstrapServers); + && Objects.equals(bootstrapServers, that.bootstrapServers) + && Objects.equals(credentials, that.credentials); } @Override public int hashCode() { - return Objects.hash(baseUri, validateCertificates, saslMechanism, bootstrapServers); + return Objects.hash(baseUri, validateCertificates, saslMechanism, bootstrapServers, credentials); } @Override diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index 9a373d8867..78f1092c31 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -12,11 +12,15 @@ */ package org.eclipse.ditto.connectivity.api; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; +import org.eclipse.ditto.base.model.common.ConditionChecker; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.internal.utils.akka.AkkaClassLoader; +import org.eclipse.ditto.internal.utils.config.DittoConfigError; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; import akka.actor.AbstractExtensionId; @@ -33,21 +37,22 @@ public interface HonoConfig extends Extension { /** * Prefix in .conf files */ - String PREFIX = "ditto.connectivity.hono-connection"; + String PREFIX = "ditto.connectivity.hono"; /** * Gets the Base URI configuration value * * @return the connection URI */ - String getBaseUri(); + URI getBaseUri(); /** * Gets validateCertificates boolean property * - * @return validateCertificates boolean property + * @return validateCertificates boolean property */ - Boolean getValidateCertificates(); + boolean isValidateCertificates(); + /** * Gets the SASL mechanism of Hono-connection (Kafka specific property) * @@ -76,7 +81,7 @@ public interface HonoConfig extends Extension { * @param connectionId The connection ID of the connection * @return hubTenantId */ - default String getTenantId(ConnectionId connectionId) { + default String getTenantId(final ConnectionId connectionId) { return ""; } @@ -94,7 +99,7 @@ enum HonoConfigValue implements KnownConfigValue { /** * SASL mechanism for connections of type Hono - */ + */ SASL_MECHANISM("sasl-mechanism", "plain"), /** @@ -139,9 +144,25 @@ public String getConfigPath() { * @return the {@code HonoConfig}. */ static HonoConfig get(final ActorSystem actorSystem) { + ConditionChecker.checkNotNull(actorSystem, "actorSystem"); return HonoConfig.ExtensionId.INSTANCE.get(actorSystem); } + /** + * Validates and gets URI from a string + * + * @param uri A {@link String} to be validated + * @return New {@link URI} from specified string + * @throws DittoConfigError if given string is not a valid URI + */ + static URI getUri(final String uri) throws DittoConfigError { + try { + return new URI(uri); + } catch (URISyntaxException e) { + throw new DittoConfigError(e); + } + } + /** * ID of the actor system extension to provide a Hono-connections configuration. */ @@ -166,7 +187,7 @@ enum SaslMechanism { PLAIN("plain"); - final String value; + private final String value; SaslMechanism(String value) { this.value = value; diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 3a5a2edc2f..7650932d38 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -35,7 +35,7 @@ public enum HonoAddressAlias { EVENT("event"), /** - * command&control address alias + * command&control address alias */ COMMAND("command"), diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index fd41f7003f..24ee937c83 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -121,8 +121,8 @@ private Connection getEnrichedConnection(final ActorSystem actorSystem, final Co connection.getId(), connection.getConnectionType(), connection.getConnectionStatus(), - honoConfig.getBaseUri()) - .validateCertificate(honoConfig.getValidateCertificates()) + honoConfig.getBaseUri().toString()) + .validateCertificate(honoConfig.isValidateCertificates()) .specificConfig(Map.of( "saslMechanism", honoConfig.getSaslMechanism().getValue(), "bootstrapServers", honoConfig.getBootstrapServers(), diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index d3a7119d4d..d03b1dd5ea 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -16,7 +16,7 @@ ditto { connectivity { - hono-connection { + hono { config-provider = "org.eclipse.ditto.connectivity.api.DefaultHonoConfig", config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} From 6a5aee5ea1d5ee92f677dd63a7ae9c78747032da Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Thu, 7 Jul 2022 17:36:11 +0300 Subject: [PATCH 16/65] Review issues fixes, unit tests fixed Signed-off-by: Andrey Balarev --- .../connectivity/api/DefaultHonoConfig.java | 19 ++++++---- .../ditto/connectivity/api/HonoConfig.java | 35 +++++++++++++++---- .../connectivity/model/HonoAddressAlias.java | 2 +- .../DefaultClientActorPropsFactory.java | 4 +-- .../src/main/resources/connectivity.conf | 4 +-- 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java index a73ea7f6e8..36376acb38 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java @@ -12,6 +12,8 @@ */ package org.eclipse.ditto.connectivity.api; +import java.net.URI; +import java.util.Arrays; import java.util.Objects; import org.eclipse.ditto.base.model.common.ConditionChecker; @@ -27,8 +29,8 @@ */ public final class DefaultHonoConfig implements HonoConfig { - private final String baseUri; - private final Boolean validateCertificates; + private final URI baseUri; + private final boolean validateCertificates; private final SaslMechanism saslMechanism; private final String bootstrapServers; @@ -38,10 +40,12 @@ public DefaultHonoConfig(final ActorSystem actorSystem) { ConditionChecker.checkNotNull(actorSystem, "actorSystem"); final Config config = actorSystem.settings().config().getConfig(PREFIX); - this.baseUri = config.getString(HonoConfigValue.BASE_URI.getConfigPath()); + this.baseUri = HonoConfig.getUri(HonoConfigValue.BASE_URI.getConfigPath()); this.validateCertificates = config.getBoolean(HonoConfigValue.VALIDATE_CERTIFICATES.getConfigPath()); this.saslMechanism = config.getEnum(SaslMechanism.class, HonoConfigValue.SASL_MECHANISM.getConfigPath()); this.bootstrapServers = config.getString(HonoConfigValue.BOOTSTRAP_SERVERS.getConfigPath()); + // Validate bootstrap servers + Arrays.stream(this.bootstrapServers.split(",")).forEach(HonoConfig::getUri); this.credentials = UserPasswordCredentials.newInstance( config.getString(HonoConfigValue.USERNAME.getConfigPath()), @@ -52,7 +56,7 @@ public DefaultHonoConfig(final ActorSystem actorSystem) { * @return Base URI, including port number */ @Override - public String getBaseUri() { + public URI getBaseUri() { return baseUri; } @@ -60,7 +64,7 @@ public String getBaseUri() { * @return validateCertificates boolean property */ @Override - public Boolean getValidateCertificates() { + public boolean isValidateCertificates() { return validateCertificates; } @@ -102,12 +106,13 @@ public boolean equals(final Object o) { return Objects.equals(baseUri, that.baseUri) && Objects.equals(validateCertificates, that.validateCertificates) && Objects.equals(saslMechanism, that.saslMechanism) - && Objects.equals(bootstrapServers, that.bootstrapServers); + && Objects.equals(bootstrapServers, that.bootstrapServers) + && Objects.equals(credentials, that.credentials); } @Override public int hashCode() { - return Objects.hash(baseUri, validateCertificates, saslMechanism, bootstrapServers); + return Objects.hash(baseUri, validateCertificates, saslMechanism, bootstrapServers, credentials); } @Override diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java index 9a373d8867..78f1092c31 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java @@ -12,11 +12,15 @@ */ package org.eclipse.ditto.connectivity.api; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; +import org.eclipse.ditto.base.model.common.ConditionChecker; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.internal.utils.akka.AkkaClassLoader; +import org.eclipse.ditto.internal.utils.config.DittoConfigError; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; import akka.actor.AbstractExtensionId; @@ -33,21 +37,22 @@ public interface HonoConfig extends Extension { /** * Prefix in .conf files */ - String PREFIX = "ditto.connectivity.hono-connection"; + String PREFIX = "ditto.connectivity.hono"; /** * Gets the Base URI configuration value * * @return the connection URI */ - String getBaseUri(); + URI getBaseUri(); /** * Gets validateCertificates boolean property * - * @return validateCertificates boolean property + * @return validateCertificates boolean property */ - Boolean getValidateCertificates(); + boolean isValidateCertificates(); + /** * Gets the SASL mechanism of Hono-connection (Kafka specific property) * @@ -76,7 +81,7 @@ public interface HonoConfig extends Extension { * @param connectionId The connection ID of the connection * @return hubTenantId */ - default String getTenantId(ConnectionId connectionId) { + default String getTenantId(final ConnectionId connectionId) { return ""; } @@ -94,7 +99,7 @@ enum HonoConfigValue implements KnownConfigValue { /** * SASL mechanism for connections of type Hono - */ + */ SASL_MECHANISM("sasl-mechanism", "plain"), /** @@ -139,9 +144,25 @@ public String getConfigPath() { * @return the {@code HonoConfig}. */ static HonoConfig get(final ActorSystem actorSystem) { + ConditionChecker.checkNotNull(actorSystem, "actorSystem"); return HonoConfig.ExtensionId.INSTANCE.get(actorSystem); } + /** + * Validates and gets URI from a string + * + * @param uri A {@link String} to be validated + * @return New {@link URI} from specified string + * @throws DittoConfigError if given string is not a valid URI + */ + static URI getUri(final String uri) throws DittoConfigError { + try { + return new URI(uri); + } catch (URISyntaxException e) { + throw new DittoConfigError(e); + } + } + /** * ID of the actor system extension to provide a Hono-connections configuration. */ @@ -166,7 +187,7 @@ enum SaslMechanism { PLAIN("plain"); - final String value; + private final String value; SaslMechanism(String value) { this.value = value; diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 3a5a2edc2f..7650932d38 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -35,7 +35,7 @@ public enum HonoAddressAlias { EVENT("event"), /** - * command&control address alias + * command&control address alias */ COMMAND("command"), diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index fd41f7003f..24ee937c83 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -121,8 +121,8 @@ private Connection getEnrichedConnection(final ActorSystem actorSystem, final Co connection.getId(), connection.getConnectionType(), connection.getConnectionStatus(), - honoConfig.getBaseUri()) - .validateCertificate(honoConfig.getValidateCertificates()) + honoConfig.getBaseUri().toString()) + .validateCertificate(honoConfig.isValidateCertificates()) .specificConfig(Map.of( "saslMechanism", honoConfig.getSaslMechanism().getValue(), "bootstrapServers", honoConfig.getBootstrapServers(), diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index d3a7119d4d..c3394364f6 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -16,7 +16,7 @@ ditto { connectivity { - hono-connection { + hono { config-provider = "org.eclipse.ditto.connectivity.api.DefaultHonoConfig", config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} @@ -91,7 +91,7 @@ ditto { connection { # A comma separated string of hostnames to which http requests will allowed. This overrides the blocked # hostnames i.e if a host is blocked *and* allowed, it will be allowed. - allowed-hostnames = "" + allowed-hostnames = "hono-endpoint" #allowed-hostnames = "localhost" allowed-hostnames = ${?CONNECTIVITY_CONNECTION_ALLOWED_HOSTNAMES} From e675897c84d504abaa33b92f2b9e9a9bcf349712 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 13 Jul 2022 12:40:23 +0300 Subject: [PATCH 17/65] DefaultHonoConfig class moved from api to service, URI init fixed --- .../connectivity/service/config}/DefaultHonoConfig.java | 5 +++-- connectivity/service/src/main/resources/connectivity.conf | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) rename connectivity/{api/src/main/java/org/eclipse/ditto/connectivity/api => service/src/main/java/org/eclipse/ditto/connectivity/service/config}/DefaultHonoConfig.java (95%) diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java similarity index 95% rename from connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java rename to connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java index 36376acb38..20edaea8e3 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/DefaultHonoConfig.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java @@ -10,13 +10,14 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.connectivity.api; +package org.eclipse.ditto.connectivity.service.config; import java.net.URI; import java.util.Arrays; import java.util.Objects; import org.eclipse.ditto.base.model.common.ConditionChecker; +import org.eclipse.ditto.connectivity.api.HonoConfig; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; @@ -40,7 +41,7 @@ public DefaultHonoConfig(final ActorSystem actorSystem) { ConditionChecker.checkNotNull(actorSystem, "actorSystem"); final Config config = actorSystem.settings().config().getConfig(PREFIX); - this.baseUri = HonoConfig.getUri(HonoConfigValue.BASE_URI.getConfigPath()); + this.baseUri = HonoConfig.getUri(config.getString(HonoConfigValue.BASE_URI.getConfigPath())); this.validateCertificates = config.getBoolean(HonoConfigValue.VALIDATE_CERTIFICATES.getConfigPath()); this.saslMechanism = config.getEnum(SaslMechanism.class, HonoConfigValue.SASL_MECHANISM.getConfigPath()); this.bootstrapServers = config.getString(HonoConfigValue.BOOTSTRAP_SERVERS.getConfigPath()); diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index c3394364f6..02105cab0e 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -17,7 +17,7 @@ ditto { connectivity { hono { - config-provider = "org.eclipse.ditto.connectivity.api.DefaultHonoConfig", + config-provider = "org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig", config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} base-uri = "tcp://localhost:30092" From cda7bb5b4c0239fcf3a2f7f91bb90fffabc66899 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Tue, 19 Jul 2022 11:43:10 +0200 Subject: [PATCH 18/65] Minor refactoring of ConfigWithFallback. * Extracted method for getting a JsonObject as a Map to improve readability. * Also use new features of Java 17 to improve readability. Signed-off-by: Juergen Fickel --- .../utils/config/ConfigWithFallback.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/ConfigWithFallback.java b/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/ConfigWithFallback.java index 1e15b1ecf3..4457b72b16 100644 --- a/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/ConfigWithFallback.java +++ b/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/ConfigWithFallback.java @@ -129,19 +129,20 @@ private static void validateArgument(final Object argument, final String argumen private static Config arrayToConfig(final KnownConfigValue[] knownConfigValues) { final Map fallbackValues = new HashMap<>(knownConfigValues.length); - for (final KnownConfigValue knownConfigValue : knownConfigValues) { - final Object fallbackValue = knownConfigValue.getDefaultValue(); - if (fallbackValue instanceof JsonObject) { - final Map fallbackMap = ((JsonObject) fallbackValue).stream() - .collect(Collectors.toMap(f -> f.getKey().toString(), JsonField::getValue)); - fallbackValues.put(knownConfigValue.getConfigPath(), fallbackMap); - } else { - fallbackValues.put(knownConfigValue.getConfigPath(), fallbackValue); + for (final var knownConfigValue : knownConfigValues) { + var fallbackValue = knownConfigValue.getDefaultValue(); + if (fallbackValue instanceof JsonObject jsonObject) { + fallbackValue = getJsonObjectAsMap(jsonObject); } + fallbackValues.put(knownConfigValue.getConfigPath(), fallbackValue); } return ConfigFactory.parseMap(fallbackValues); } + private static Map getJsonObjectAsMap(final JsonObject jsonObject) { + return jsonObject.stream().collect(Collectors.toMap(JsonField::getKeyName, JsonField::getValue)); + } + @Override public ConfigObject root() { return baseConfig.root(); From a843974afe166e120dd565f7ad2d71a240904745 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Tue, 19 Jul 2022 11:44:20 +0200 Subject: [PATCH 19/65] Streamline behaviour of DefaultScopedConfig in case of an unknown enum value. Signed-off-by: Juergen Fickel --- .../ditto/internal/utils/config/DefaultScopedConfig.java | 2 +- .../ditto/internal/utils/config/DefaultScopedConfigTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/DefaultScopedConfig.java b/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/DefaultScopedConfig.java index 495e9dfca5..a4a74cac7a 100644 --- a/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/DefaultScopedConfig.java +++ b/internal/utils/config/src/main/java/org/eclipse/ditto/internal/utils/config/DefaultScopedConfig.java @@ -316,7 +316,7 @@ public String getString(final String path) { public > T getEnum(final Class enumClass, final String path) { try { return config.getEnum(enumClass, path); - } catch (final ConfigException.Missing | ConfigException.WrongType e) { + } catch (final ConfigException.Missing | ConfigException.WrongType | ConfigException.BadValue e) { final var msgPattern = "Failed to get Enum for path <{0}>!"; throw new DittoConfigError(MessageFormat.format(msgPattern, appendToConfigPath(path)), e); } diff --git a/internal/utils/config/src/test/java/org/eclipse/ditto/internal/utils/config/DefaultScopedConfigTest.java b/internal/utils/config/src/test/java/org/eclipse/ditto/internal/utils/config/DefaultScopedConfigTest.java index d5296dd94c..24202d5858 100644 --- a/internal/utils/config/src/test/java/org/eclipse/ditto/internal/utils/config/DefaultScopedConfigTest.java +++ b/internal/utils/config/src/test/java/org/eclipse/ditto/internal/utils/config/DefaultScopedConfigTest.java @@ -21,7 +21,6 @@ import org.assertj.core.api.JUnitSoftAssertions; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; From 09907a9d1e61a1db2a3d18f9a1fdce066c1b4650 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Tue, 19 Jul 2022 12:20:07 +0200 Subject: [PATCH 20/65] Refactored HonoConfig. * Moved interface from connectivity API to connectivity service to limit its scope. * Moved utility method for getting URI from HonoConfig to implementing class because it is the only place where it is required now. * Changed getter for bootstrap server URIs to actually return a set of URIs instead of a comma separated string. * Renamed getter for credentials to state which type of credentials it returns in case there would be other credential types in future. * Let DefaultHonoConfig base on ScopedConfig to get DittoConfigErrors in case of configuration errors. * Added unit tests for DefaultHonoConfig. * Refined some Javadoc comments. Signed-off-by: Juergen Fickel --- .../service/config/DefaultHonoConfig.java | 127 ++++++---- .../service/config}/HonoConfig.java | 89 ++++--- .../DefaultClientActorPropsFactory.java | 65 +++-- .../service/config/DefaultHonoConfigTest.java | 236 ++++++++++++++++++ 4 files changed, 410 insertions(+), 107 deletions(-) rename connectivity/{api/src/main/java/org/eclipse/ditto/connectivity/api => service/src/main/java/org/eclipse/ditto/connectivity/service/config}/HonoConfig.java (57%) create mode 100644 connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfigTest.java diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java index 20edaea8e3..881492e2c5 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java @@ -12,86 +12,126 @@ */ package org.eclipse.ditto.connectivity.service.config; +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + import java.net.URI; -import java.util.Arrays; +import java.net.URISyntaxException; +import java.text.MessageFormat; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; + +import javax.annotation.concurrent.Immutable; -import org.eclipse.ditto.base.model.common.ConditionChecker; -import org.eclipse.ditto.connectivity.api.HonoConfig; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; +import org.eclipse.ditto.internal.utils.config.ConfigWithFallback; +import org.eclipse.ditto.internal.utils.config.DittoConfigError; +import org.eclipse.ditto.internal.utils.config.ScopedConfig; import com.typesafe.config.Config; import akka.actor.ActorSystem; /** - * Configuration class providing parameters for connection type 'Hono' in Ditto from static configuration + * Default implementation for {@link HonoConfig}. */ +@Immutable public final class DefaultHonoConfig implements HonoConfig { private final URI baseUri; private final boolean validateCertificates; private final SaslMechanism saslMechanism; - private final String bootstrapServers; - + private final Set bootstrapServerUris; private final UserPasswordCredentials credentials; + /** + * Constructs a {@code DefaultHonoConfig} for the specified ActorSystem. + * + * @param actorSystem the actor system that provides the overall core config. + * @throws NullPointerException if {@code actorSystem} is {@code null}. + */ public DefaultHonoConfig(final ActorSystem actorSystem) { - ConditionChecker.checkNotNull(actorSystem, "actorSystem"); - final Config config = actorSystem.settings().config().getConfig(PREFIX); - - this.baseUri = HonoConfig.getUri(config.getString(HonoConfigValue.BASE_URI.getConfigPath())); - this.validateCertificates = config.getBoolean(HonoConfigValue.VALIDATE_CERTIFICATES.getConfigPath()); - this.saslMechanism = config.getEnum(SaslMechanism.class, HonoConfigValue.SASL_MECHANISM.getConfigPath()); - this.bootstrapServers = config.getString(HonoConfigValue.BOOTSTRAP_SERVERS.getConfigPath()); - // Validate bootstrap servers - Arrays.stream(this.bootstrapServers.split(",")).forEach(HonoConfig::getUri); - - this.credentials = UserPasswordCredentials.newInstance( - config.getString(HonoConfigValue.USERNAME.getConfigPath()), - config.getString(HonoConfigValue.PASSWORD.getConfigPath())); + this(ConfigWithFallback.newInstance(checkNotNull(actorSystem, "actorSystem").settings().config(), + PREFIX, + HonoConfigValue.values())); + } + + private DefaultHonoConfig(final ScopedConfig scopedConfig) { + baseUri = getBaseUriOrThrow(scopedConfig); + validateCertificates = scopedConfig.getBoolean(HonoConfigValue.VALIDATE_CERTIFICATES.getConfigPath()); + saslMechanism = scopedConfig.getEnum(SaslMechanism.class, HonoConfigValue.SASL_MECHANISM.getConfigPath()); + bootstrapServerUris = Collections.unmodifiableSet(getBootstrapServerUrisOrThrow(scopedConfig)); + credentials = UserPasswordCredentials.newInstance( + scopedConfig.getString(HonoConfigValue.USERNAME.getConfigPath()), + scopedConfig.getString(HonoConfigValue.PASSWORD.getConfigPath()) + ); + } + + private static URI getBaseUriOrThrow(final Config scopedConfig) { + final var configPath = HonoConfigValue.BASE_URI.getConfigPath(); + try { + return new URI(scopedConfig.getString(configPath)); + } catch (final URISyntaxException e) { + throw new DittoConfigError( + MessageFormat.format("The string value at <{0}> is not a {1}: {2}", + configPath, + URI.class.getSimpleName(), + e.getMessage()), + e + ); + } + } + + private static Set getBootstrapServerUrisOrThrow(final Config scopedConfig) { + final var configPath = HonoConfigValue.BOOTSTRAP_SERVERS.getConfigPath(); + final BiFunction getUriOrThrow = (index, uriString) -> { + try { + return new URI(uriString.trim()); + } catch (final URISyntaxException e) { + throw new DittoConfigError( + MessageFormat.format("The string at index <{0}> for key <{1}> is not a valid URI: {2}", + index, + configPath, + e.getMessage()), + e + ); + } + }; + + final var bootstrapServersString = scopedConfig.getString(configPath); + final var bootstrapServerUriStrings = bootstrapServersString.split(","); + final Set result = new LinkedHashSet<>(bootstrapServerUriStrings.length); + for (var i = 0; i < bootstrapServerUriStrings.length; i++) { + result.add(getUriOrThrow.apply(i, bootstrapServerUriStrings[i])); + } + return result; } - /** - * @return Base URI, including port number - */ @Override public URI getBaseUri() { return baseUri; } - /** - * @return validateCertificates boolean property - */ @Override public boolean isValidateCertificates() { return validateCertificates; } - /** - * @return SASL mechanism property - */ @Override public SaslMechanism getSaslMechanism() { return saslMechanism; } - /** - * @return Bootstrap servers address(es) - */ @Override - public String getBootstrapServers() { - return bootstrapServers; + public Set getBootstrapServerUris() { + return bootstrapServerUris; } - /** - * Gets credentials for hono messaging - * @param connectionId The connection ID of the connection - * @return {@link org.eclipse.ditto.connectivity.model.UserPasswordCredentials} for hub messaging - */ @Override - public UserPasswordCredentials getCredentials(final ConnectionId connectionId) { + public UserPasswordCredentials getUserPasswordCredentials(final ConnectionId connectionId) { return credentials; } @@ -103,17 +143,18 @@ public boolean equals(final Object o) { if (o == null || getClass() != o.getClass()) { return false; } - final DefaultHonoConfig that = (DefaultHonoConfig) o; + final var that = (DefaultHonoConfig) o; return Objects.equals(baseUri, that.baseUri) && Objects.equals(validateCertificates, that.validateCertificates) && Objects.equals(saslMechanism, that.saslMechanism) - && Objects.equals(bootstrapServers, that.bootstrapServers) + && Objects.equals(bootstrapServerUris, that.bootstrapServerUris) && Objects.equals(credentials, that.credentials); } + @Override public int hashCode() { - return Objects.hash(baseUri, validateCertificates, saslMechanism, bootstrapServers, credentials); + return Objects.hash(baseUri, validateCertificates, saslMechanism, bootstrapServerUris, credentials); } @Override @@ -122,7 +163,7 @@ public String toString() { "baseUri=" + baseUri + ", validateCertificates=" + validateCertificates + ", saslMechanism=" + saslMechanism + - ", bootstrapServers=" + bootstrapServers + + ", bootstrapServers=" + bootstrapServerUris + "]"; } diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java similarity index 57% rename from connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java rename to connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java index 78f1092c31..31f90afe3d 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/HonoConfig.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java @@ -10,11 +10,12 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.ditto.connectivity.api; +package org.eclipse.ditto.connectivity.service.config; import java.net.URI; import java.net.URISyntaxException; import java.util.List; +import java.util.Set; import org.eclipse.ditto.base.model.common.ConditionChecker; import org.eclipse.ditto.connectivity.model.ConnectionId; @@ -29,8 +30,8 @@ import akka.actor.Extension; /** - * Configuration interface for connection type 'Hono' parameters - * Via actor system extension, it enables different implementations per service type (Ditto/Things) + * This interface provides access to the configuration properties Hono connections. + * The actual configuration can be obtained via actor system extension. */ public interface HonoConfig extends Extension { @@ -40,47 +41,51 @@ public interface HonoConfig extends Extension { String PREFIX = "ditto.connectivity.hono"; /** - * Gets the Base URI configuration value + * Gets the Base URI configuration value. * - * @return the connection URI + * @return the connection base URI. */ URI getBaseUri(); /** - * Gets validateCertificates boolean property + * Indicates whether the certificates should be validated. * - * @return validateCertificates boolean property + * @return {@code true} if the certificates should be validated, {@code false} else. */ boolean isValidateCertificates(); /** - * Gets the SASL mechanism of Hono-connection (Kafka specific property) + * Gets the SASL mechanism of Hono-connection (Kafka specific property). * - * @return {@link SaslMechanism} + * @return the configured SaslMechanism. */ SaslMechanism getSaslMechanism(); /** - * Gets bootstrap servers + * Returns the URIs of bootstrap servers. * - * @return {@link String} containing comma separated bootstrap server list + * @return an unmodifiable unsorted Set containing the URIs of bootstrap servers. */ - String getBootstrapServers(); + Set getBootstrapServerUris(); /** - * Gets the credentials for specified Hono-connection + * Gets the credentials for the specified Hono connection. * - * @param connectionId The connection ID of the connection - * @return The credentials of the connection + * @param connectionId the ID of the connection. + * @return the credentials of the connection. + * @throws NullPointerException if {@code connectionId} is {@code null}. */ - UserPasswordCredentials getCredentials(ConnectionId connectionId); + // TODO jff delete connection ID parameter because for Ditto it does not make sense. + UserPasswordCredentials getUserPasswordCredentials(ConnectionId connectionId); /** - * Gets Hub tenant_id property + * Gets the Hub tenant ID property for the specified Hono connection. * - * @param connectionId The connection ID of the connection - * @return hubTenantId + * @param connectionId the ID of the connection. + * @return the Hub tenant ID, {@code ""} by default. + * @throws NullPointerException if {@code connectionId} is {@code null}. */ + // TODO jff delete method after obtaining the tenant ID is moved to another place. default String getTenantId(final ConnectionId connectionId) { return ""; } @@ -88,39 +93,39 @@ default String getTenantId(final ConnectionId connectionId) { enum HonoConfigValue implements KnownConfigValue { /** - * Base URI, including port number + * Base URL, including port number. */ - BASE_URI("base-uri", ""), + BASE_URI("base-uri", "tcp://localhost:30092"), /** - * validateCertificates boolean property + * validateCertificates boolean property. */ VALIDATE_CERTIFICATES("validate-certificates", false), /** - * SASL mechanism for connections of type Hono + * SASL mechanism for connections of type Hono. */ - SASL_MECHANISM("sasl-mechanism", "plain"), + SASL_MECHANISM("sasl-mechanism", SaslMechanism.PLAIN.name()), /** - * Bootstrap servers, comma separated + * Bootstrap servers, comma separated. */ - BOOTSTRAP_SERVERS("bootstrap-servers", ""), + BOOTSTRAP_SERVERS("bootstrap-servers", "bootstrap.server:9999"), /** - * Username + * The Hono credentials username. */ USERNAME("username", ""), /** - * Password + * The Hono credentials password. */ PASSWORD("password", ""); private final String path; private final Object defaultValue; - HonoConfigValue(final String thePath, final Object theDefaultValue) { + private HonoConfigValue(final String thePath, final Object theDefaultValue) { path = thePath; defaultValue = theDefaultValue; } @@ -144,8 +149,7 @@ public String getConfigPath() { * @return the {@code HonoConfig}. */ static HonoConfig get(final ActorSystem actorSystem) { - ConditionChecker.checkNotNull(actorSystem, "actorSystem"); - return HonoConfig.ExtensionId.INSTANCE.get(actorSystem); + return HonoConfig.ExtensionId.INSTANCE.get(ConditionChecker.checkNotNull(actorSystem, "actorSystem")); } /** @@ -158,7 +162,7 @@ static HonoConfig get(final ActorSystem actorSystem) { static URI getUri(final String uri) throws DittoConfigError { try { return new URI(uri); - } catch (URISyntaxException e) { + } catch (final URISyntaxException e) { throw new DittoConfigError(e); } } @@ -174,13 +178,14 @@ final class ExtensionId extends AbstractExtensionId { @Override public HonoConfig createExtension(final ExtendedActorSystem system) { - - final String implementation = system.settings().config().getString(CONFIG_PATH); - return AkkaClassLoader.instantiate(system, HonoConfig.class, - implementation, + ConditionChecker.checkNotNull(system, "system"); + return AkkaClassLoader.instantiate(system, + HonoConfig.class, + system.settings().config().getString(CONFIG_PATH), List.of(ActorSystem.class), List.of(system)); } + } enum SaslMechanism { @@ -189,12 +194,20 @@ enum SaslMechanism { private final String value; - SaslMechanism(String value) { + private SaslMechanism(final String value) { this.value = value; } - public String getValue() { + /** + * Returns the value of this SaslMechanism. + * + * @return the value. + */ + @Override + public String toString() { return value; } + } + } \ No newline at end of file diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index 25104b5369..b1c3d1dbbe 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -12,16 +12,17 @@ */ package org.eclipse.ditto.connectivity.service.messaging; +import java.net.URI; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.connectivity.api.HonoConfig; +import org.eclipse.ditto.connectivity.service.config.HonoConfig; import org.eclipse.ditto.connectivity.model.Connection; -import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.HonoAddressAlias; import org.eclipse.ditto.connectivity.model.ImmutableHeaderMapping; @@ -36,7 +37,6 @@ import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonObjectBuilder; import org.eclipse.ditto.json.JsonValue; import com.typesafe.config.Config; @@ -54,7 +54,9 @@ public final class DefaultClientActorPropsFactory implements ClientActorPropsFac @Nullable private static DefaultClientActorPropsFactory instance; - private DefaultClientActorPropsFactory() {} + private DefaultClientActorPropsFactory() { + super(); + } /** * Returns an instance of {@code DefaultClientActorPropsFactory}. Creates a new one if not already done. @@ -69,7 +71,8 @@ public static DefaultClientActorPropsFactory getInstance() { } @Override - public Props getActorPropsForType(final Connection connection, final ActorRef proxyActor, + public Props getActorPropsForType(final Connection connection, + final ActorRef proxyActor, final ActorRef connectionActor, final ActorSystem actorSystem, final DittoHeaders dittoHeaders, @@ -102,41 +105,50 @@ public Props getActorPropsForType(final Connection connection, final ActorRef pr connectionActor, dittoHeaders, connectivityConfigOverwrites); - case HONO -> - KafkaClientActor.props(getEnrichedConnection(actorSystem, connection), - proxyActor, connectionActor, dittoHeaders, connectivityConfigOverwrites); + case HONO -> KafkaClientActor.props(getEnrichedConnection(actorSystem, connection), + proxyActor, + connectionActor, + dittoHeaders, + connectivityConfigOverwrites); }; } - private Connection getEnrichedConnection(final ActorSystem actorSystem, final Connection connection) { - var honoConfig = HonoConfig.get(actorSystem); - final ConnectionId connectionId = connection.getId(); - return ConnectivityModelFactory.newConnectionBuilder( - connection.getId(), + private static Connection getEnrichedConnection(final ActorSystem actorSystem, final Connection connection) { + final var honoConfig = HonoConfig.get(actorSystem); + final var connectionId = connection.getId(); + final var tenantId = honoConfig.getTenantId(connectionId); + return ConnectivityModelFactory.newConnectionBuilder(connection.getId(), connection.getConnectionType(), connection.getConnectionStatus(), honoConfig.getBaseUri().toString()) .validateCertificate(honoConfig.isValidateCertificates()) .specificConfig(Map.of( - "saslMechanism", honoConfig.getSaslMechanism().getValue(), - "bootstrapServers", honoConfig.getBootstrapServers(), - "groupId", honoConfig.getTenantId(connectionId) + "_" + connectionId)) - .credentials(honoConfig.getCredentials(connectionId)) + "saslMechanism", honoConfig.getSaslMechanism().toString(), + "bootstrapServers", getBootstrapServerUrisAsCommaSeparatedListString(honoConfig), + "groupId", tenantId + "_" + connectionId) + ) + .credentials(honoConfig.getUserPasswordCredentials(connectionId)) .sources(connection.getSources() .stream() .map(source -> ConnectivityModelFactory.sourceFromJson( - resolveSourceAliases(source, honoConfig.getTenantId(connectionId)), 1)) + resolveSourceAliases(source, tenantId), 1)) .toList()) .targets(connection.getTargets() .stream() - .map(target -> ConnectivityModelFactory.targetFromJson( - resolveTargetAlias(target, honoConfig.getTenantId(connectionId)))) + .map(target -> ConnectivityModelFactory.targetFromJson(resolveTargetAlias(target, tenantId))) .toList()) .build(); } - private JsonObject resolveSourceAliases(final Source source, String tenantId) { - JsonObjectBuilder sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) + private static String getBootstrapServerUrisAsCommaSeparatedListString(final HonoConfig honoConfig) { + return honoConfig.getBootstrapServerUris() + .stream() + .map(URI::toString) + .collect(Collectors.joining(",")); + } + + private static JsonObject resolveSourceAliases(final Source source, final String tenantId) { + final var sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() .map(address -> HonoAddressAlias.resolve(address, tenantId)) .map(JsonValue::of) @@ -163,17 +175,18 @@ private JsonObject resolveSourceAliases(final Source source, String tenantId) { return sourceBuilder.build(); } - private JsonObject resolveTargetAlias(final Target target, String tenantId) { - JsonObjectBuilder targetBuilder = JsonFactory.newObjectBuilder(target.toJson()) + private static JsonObject resolveTargetAlias(final Target target, final String tenantId) { + final var targetBuilder = JsonFactory.newObjectBuilder(target.toJson()) .set(Target.JsonFields.ADDRESS, Optional.of(target.getAddress()) .map(address -> HonoAddressAlias.resolve(address, tenantId, true)) .orElse(null), jsonField -> !jsonField.getValue().asString().isEmpty()); - var headerMapping = target.getHeaderMapping().toJson() + final var headerMapping = target.getHeaderMapping().toJson() .setValue("device_id", "{{ thing:id }}") .setValue("correlation-id", "{{ header:correlation-id }}") .setValue("subject", "{{ header:subject | fn:default(topic:action-subject) }}"); if (target.getTopics().stream() - .anyMatch(topic -> topic.getTopic() == Topic.LIVE_MESSAGES || topic.getTopic() == Topic.LIVE_COMMANDS)) { + .anyMatch(topic -> topic.getTopic() == Topic.LIVE_MESSAGES || + topic.getTopic() == Topic.LIVE_COMMANDS)) { headerMapping.setValue("response-required", "{{ header:response-required }}"); } targetBuilder.set("headerMapping", headerMapping); diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfigTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfigTest.java new file mode 100644 index 0000000000..614f3e62d9 --- /dev/null +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfigTest.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.service.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.eclipse.ditto.connectivity.model.ConnectionId.generateRandom; +import static org.mutabilitydetector.unittesting.AllowedReason.assumingFields; +import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; +import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; + +import java.net.URI; +import java.net.URISyntaxException; +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; +import org.eclipse.ditto.internal.utils.config.DittoConfigError; +import org.eclipse.ditto.internal.utils.config.WithConfigPath; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigFactory; + +import akka.actor.ActorSystem; +import akka.testkit.javadsl.TestKit; +import nl.jqno.equalsverifier.EqualsVerifier; +import scala.concurrent.duration.FiniteDuration; + +/** + * Unit test for {@link DefaultHonoConfig}. + */ +public final class DefaultHonoConfigTest { + + @Rule + public final TestName testName = new TestName(); + + private ActorSystem actorSystem; + + @After + public void after() { + if (null != actorSystem) { + TestKit.shutdownActorSystem(actorSystem, FiniteDuration.apply(5, TimeUnit.SECONDS), false); + } + } + + @Test + public void assertImmutability() { + assertInstancesOf(DefaultHonoConfig.class, + areImmutable(), + assumingFields("bootstrapServerUris").areSafelyCopiedUnmodifiableCollectionsWithImmutableElements()); + } + + @Test + public void testHashCodeAndEquals() { + EqualsVerifier.forClass(DefaultHonoConfig.class) + .usingGetClass() + .verify(); + } + + @Test + public void newInstanceWithNullActorSystemThrowsException() { + assertThatNullPointerException() + .isThrownBy(() -> new DefaultHonoConfig(null)) + .withMessage("The actorSystem must not be null!") + .withNoCause(); + } + + @Test + public void newInstanceThrowsDittoConfigErrorIfBaseUriSyntaxIsInvalid() { + final var configKey = getFullQualifiedConfigKey(HonoConfig.HonoConfigValue.BASE_URI); + final var invalidUri = "192.168.1.256:80"; + + assertThatExceptionOfType(DittoConfigError.class) + .isThrownBy(() -> new DefaultHonoConfig(getActorSystem( + ConfigFactory.parseMap(Map.of(configKey, invalidUri)) + ))) + .withMessageStartingWith("The string value at <%s> is not a URI:", + HonoConfig.HonoConfigValue.BASE_URI.getConfigPath()) + .withMessageEndingWith(invalidUri) + .withCauseInstanceOf(URISyntaxException.class); + } + + @Test + public void getBaseUriReturnsDefaultValueIfNotContainedInConfig() { + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.empty())); + + assertThat(defaultHonoConfig.getBaseUri()) + .isEqualTo(URI.create(HonoConfig.HonoConfigValue.BASE_URI.getDefaultValue().toString())); + } + + @Test + public void getBaseUriReturnsExplicitlyConfiguredValue() { + final var baseUri = URI.create("example.org:8080"); + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.parseMap( + Map.of(getFullQualifiedConfigKey(HonoConfig.HonoConfigValue.BASE_URI), baseUri.toString()) + ))); + + assertThat(defaultHonoConfig.getBaseUri()).isEqualTo(baseUri); + } + + @Test + public void isValidateCertificatesReturnsDefaultValueIfNotContainedInConfig() { + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.empty())); + + assertThat(defaultHonoConfig.isValidateCertificates()) + .isEqualTo(HonoConfig.HonoConfigValue.VALIDATE_CERTIFICATES.getDefaultValue()); + } + + @Test + public void isValidateCertificatesReturnsExplicitlyConfiguredValue() { + final var validateCertificates = true; + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.parseMap( + Map.of(getFullQualifiedConfigKey(HonoConfig.HonoConfigValue.VALIDATE_CERTIFICATES), + validateCertificates) + ))); + + assertThat(defaultHonoConfig.isValidateCertificates()).isEqualTo(validateCertificates); + } + + @Test + public void newInstanceThrowsDittoConfigErrorIfSaslMechanismIsUnknown() { + assertThatExceptionOfType(DittoConfigError.class) + .isThrownBy(() -> new DefaultHonoConfig(getActorSystem(ConfigFactory.parseMap( + Map.of(getFullQualifiedConfigKey(HonoConfig.HonoConfigValue.SASL_MECHANISM), 42) + )))) + .withCauseInstanceOf(ConfigException.BadValue.class); + } + + @Test + public void getSaslMechanismReturnsDefaultValueIfNotContainedInConfig() { + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.empty())); + + assertThat(defaultHonoConfig.getSaslMechanism()) + .isEqualTo(HonoConfig.SaslMechanism.valueOf( + HonoConfig.HonoConfigValue.SASL_MECHANISM.getDefaultValue().toString() + )); + } + + @Test + public void newInstanceThrowsDittoConfigErrorIfOneBootstrapServerUriSyntaxIsInvalid() { + final var configKey = getFullQualifiedConfigKey(HonoConfig.HonoConfigValue.BOOTSTRAP_SERVERS); + final var invalidUri = "192.168.1.256:80"; + final var bootstrapServerUrisString = "example.com," + invalidUri + ",10.1.0.35:8080"; + + assertThatExceptionOfType(DittoConfigError.class) + .isThrownBy(() -> new DefaultHonoConfig(getActorSystem( + ConfigFactory.parseMap(Map.of(configKey, bootstrapServerUrisString)) + ))) + .withMessageStartingWith("The string at index <1> for key <%s> is not a valid URI:", + HonoConfig.HonoConfigValue.BOOTSTRAP_SERVERS.getConfigPath()) + .withMessageEndingWith(invalidUri) + .withCauseInstanceOf(URISyntaxException.class); + } + + @Test + public void getBootstrapServerUrisReturnsDefaultValueIfNotContainedInConfig() { + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.empty())); + + assertThat(defaultHonoConfig.getBootstrapServerUris()) + .containsOnly(URI.create(HonoConfig.HonoConfigValue.BOOTSTRAP_SERVERS.getDefaultValue().toString())); + } + + @Test + public void getBootstrapServerUrisReturnsExplicitlyConfiguredValues() { + final var bootstrapServerUris = List.of(URI.create("www.example.org"), + URI.create("tcp://192.168.10.1:8080"), + URI.create("file://bin/server")); + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.parseMap(Map.of( + getFullQualifiedConfigKey(HonoConfig.HonoConfigValue.BOOTSTRAP_SERVERS), + bootstrapServerUris.stream() + .map(URI::toString) + .map(s -> s.concat(" ")) // blanks should get trimmed + .collect(Collectors.joining(",")) + )))); + + assertThat(defaultHonoConfig.getBootstrapServerUris()).containsExactlyElementsOf(bootstrapServerUris); + } + + @Test + public void getCredentialsReturnsDefaultValueIfNotContainedInConfig() { + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.empty())); + + assertThat(defaultHonoConfig.getUserPasswordCredentials(generateRandom())) + .isEqualTo(UserPasswordCredentials.newInstance("", "")); + } + + @Test + public void getCredentialsReturnsExplicitlyConfiguredValues() { + final var username = "Herbert W. Franke"; + final var password = "PeterParsival"; + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.parseMap(Map.of( + getFullQualifiedConfigKey(HonoConfig.HonoConfigValue.USERNAME), username, + getFullQualifiedConfigKey(HonoConfig.HonoConfigValue.PASSWORD), password + )))); + + assertThat(defaultHonoConfig.getUserPasswordCredentials(null)) + .isEqualTo(UserPasswordCredentials.newInstance(username, password)); + } + + @Test + public void getTenantIdReturnsEmptyString() { + final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.empty())); + + assertThat(defaultHonoConfig.getTenantId(null)).isEmpty(); + assertThat(defaultHonoConfig.getTenantId(generateRandom())).isEmpty(); + } + + private static String getFullQualifiedConfigKey(final WithConfigPath withConfigPath) { + return MessageFormat.format("{0}.{1}", HonoConfig.PREFIX, withConfigPath.getConfigPath()); + } + + private ActorSystem getActorSystem(final Config config) { + actorSystem = ActorSystem.create(testName.getMethodName(), config); + return actorSystem; + } + +} \ No newline at end of file From 710f8184f5faf5631e5b9b940c26514c6307c550 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 20 Jul 2022 15:45:00 +0300 Subject: [PATCH 21/65] Connection enrichment refactored, HonoConfig cleaned up. Connection enrichment extracted in a new class DefaultHonoConnectionFactory HonoConfig cleaned up from the method getTenantId() and the parameter of getCredentials() HonoConfig simplified - extension mechanism removed. Signed-off-by: Andrey Balarev --- .../connectivity/model/HonoAddressAlias.java | 3 +- .../service/config/DefaultHonoConfig.java | 4 +- .../service/config/HonoConfig.java | 58 +-------- .../DefaultClientActorPropsFactory.java | 102 +-------------- .../hono/DefaultHonoConnectionFactory.java | 27 ++++ .../messaging/hono/HonoConnectionFactory.java | 117 ++++++++++++++++++ .../src/main/resources/connectivity.conf | 3 - .../service/config/DefaultHonoConfigTest.java | 13 +- 8 files changed, 157 insertions(+), 170 deletions(-) create mode 100644 connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java create mode 100644 connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 7650932d38..7e21c69a31 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -102,8 +102,9 @@ public static Optional fromName(String name) { * @return the resolved alias or empty if not an alias */ public static String resolve(String alias, String tenantId, boolean thingSuffix) { + String suffix = thingSuffix ? "/{{thing:id}}" : ""; return fromName(alias) - .map(found -> "hono." + found.getName() + "." + tenantId + (thingSuffix ? "/{{thing:id}}" : "")) + .map(found -> "hono." + found.getName() + (tenantId.isEmpty() ? "" : "." + tenantId) + suffix) .orElse(alias); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java index 881492e2c5..fe42bc5402 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfig.java @@ -25,7 +25,6 @@ import javax.annotation.concurrent.Immutable; -import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.internal.utils.config.ConfigWithFallback; import org.eclipse.ditto.internal.utils.config.DittoConfigError; @@ -47,6 +46,7 @@ public final class DefaultHonoConfig implements HonoConfig { private final Set bootstrapServerUris; private final UserPasswordCredentials credentials; + /** * Constructs a {@code DefaultHonoConfig} for the specified ActorSystem. * @@ -131,7 +131,7 @@ public Set getBootstrapServerUris() { } @Override - public UserPasswordCredentials getUserPasswordCredentials(final ConnectionId connectionId) { + public UserPasswordCredentials getUserPasswordCredentials() { return credentials; } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java index 31f90afe3d..3818ab97be 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java @@ -14,19 +14,12 @@ import java.net.URI; import java.net.URISyntaxException; -import java.util.List; import java.util.Set; -import org.eclipse.ditto.base.model.common.ConditionChecker; -import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; -import org.eclipse.ditto.internal.utils.akka.AkkaClassLoader; import org.eclipse.ditto.internal.utils.config.DittoConfigError; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; -import akka.actor.AbstractExtensionId; -import akka.actor.ActorSystem; -import akka.actor.ExtendedActorSystem; import akka.actor.Extension; /** @@ -71,24 +64,10 @@ public interface HonoConfig extends Extension { /** * Gets the credentials for the specified Hono connection. * - * @param connectionId the ID of the connection. * @return the credentials of the connection. * @throws NullPointerException if {@code connectionId} is {@code null}. */ - // TODO jff delete connection ID parameter because for Ditto it does not make sense. - UserPasswordCredentials getUserPasswordCredentials(ConnectionId connectionId); - - /** - * Gets the Hub tenant ID property for the specified Hono connection. - * - * @param connectionId the ID of the connection. - * @return the Hub tenant ID, {@code ""} by default. - * @throws NullPointerException if {@code connectionId} is {@code null}. - */ - // TODO jff delete method after obtaining the tenant ID is moved to another place. - default String getTenantId(final ConnectionId connectionId) { - return ""; - } + UserPasswordCredentials getUserPasswordCredentials(); enum HonoConfigValue implements KnownConfigValue { @@ -125,7 +104,7 @@ enum HonoConfigValue implements KnownConfigValue { private final String path; private final Object defaultValue; - private HonoConfigValue(final String thePath, final Object theDefaultValue) { + HonoConfigValue(final String thePath, final Object theDefaultValue) { path = thePath; defaultValue = theDefaultValue; } @@ -142,16 +121,6 @@ public String getConfigPath() { } - /** - * Load the {@code HonoConfig} extension. - * - * @param actorSystem The actor system in which to load the configuration. - * @return the {@code HonoConfig}. - */ - static HonoConfig get(final ActorSystem actorSystem) { - return HonoConfig.ExtensionId.INSTANCE.get(ConditionChecker.checkNotNull(actorSystem, "actorSystem")); - } - /** * Validates and gets URI from a string * @@ -167,34 +136,13 @@ static URI getUri(final String uri) throws DittoConfigError { } } - /** - * ID of the actor system extension to provide a Hono-connections configuration. - */ - final class ExtensionId extends AbstractExtensionId { - - private static final String CONFIG_PATH = PREFIX + ".config-provider"; - - private static final ExtensionId INSTANCE = new ExtensionId(); - - @Override - public HonoConfig createExtension(final ExtendedActorSystem system) { - ConditionChecker.checkNotNull(system, "system"); - return AkkaClassLoader.instantiate(system, - HonoConfig.class, - system.settings().config().getString(CONFIG_PATH), - List.of(ActorSystem.class), - List.of(system)); - } - - } - enum SaslMechanism { PLAIN("plain"); private final String value; - private SaslMechanism(final String value) { + SaslMechanism(final String value) { this.value = value; } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index b1c3d1dbbe..21abd71312 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -12,32 +12,17 @@ */ package org.eclipse.ditto.connectivity.service.messaging; -import java.net.URI; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.connectivity.service.config.HonoConfig; import org.eclipse.ditto.connectivity.model.Connection; -import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; -import org.eclipse.ditto.connectivity.model.HonoAddressAlias; -import org.eclipse.ditto.connectivity.model.ImmutableHeaderMapping; -import org.eclipse.ditto.connectivity.model.Source; -import org.eclipse.ditto.connectivity.model.Target; -import org.eclipse.ditto.connectivity.model.Topic; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpClientActor; +import org.eclipse.ditto.connectivity.service.messaging.hono.DefaultHonoConnectionFactory; import org.eclipse.ditto.connectivity.service.messaging.httppush.HttpPushClientActor; import org.eclipse.ditto.connectivity.service.messaging.kafka.KafkaClientActor; import org.eclipse.ditto.connectivity.service.messaging.mqtt.hivemq.MqttClientActor; import org.eclipse.ditto.connectivity.service.messaging.rabbitmq.RabbitMQClientActor; -import org.eclipse.ditto.json.JsonArray; -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonValue; import com.typesafe.config.Config; @@ -54,7 +39,7 @@ public final class DefaultClientActorPropsFactory implements ClientActorPropsFac @Nullable private static DefaultClientActorPropsFactory instance; - private DefaultClientActorPropsFactory() { + public DefaultClientActorPropsFactory() { super(); } @@ -105,7 +90,8 @@ public Props getActorPropsForType(final Connection connection, connectionActor, dittoHeaders, connectivityConfigOverwrites); - case HONO -> KafkaClientActor.props(getEnrichedConnection(actorSystem, connection), + case HONO -> KafkaClientActor.props( + DefaultHonoConnectionFactory.getEnrichedConnection(actorSystem, connection), proxyActor, connectionActor, dittoHeaders, @@ -113,84 +99,4 @@ public Props getActorPropsForType(final Connection connection, }; } - private static Connection getEnrichedConnection(final ActorSystem actorSystem, final Connection connection) { - final var honoConfig = HonoConfig.get(actorSystem); - final var connectionId = connection.getId(); - final var tenantId = honoConfig.getTenantId(connectionId); - return ConnectivityModelFactory.newConnectionBuilder(connection.getId(), - connection.getConnectionType(), - connection.getConnectionStatus(), - honoConfig.getBaseUri().toString()) - .validateCertificate(honoConfig.isValidateCertificates()) - .specificConfig(Map.of( - "saslMechanism", honoConfig.getSaslMechanism().toString(), - "bootstrapServers", getBootstrapServerUrisAsCommaSeparatedListString(honoConfig), - "groupId", tenantId + "_" + connectionId) - ) - .credentials(honoConfig.getUserPasswordCredentials(connectionId)) - .sources(connection.getSources() - .stream() - .map(source -> ConnectivityModelFactory.sourceFromJson( - resolveSourceAliases(source, tenantId), 1)) - .toList()) - .targets(connection.getTargets() - .stream() - .map(target -> ConnectivityModelFactory.targetFromJson(resolveTargetAlias(target, tenantId))) - .toList()) - .build(); - } - - private static String getBootstrapServerUrisAsCommaSeparatedListString(final HonoConfig honoConfig) { - return honoConfig.getBootstrapServerUris() - .stream() - .map(URI::toString) - .collect(Collectors.joining(",")); - } - - private static JsonObject resolveSourceAliases(final Source source, final String tenantId) { - final var sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) - .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() - .map(address -> HonoAddressAlias.resolve(address, tenantId)) - .map(JsonValue::of) - .toList())); - source.getReplyTarget().ifPresent(replyTarget -> { - var headerMapping = replyTarget.getHeaderMapping().toJson() - .setValue("correlation-id", "{{ header:correlation-id }}"); - if (HonoAddressAlias.COMMAND.getName().equals(replyTarget.getAddress())) { - headerMapping = headerMapping - .setValue("device_id", "{{ thing:id }}") - .setValue("subject", - "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); - } - sourceBuilder.set("replyTarget", replyTarget.toBuilder() - .address(HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)) - .headerMapping(ImmutableHeaderMapping.fromJson(headerMapping)) - .build().toJson()); - }); - if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getName())) { - sourceBuilder.set("headerMapping", source.getHeaderMapping().toJson() - .setValue("correlation-id", "{{ header:correlation-id }}") - .setValue("status", "{{ header:status }}")); - } - return sourceBuilder.build(); - } - - private static JsonObject resolveTargetAlias(final Target target, final String tenantId) { - final var targetBuilder = JsonFactory.newObjectBuilder(target.toJson()) - .set(Target.JsonFields.ADDRESS, Optional.of(target.getAddress()) - .map(address -> HonoAddressAlias.resolve(address, tenantId, true)) - .orElse(null), jsonField -> !jsonField.getValue().asString().isEmpty()); - final var headerMapping = target.getHeaderMapping().toJson() - .setValue("device_id", "{{ thing:id }}") - .setValue("correlation-id", "{{ header:correlation-id }}") - .setValue("subject", "{{ header:subject | fn:default(topic:action-subject) }}"); - if (target.getTopics().stream() - .anyMatch(topic -> topic.getTopic() == Topic.LIVE_MESSAGES || - topic.getTopic() == Topic.LIVE_COMMANDS)) { - headerMapping.setValue("response-required", "{{ header:response-required }}"); - } - targetBuilder.set("headerMapping", headerMapping); - return targetBuilder.build(); - } - } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java new file mode 100644 index 0000000000..8c816501ba --- /dev/null +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java @@ -0,0 +1,27 @@ +package org.eclipse.ditto.connectivity.service.messaging.hono; + +import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; + +import akka.actor.ActorSystem; + +public class DefaultHonoConnectionFactory extends HonoConnectionFactory { + + private DefaultHonoConnectionFactory(ActorSystem actorSystem, Connection connection) { + super(actorSystem, connection); + } + + @Override + public UserPasswordCredentials getCredentials() { + return honoConfig.getUserPasswordCredentials(); + } + + @Override + protected String getTenantId() { + return ""; + } + + public static Connection getEnrichedConnection(ActorSystem actorSystem, Connection connection) { + return new DefaultHonoConnectionFactory(actorSystem, connection).enrichConnection(); + } +} diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java new file mode 100644 index 0000000000..d001ce34b6 --- /dev/null +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -0,0 +1,117 @@ +package org.eclipse.ditto.connectivity.service.messaging.hono; + +import java.net.URI; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; +import org.eclipse.ditto.connectivity.model.HonoAddressAlias; +import org.eclipse.ditto.connectivity.model.ImmutableHeaderMapping; +import org.eclipse.ditto.connectivity.model.Source; +import org.eclipse.ditto.connectivity.model.Target; +import org.eclipse.ditto.connectivity.model.Topic; +import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; +import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; +import org.eclipse.ditto.connectivity.service.config.HonoConfig; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonValue; + +import akka.actor.ActorSystem; + +public abstract class HonoConnectionFactory { + protected final ActorSystem actorSystem; + protected final Connection connection; + protected final HonoConfig honoConfig; + + protected HonoConnectionFactory(ActorSystem actorSystem, Connection connection) { + this.actorSystem = actorSystem; + this.connection = connection; + this.honoConfig = new DefaultHonoConfig(actorSystem); + } + + protected abstract UserPasswordCredentials getCredentials(); + protected abstract String getTenantId(); + + public Connection enrichConnection() { + final var connectionId = connection.getId(); + return ConnectivityModelFactory.newConnectionBuilder(connection.getId(), + connection.getConnectionType(), + connection.getConnectionStatus(), + honoConfig.getBaseUri().toString()) + .validateCertificate(honoConfig.isValidateCertificates()) + .specificConfig(Map.of( + "saslMechanism", honoConfig.getSaslMechanism().toString(), + "bootstrapServers", getBootstrapServerUrisAsCommaSeparatedListString(honoConfig), + "groupId", (getTenantId().isEmpty() ? "" : getTenantId() + "_") + connectionId) + ) + .credentials(getCredentials()) + .sources(connection.getSources() + .stream() + .map(source -> ConnectivityModelFactory.sourceFromJson( + resolveSourceAliases(source, getTenantId()), 1)) + .toList()) + .targets(connection.getTargets() + .stream() + .map(target -> ConnectivityModelFactory.targetFromJson(resolveTargetAlias(target, getTenantId()))) + .toList()) + .build(); + } + + private static String getBootstrapServerUrisAsCommaSeparatedListString(final HonoConfig honoConfig) { + return honoConfig.getBootstrapServerUris() + .stream() + .map(URI::toString) + .collect(Collectors.joining(",")); + } + + private static JsonObject resolveSourceAliases(final Source source, final String tenantId) { + final var sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) + .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() + .map(address -> HonoAddressAlias.resolve(address, tenantId)) + .map(JsonValue::of) + .toList())); + source.getReplyTarget().ifPresent(replyTarget -> { + var headerMapping = replyTarget.getHeaderMapping().toJson() + .setValue("correlation-id", "{{ header:correlation-id }}"); + if (HonoAddressAlias.COMMAND.getName().equals(replyTarget.getAddress())) { + headerMapping = headerMapping + .setValue("device_id", "{{ thing:id }}") + .setValue("subject", + "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); + } + sourceBuilder.set("replyTarget", replyTarget.toBuilder() + .address(HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)) + .headerMapping(ImmutableHeaderMapping.fromJson(headerMapping)) + .build().toJson()); + }); + if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getName())) { + sourceBuilder.set("headerMapping", source.getHeaderMapping().toJson() + .setValue("correlation-id", "{{ header:correlation-id }}") + .setValue("status", "{{ header:status }}")); + } + return sourceBuilder.build(); + } + + private static JsonObject resolveTargetAlias(final Target target, final String tenantId) { + final var targetBuilder = JsonFactory.newObjectBuilder(target.toJson()) + .set(Target.JsonFields.ADDRESS, Optional.of(target.getAddress()) + .map(address -> HonoAddressAlias.resolve(address, tenantId, true)) + .orElse(null), jsonField -> !jsonField.getValue().asString().isEmpty()); + var headerMapping = target.getHeaderMapping().toJson() + .setValue("device_id", "{{ thing:id }}") + .setValue("correlation-id", "{{ header:correlation-id }}") + .setValue("subject", "{{ header:subject | fn:default(topic:action-subject) }}"); + if (target.getTopics().stream() + .anyMatch(topic -> topic.getTopic() == Topic.LIVE_MESSAGES || + topic.getTopic() == Topic.LIVE_COMMANDS)) { + headerMapping = headerMapping.setValue("response-required", "{{ header:response-required }}"); + } + targetBuilder.set("headerMapping", headerMapping); + return targetBuilder.build(); + } + +} diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index 6905c9bccc..9f1c9535dd 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -19,9 +19,6 @@ ditto { connectivity { hono { - config-provider = "org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig", - config-provider = ${?HONO_CONNECTION_CONFIG_PROVIDER} - base-uri = "tcp://localhost:30092" base-uri = ${?HONO_CONNECTION_URI} diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfigTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfigTest.java index 614f3e62d9..374a1b1fca 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfigTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/config/DefaultHonoConfigTest.java @@ -15,7 +15,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNullPointerException; -import static org.eclipse.ditto.connectivity.model.ConnectionId.generateRandom; import static org.mutabilitydetector.unittesting.AllowedReason.assumingFields; import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; @@ -199,7 +198,7 @@ public void getBootstrapServerUrisReturnsExplicitlyConfiguredValues() { public void getCredentialsReturnsDefaultValueIfNotContainedInConfig() { final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.empty())); - assertThat(defaultHonoConfig.getUserPasswordCredentials(generateRandom())) + assertThat(defaultHonoConfig.getUserPasswordCredentials()) .isEqualTo(UserPasswordCredentials.newInstance("", "")); } @@ -212,18 +211,10 @@ public void getCredentialsReturnsExplicitlyConfiguredValues() { getFullQualifiedConfigKey(HonoConfig.HonoConfigValue.PASSWORD), password )))); - assertThat(defaultHonoConfig.getUserPasswordCredentials(null)) + assertThat(defaultHonoConfig.getUserPasswordCredentials()) .isEqualTo(UserPasswordCredentials.newInstance(username, password)); } - @Test - public void getTenantIdReturnsEmptyString() { - final var defaultHonoConfig = new DefaultHonoConfig(getActorSystem(ConfigFactory.empty())); - - assertThat(defaultHonoConfig.getTenantId(null)).isEmpty(); - assertThat(defaultHonoConfig.getTenantId(generateRandom())).isEmpty(); - } - private static String getFullQualifiedConfigKey(final WithConfigPath withConfigPath) { return MessageFormat.format("{0}.{1}", HonoConfig.PREFIX, withConfigPath.getConfigPath()); } From c59a028a0c458e043fdb71b1c0ce97b9df51e6d5 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Thu, 21 Jul 2022 19:47:43 +0200 Subject: [PATCH 22/65] Refactored `HonoAddressAlias` and added unit tests. * Renamed method `getName` to `getAliasValue` to distinct it more clearly from an enum's inherent `name` method. * Converted methods for resolving addresses from static methods to instance methods. This makes them less error-prone because the alias value does not have to be provided. Furthermore, got rid of the boolean parameter in favour of a more expressive method name. Signed-off-by: Juergen Fickel --- .../connectivity/model/HonoAddressAlias.java | 119 +++++++++--------- .../model/HonoAddressAliasTest.java | 110 ++++++++++++++++ .../messaging/hono/HonoConnectionFactory.java | 68 ++++++---- .../service/messaging/hono/HonoValidator.java | 12 +- 4 files changed, 225 insertions(+), 84 deletions(-) create mode 100644 connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 7e21c69a31..99dfbfe16a 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -10,113 +10,118 @@ * * SPDX-License-Identifier: EPL-2.0 */ - package org.eclipse.ditto.connectivity.model; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.Locale; +import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.common.ConditionChecker; /** * Possible address aliases used by connections of type 'Hono' */ public enum HonoAddressAlias { + /** - * telemetry address alias + * telemetry address alias. */ TELEMETRY("telemetry"), /** - * event address alias + * event address alias. */ EVENT("event"), /** - * command&control address alias + * command&control address alias. */ COMMAND("command"), /** - * command response address alias + * command response address alias. */ COMMAND_RESPONSE("command_response"); - private final String name; - - private static final Map HONO_ADDRESS_ALIAS_MAP; + private final String value; - static { - Map map = new ConcurrentHashMap<>(); - for (HonoAddressAlias alias : HonoAddressAlias.values()) { - map.put(alias.getName(), alias); - } - HONO_ADDRESS_ALIAS_MAP = Collections.unmodifiableMap(map); - } - - HonoAddressAlias(String name) { - this.name = name; + private HonoAddressAlias(final String value) { + this.value = value; } /** - * Gets the name of the alias + * Returns all defined HonoAddressAlias values. * - * @return The name of the alias + * @return a stream with HonoAddressAlias values. */ - public String getName() { - return name; + public static Stream aliasValues() { + return Stream.of(values()).map(HonoAddressAlias::getAliasValue); } /** - * Returns all defined HonoAddressAlias names + * Returns the HonoAddressAlias to which the given alias value is mapped. + * This method is fault-tolerant for its parameter to some degree: + *
    + *
  • it accepts {@code null},
  • + *
  • it trims white spaces and
  • + *
  • it converts the specified string to lower case.
  • + *
* - * @return A list with HonoAddressAlias names + * @param aliasValue the aliasValue of the supposed HonoAddressAlias. + * @return an Optional containing the HonoAddressAlias which matches {@code aliasValue} or an empty Optional if none + * matches. */ - public static List names() { - return new ArrayList<>(HONO_ADDRESS_ALIAS_MAP.keySet()); + public static Optional forAliasValue(@Nullable final String aliasValue) { + return Stream.of(values()) + .filter(alias -> null != aliasValue && + Objects.equals(alias.getAliasValue(), aliasValue.trim().toLowerCase(Locale.ENGLISH))) + .findAny(); } /** - * Returns the HonoAddressAlias to which the given name is mapped + * Gets the value of the alias. * - * @param name of HonoAddressAlias - * @return the HonoAddressAlias to which the given name is mapped + * @return the value of the alias. */ - public static Optional fromName(String name) { - try { - return Optional.of(HONO_ADDRESS_ALIAS_MAP.get(name)); - } catch (NullPointerException | ClassCastException ex) { - return Optional.empty(); - } + public String getAliasValue() { + return value; } /** - * Resolves the input as a potential address alias or returns empty string if not an existing alias. + * Resolves the source or target address of this address alias for the specified tenant ID. * - * @param alias the alias name to resolve - * @param tenantId the tenantId - used in the resolve pattern - * @param thingSuffix if true, adds '/{{thing:id}}' suffix on resolve - needed for replyTarget addresses - * if false - does not add suffix - * @return the resolved alias or empty if not an alias + * @param tenantId the tenant ID to resolve the address of this alias for. + * @return the resolved address of this address alias for {@code tenantId}. + * @throws NullPointerException if {@code tenantId} is {@code null}. */ - public static String resolve(String alias, String tenantId, boolean thingSuffix) { - String suffix = thingSuffix ? "/{{thing:id}}" : ""; - return fromName(alias) - .map(found -> "hono." + found.getName() + (tenantId.isEmpty() ? "" : "." + tenantId) + suffix) - .orElse(alias); + public String resolveAddress(final CharSequence tenantId) { + ConditionChecker.checkNotNull(tenantId, "tenantId"); + + final String prefix = "hono."; + final String aliasValue = getAliasValue(); + final int tenantIdLength = tenantId.length(); + final StringBuilder sb = new StringBuilder(prefix.length() + aliasValue.length() + 1 + tenantIdLength); + sb.append(prefix).append(aliasValue); + if (0 < tenantIdLength) { + sb.append(".").append(tenantId); + } + return sb.toString(); } /** - * Resolves the input as a potential address alias or returns empty string if not an existing aliass. + * Resolves the source or target address of this address alias for the specified tenant ID and appends a suffix + * for thing ID. + * This is mainly needed for reply target addresses. * - * @param alias the alias name to resolve - * @param tenantId the tenantId - used in the resolve pattern - * @return the resolved alias or empty if not an alias + * @param tenantId the tenant ID to resolve the address of this alias for. + * @return the resolved address of this address alias for {@code tenantId} with {@code "/{{thing:id}}"} appended. + * @throws NullPointerException if {@code tenantId} is {@code null}. */ - public static String resolve(String alias, String tenantId) throws IllegalArgumentException { - return resolve(alias, tenantId, false); + public String resolveAddressWithThingIdSuffix(final CharSequence tenantId) { + return resolveAddress(tenantId) + "/{{thing:id}}"; } } diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java new file mode 100644 index 0000000000..bb399b70ea --- /dev/null +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.eclipse.ditto.connectivity.model.HonoAddressAlias.COMMAND_RESPONSE; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.UUID; + +import org.assertj.core.api.AutoCloseableSoftAssertions; +import org.junit.Test; + +/** + * Unit test for {@link HonoAddressAlias}. + */ +public final class HonoAddressAliasTest { + + @Test + public void aliasValuesReturnsExpected() { + final HonoAddressAlias[] honoAddressAliases = HonoAddressAlias.values(); + final Collection expectedAliasValues = new LinkedHashSet<>(honoAddressAliases.length); + for (final HonoAddressAlias honoAddressAlias : honoAddressAliases) { + expectedAliasValues.add(honoAddressAlias.getAliasValue()); + } + assertThat(HonoAddressAlias.aliasValues()).hasSameElementsAs(expectedAliasValues); + } + + @Test + public void forAliasValueWithNullAliasValueReturnsEmptyOptional() { + assertThat(HonoAddressAlias.forAliasValue(null)).isEmpty(); + } + + @Test + public void forAliasValueWithUnknownAliasValueReturnsEmptyOptional() { + assertThat(HonoAddressAlias.forAliasValue(String.valueOf(UUID.randomUUID()))).isEmpty(); + } + + @Test + public void forAliasValueIsTolerantForSmallDiscrepancyInSpecifiedAliasValue() { + try (final AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + softly.assertThat(HonoAddressAlias.forAliasValue(" TeLeMeTrY ")).hasValue(HonoAddressAlias.TELEMETRY); + softly.assertThat(HonoAddressAlias.forAliasValue(COMMAND_RESPONSE.name())) + .as(COMMAND_RESPONSE.name()) + .hasValue(COMMAND_RESPONSE); + } + } + + @Test + public void forAliasValueReturnsExpectedForKnownAliasValue() { + try (final AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { + for (final HonoAddressAlias honoAddressAlias : HonoAddressAlias.values()) { + softly.assertThat(HonoAddressAlias.forAliasValue(honoAddressAlias.getAliasValue())) + .as(honoAddressAlias.getAliasValue()) + .hasValue(honoAddressAlias); + } + } + } + + @Test + public void resolveAddressWithNullTenantIdThrowsException() { + assertThatNullPointerException() + .isThrownBy(() -> HonoAddressAlias.EVENT.resolveAddress(null)) + .withMessage("The tenantId must not be null!") + .withNoCause(); + } + + @Test + public void resolveAddressWithNonEmptyTenantIdReturnsExpected() { + final HonoAddressAlias honoAddressAlias = HonoAddressAlias.EVENT; + final String tenantId = "myTenant"; + + final String resolvedAddress = honoAddressAlias.resolveAddress(tenantId); + + assertThat(resolvedAddress).isEqualTo("hono." + honoAddressAlias.getAliasValue() + "." + tenantId); + } + + @Test + public void resolveAddressWithEmptyTenantIdReturnsExpected() { + final HonoAddressAlias honoAddressAlias = HonoAddressAlias.EVENT; + + final String resolvedAddress = honoAddressAlias.resolveAddress(""); + + assertThat(resolvedAddress).isEqualTo("hono." + honoAddressAlias.getAliasValue()); + } + + @Test + public void resolveAddressWithThingIdSuffixReturnsExpected() { + final HonoAddressAlias honoAddressAlias = HonoAddressAlias.EVENT; + final String tenantId = "myTenant"; + + final String resolvedAddress = honoAddressAlias.resolveAddressWithThingIdSuffix(tenantId); + + assertThat(resolvedAddress) + .isEqualTo("hono." + honoAddressAlias.getAliasValue() + "." + tenantId + "/{{thing:id}}"); + } + +} \ No newline at end of file diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index d001ce34b6..258b796cfb 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -1,9 +1,22 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ package org.eclipse.ditto.connectivity.service.messaging.hono; import java.net.URI; import java.util.Map; -import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; @@ -15,7 +28,7 @@ import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; import org.eclipse.ditto.connectivity.service.config.HonoConfig; -import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonCollectors; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonValue; @@ -23,21 +36,21 @@ import akka.actor.ActorSystem; public abstract class HonoConnectionFactory { - protected final ActorSystem actorSystem; + protected final Connection connection; protected final HonoConfig honoConfig; - protected HonoConnectionFactory(ActorSystem actorSystem, Connection connection) { - this.actorSystem = actorSystem; + protected HonoConnectionFactory(final ActorSystem actorSystem, final Connection connection) { this.connection = connection; - this.honoConfig = new DefaultHonoConfig(actorSystem); + honoConfig = new DefaultHonoConfig(actorSystem); } protected abstract UserPasswordCredentials getCredentials(); + protected abstract String getTenantId(); public Connection enrichConnection() { - final var connectionId = connection.getId(); + final var tenantId = getTenantId(); return ConnectivityModelFactory.newConnectionBuilder(connection.getId(), connection.getConnectionType(), connection.getConnectionStatus(), @@ -46,17 +59,17 @@ public Connection enrichConnection() { .specificConfig(Map.of( "saslMechanism", honoConfig.getSaslMechanism().toString(), "bootstrapServers", getBootstrapServerUrisAsCommaSeparatedListString(honoConfig), - "groupId", (getTenantId().isEmpty() ? "" : getTenantId() + "_") + connectionId) + "groupId", (tenantId.isEmpty() ? "" : tenantId + "_") + connection.getId()) ) .credentials(getCredentials()) .sources(connection.getSources() .stream() .map(source -> ConnectivityModelFactory.sourceFromJson( - resolveSourceAliases(source, getTenantId()), 1)) + resolveSourceAliases(source, tenantId), 1)) .toList()) .targets(connection.getTargets() .stream() - .map(target -> ConnectivityModelFactory.targetFromJson(resolveTargetAlias(target, getTenantId()))) + .map(target -> ConnectivityModelFactory.targetFromJson(resolveTargetAlias(target, tenantId))) .toList()) .build(); } @@ -68,27 +81,26 @@ private static String getBootstrapServerUrisAsCommaSeparatedListString(final Hon .collect(Collectors.joining(",")); } - private static JsonObject resolveSourceAliases(final Source source, final String tenantId) { + private static JsonObject resolveSourceAliases(final Source source, final CharSequence tenantId) { final var sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) - .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() - .map(address -> HonoAddressAlias.resolve(address, tenantId)) + .set(Source.JsonFields.ADDRESSES, resolveSourceAddresses(source.getAddresses(), tenantId) .map(JsonValue::of) - .toList())); + .collect(JsonCollectors.valuesToArray())); source.getReplyTarget().ifPresent(replyTarget -> { var headerMapping = replyTarget.getHeaderMapping().toJson() .setValue("correlation-id", "{{ header:correlation-id }}"); - if (HonoAddressAlias.COMMAND.getName().equals(replyTarget.getAddress())) { + if (HonoAddressAlias.COMMAND.getAliasValue().equals(replyTarget.getAddress())) { headerMapping = headerMapping .setValue("device_id", "{{ thing:id }}") .setValue("subject", "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); } sourceBuilder.set("replyTarget", replyTarget.toBuilder() - .address(HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)) + .address(resolveTargetAddress(replyTarget.getAddress(), tenantId)) .headerMapping(ImmutableHeaderMapping.fromJson(headerMapping)) .build().toJson()); }); - if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getName())) { + if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getAliasValue())) { sourceBuilder.set("headerMapping", source.getHeaderMapping().toJson() .setValue("correlation-id", "{{ header:correlation-id }}") .setValue("status", "{{ header:status }}")); @@ -96,11 +108,25 @@ private static JsonObject resolveSourceAliases(final Source source, final String return sourceBuilder.build(); } - private static JsonObject resolveTargetAlias(final Target target, final String tenantId) { + private static Stream resolveSourceAddresses( + final Set unresolvedSourceAddresses, + final CharSequence tenantId + ) { + return unresolvedSourceAddresses.stream() + .map(unresolvedSourceAddress -> HonoAddressAlias.forAliasValue(unresolvedSourceAddress) + .map(honoAddressAlias -> honoAddressAlias.resolveAddress(tenantId)) + .orElse(unresolvedSourceAddress)); + } + + private static String resolveTargetAddress(final String unresolvedTargetAddress, final CharSequence tenantId) { + return HonoAddressAlias.forAliasValue(unresolvedTargetAddress) + .map(honoAddressAlias -> honoAddressAlias.resolveAddressWithThingIdSuffix(tenantId)) + .orElse(unresolvedTargetAddress); + } + + private static JsonObject resolveTargetAlias(final Target target, final CharSequence tenantId) { final var targetBuilder = JsonFactory.newObjectBuilder(target.toJson()) - .set(Target.JsonFields.ADDRESS, Optional.of(target.getAddress()) - .map(address -> HonoAddressAlias.resolve(address, tenantId, true)) - .orElse(null), jsonField -> !jsonField.getValue().asString().isEmpty()); + .set(Target.JsonFields.ADDRESS, resolveTargetAddress(target.getAddress(), tenantId)); var headerMapping = target.getHeaderMapping().toJson() .setValue("device_id", "{{ thing:id }}") .setValue("correlation-id", "{{ header:correlation-id }}") diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java index 45896a06d4..cace0426a2 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -119,7 +119,8 @@ private static void validateTargetAddress(final String address, final DittoHeade throwEmptyException(dittoHeaders); } - HonoAddressAlias.fromName(address).filter(alias -> alias == HonoAddressAlias.COMMAND) + HonoAddressAlias.forAliasValue(address) + .filter(HonoAddressAlias.COMMAND::equals) .orElseThrow(() -> buildInvalidTargetAddressException(address, dittoHeaders)); } @@ -128,12 +129,11 @@ private static void validateSourceAddress(final String address, final DittoHeade throwEmptyException(dittoHeaders); } - var honoAddressAlias = HonoAddressAlias.fromName(address); + var honoAddressAlias = HonoAddressAlias.forAliasValue(address); honoAddressAlias.filter(alias -> alias != HonoAddressAlias.COMMAND) .orElseThrow(() -> { - String aliases = HonoAddressAlias.names() - .stream() - .filter(item -> !item.equalsIgnoreCase(HonoAddressAlias.COMMAND.getName())) + String aliases = HonoAddressAlias.aliasValues() + .filter(item -> !item.equalsIgnoreCase(HonoAddressAlias.COMMAND.getAliasValue())) .toList() .toString(); return buildInvalidSourceAddressException(address, dittoHeaders, aliases); From 0391f9283be4f8ec5184915538f3a6d5432ea773 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Fri, 22 Jul 2022 15:21:37 +0200 Subject: [PATCH 23/65] Refactored `HonoValidator` and adjusted unit tests. * Applied clean code principles to `HonoValidator` to improve its readability. * Made existing unit tests more comprehensive. * Added some unit test cases. Signed-off-by: Juergen Fickel --- .../service/messaging/hono/HonoValidator.java | 169 ++++++----- .../messaging/hono/HonoValidatorTest.java | 279 +++++++++++++----- 2 files changed, 292 insertions(+), 156 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java index cace0426a2..3c82e28408 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java @@ -12,18 +12,20 @@ */ package org.eclipse.ditto.connectivity.service.messaging.hono; -import static org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders.newEntityPlaceholder; -import static org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders.newFeaturePlaceholder; -import static org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders.newPolicyPlaceholder; -import static org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders.newThingPlaceholder; - import java.text.MessageFormat; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionConfigurationInvalidException; import org.eclipse.ditto.connectivity.model.ConnectionType; @@ -39,16 +41,15 @@ @Immutable public final class HonoValidator extends AbstractProtocolValidator { - private static final String INVALID_SOURCE_ADDRESS_ALIAS_FORMAT = "The provided source address is not valid: {0}." + - " It should be one of the defined {1} aliases."; - private static final String INVALID_TARGET_ADDRESS_ALIAS_FORMAT = "The provided target address is not" + - "valid: {0}. It should be 'command' alias."; - private static final String NOT_EMPTY_FORMAT = "The provided {0} in your target address may not be empty."; - @Nullable private static HonoValidator instance; + private final Set allowedSourceAddressHonoAliasValues; + private HonoValidator() { - super(); + allowedSourceAddressHonoAliasValues = Stream.of(HonoAddressAlias.values()) + .filter(honoAddressAlias -> HonoAddressAlias.COMMAND != honoAddressAlias) + .map(HonoAddressAlias::getAliasValue) + .collect(Collectors.toCollection(LinkedHashSet::new)); } /** @@ -57,7 +58,7 @@ private HonoValidator() { * @return the instance. */ public static HonoValidator getInstance() { - HonoValidator result = instance; + var result = instance; if (null == result) { result = new HonoValidator(); instance = result; @@ -75,93 +76,111 @@ public void validate(final Connection connection, final DittoHeaders dittoHeaders, final ActorSystem actorSystem, final ConnectivityConfig connectivityConfig) { + validateSourceConfigs(connection, dittoHeaders); validateTargetConfigs(connection, dittoHeaders); validatePayloadMappings(connection, actorSystem, connectivityConfig, dittoHeaders); } @Override - protected void validateSource(final Source source, final DittoHeaders dittoHeaders, + protected void validateSource(final Source source, + final DittoHeaders dittoHeaders, final Supplier sourceDescription) { - source.getEnforcement().ifPresent(enforcement -> { - validateTemplate(enforcement.getInput(), dittoHeaders, PlaceholderFactory.newHeadersPlaceholder()); - enforcement.getFilters().forEach(filterTemplate -> - validateTemplate(filterTemplate, dittoHeaders, newThingPlaceholder(), newPolicyPlaceholder(), - newEntityPlaceholder(), newFeaturePlaceholder())); - }); - source.getAddresses().forEach( - address -> validateSourceAddress(address, dittoHeaders)); + + validateSourceEnforcement(source, dittoHeaders); + validateSourceAddresses(source, dittoHeaders); validateSourceQos(source, dittoHeaders); } - @Override - protected void validateTarget(final Target target, final DittoHeaders dittoHeaders, - final Supplier targetDescription) { - validateTargetAddress(target.getAddress(), dittoHeaders); - validateExtraFields(target); + private void validateSourceEnforcement(final Source source, final DittoHeaders dittoHeaders) { + final Consumer validateInputTemplate = + inputTemplate -> validateTemplate(inputTemplate, + dittoHeaders, + PlaceholderFactory.newHeadersPlaceholder()); + + final Consumer> validateFilterTemplates = + filters -> filters.forEach( + filterTemplate -> validateTemplate(filterTemplate, + dittoHeaders, + ConnectivityPlaceholders.newThingPlaceholder(), + ConnectivityPlaceholders.newPolicyPlaceholder(), + ConnectivityPlaceholders.newEntityPlaceholder(), + ConnectivityPlaceholders.newFeaturePlaceholder()) + ); + + source.getEnforcement() + .ifPresent(enforcement -> { + validateInputTemplate.accept(enforcement.getInput()); + validateFilterTemplates.accept(enforcement.getFilters()); + }); } - private static void validateSourceQos(final Source source, final DittoHeaders dittoHeaders) { - source.getQos().ifPresent(qos -> { - if (qos < 0 || qos > 1) { - throw ConnectionConfigurationInvalidException - .newBuilder("Invalid 'qos' value for Kafka source, supported are: <0> or <1>. " + - "Configured 'qos' value was: <" + qos + ">" - ) - .dittoHeaders(dittoHeaders) - .build(); - } - }); + private void validateSourceAddresses(final Source source, final DittoHeaders dittoHeaders) { + final var sourceAddresses = source.getAddresses(); + sourceAddresses.forEach(address -> validateSourceAddress(address, dittoHeaders)); } - private static void validateTargetAddress(final String address, final DittoHeaders dittoHeaders) { - if (address.isEmpty()) { - throwEmptyException(dittoHeaders); + private void validateSourceAddress(final String sourceAddress, final DittoHeaders dittoHeaders) { + if (sourceAddress.isEmpty()) { + throw newConnectionConfigurationInvalidException("The provided source address must not be empty.", + dittoHeaders); } - HonoAddressAlias.forAliasValue(address) - .filter(HonoAddressAlias.COMMAND::equals) - .orElseThrow(() -> buildInvalidTargetAddressException(address, dittoHeaders)); + if (!allowedSourceAddressHonoAliasValues.contains(sourceAddress)) { + throw newConnectionConfigurationInvalidException( + MessageFormat.format("The provided source address <{0}> is invalid." + + " It should be one of the defined aliases: {1}", + sourceAddress, + allowedSourceAddressHonoAliasValues), + dittoHeaders + ); + } } - private static void validateSourceAddress(final String address, final DittoHeaders dittoHeaders) { - if (address.isEmpty()) { - throwEmptyException(dittoHeaders); - } + private static ConnectionConfigurationInvalidException newConnectionConfigurationInvalidException( + final String errorMessage, + final DittoHeaders dittoHeaders + ) { + return ConnectionConfigurationInvalidException.newBuilder(errorMessage).dittoHeaders(dittoHeaders).build(); + } - var honoAddressAlias = HonoAddressAlias.forAliasValue(address); - honoAddressAlias.filter(alias -> alias != HonoAddressAlias.COMMAND) - .orElseThrow(() -> { - String aliases = HonoAddressAlias.aliasValues() - .filter(item -> !item.equalsIgnoreCase(HonoAddressAlias.COMMAND.getAliasValue())) - .toList() - .toString(); - return buildInvalidSourceAddressException(address, dittoHeaders, aliases); + private static void validateSourceQos(final Source source, final DittoHeaders dittoHeaders) { + source.getQos() + .filter(qos -> qos < 0 || qos > 1) + .ifPresent(qos -> { + throw newConnectionConfigurationInvalidException( + MessageFormat.format( + "Invalid source ''qos'' value <{0}>. Supported values are <0> and <1>.", + qos), + dittoHeaders + ); }); } - private static void throwEmptyException(final DittoHeaders dittoHeaders) { - final String message = MessageFormat.format(NOT_EMPTY_FORMAT, "address"); - throw ConnectionConfigurationInvalidException.newBuilder(message) - .dittoHeaders(dittoHeaders) - .build(); - } + @Override + protected void validateTarget(final Target target, + final DittoHeaders dittoHeaders, + final Supplier targetDescription) { - private static ConnectionConfigurationInvalidException buildInvalidTargetAddressException(String address, - final DittoHeaders dittoHeaders) { - final String message = MessageFormat.format(INVALID_TARGET_ADDRESS_ALIAS_FORMAT, address); - throw ConnectionConfigurationInvalidException.newBuilder(message) - .dittoHeaders(dittoHeaders) - .build(); + validateTargetAddress(target.getAddress(), dittoHeaders); + validateExtraFields(target); } - private static ConnectionConfigurationInvalidException buildInvalidSourceAddressException(String address, - final DittoHeaders dittoHeaders, String definedAliases) { - final String message = MessageFormat.format(INVALID_SOURCE_ADDRESS_ALIAS_FORMAT, address, - definedAliases); - throw ConnectionConfigurationInvalidException.newBuilder(message) - .dittoHeaders(dittoHeaders) - .build(); + private static void validateTargetAddress(final String targetAddress, final DittoHeaders dittoHeaders) { + if (targetAddress.isEmpty()) { + throw newConnectionConfigurationInvalidException("The provided target address must not be empty.", + dittoHeaders); + } + + final var allowedTargetAddressAliasValue = HonoAddressAlias.COMMAND.getAliasValue(); + if (!Objects.equals(allowedTargetAddressAliasValue, targetAddress)) { + throw newConnectionConfigurationInvalidException( + MessageFormat.format("The provided target address <{0}> is invalid. It should be <{1}>.", + targetAddress, + allowedTargetAddressAliasValue), + dittoHeaders + ); + } } } diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java index 273b8f3d24..1b698e855f 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java @@ -13,11 +13,16 @@ package org.eclipse.ditto.connectivity.service.messaging.hono; import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.eclipse.ditto.connectivity.service.messaging.TestConstants.Authorization.AUTHORIZATION_CONTEXT; -import java.util.concurrent.TimeUnit; +import java.util.List; +import java.util.stream.Stream; +import org.assertj.core.api.JUnitSoftAssertions; +import org.eclipse.ditto.base.model.correlationid.TestNameCorrelationId; import org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionConfigurationInvalidException; @@ -25,118 +30,226 @@ import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.ConnectivityStatus; +import org.eclipse.ditto.connectivity.model.Enforcement; +import org.eclipse.ditto.connectivity.model.HonoAddressAlias; import org.eclipse.ditto.connectivity.model.Topic; import org.eclipse.ditto.connectivity.service.config.ConnectivityConfig; import org.eclipse.ditto.connectivity.service.messaging.TestConstants; -import org.junit.AfterClass; +import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; +import org.eclipse.ditto.placeholders.UnresolvedPlaceholderException; import org.junit.Before; -import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; -import akka.actor.ActorSystem; -import akka.testkit.javadsl.TestKit; - /** - * Unit test for {@link org.eclipse.ditto.connectivity.service.messaging.hono.HonoValidatorTest}. + * Unit test for {@link org.eclipse.ditto.connectivity.service.messaging.hono.HonoValidator}. */ public final class HonoValidatorTest { + @ClassRule + public static final ActorSystemResource ACTOR_SYSTEM_RESOURCE = + ActorSystemResource.newInstance(TestConstants.CONFIG); + private static final ConnectionId CONNECTION_ID = TestConstants.createRandomConnectionId(); - private static ActorSystem actorSystem; - private static ConnectivityConfig connectivityConfig; + private static final ConnectivityConfig CONNECTIVITY_CONFIG = TestConstants.CONNECTIVITY_CONFIG; + + @Rule + public final TestNameCorrelationId testNameCorrelationId = TestNameCorrelationId.newInstance(); + + @Rule + public final JUnitSoftAssertions softly = new JUnitSoftAssertions(); private HonoValidator underTest; - @BeforeClass - public static void initTestFixture() { - actorSystem = ActorSystem.create("AkkaTestSystem", TestConstants.CONFIG); - connectivityConfig = TestConstants.CONNECTIVITY_CONFIG; + @Before + public void before() { + underTest = HonoValidator.getInstance(); } - @AfterClass - public static void tearDown() { - if (actorSystem != null) { - TestKit.shutdownActorSystem(actorSystem, scala.concurrent.duration.Duration.apply(5, TimeUnit.SECONDS), - false); - } + @Test + public void validateWithValidEnforcementThrowsNoException() { + assertThatCode( + () -> underTest.validate(getConnectionWithSourceEnforcement( + ConnectivityModelFactory.newEnforcement("{{ header:device_id }}", + "{{ thing:id }}", + "{{ thing:name }}", + "{{ thing:namespace }}") + ), + getDittoHeadersWithCorrelationId(), + ACTOR_SYSTEM_RESOURCE.getActorSystem(), + CONNECTIVITY_CONFIG) + ).doesNotThrowAnyException(); } - @Before - public void setUp() { - underTest = HonoValidator.getInstance(); + @Test + public void validateWithInvalidMatcherThrowsException() { + final var dittoHeaders = getDittoHeadersWithCorrelationId(); + + assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) + .isThrownBy(() -> underTest.validate(getConnectionWithSourceEnforcement( + ConnectivityModelFactory.newEnforcement( + "{{ header:device_id }}", + "{{ header:ditto }}") + ), + dittoHeaders, + ACTOR_SYSTEM_RESOURCE.getActorSystem(), + CONNECTIVITY_CONFIG)) + .withCauseInstanceOf(UnresolvedPlaceholderException.class) + .satisfies(exception -> assertThat(exception.getDittoHeaders()).isEqualTo(dittoHeaders)); } @Test - public void testValidSourceAddress() { - final DittoHeaders emptyDittoHeaders = DittoHeaders.empty(); - underTest.validate(getConnectionWithSource("event"), emptyDittoHeaders, actorSystem, - connectivityConfig); - underTest.validate(getConnectionWithSource("telemetry"), emptyDittoHeaders, actorSystem, - connectivityConfig); - underTest.validate(getConnectionWithSource("command_response"), emptyDittoHeaders, actorSystem, - connectivityConfig); + public void validateWithValidSourceAddressesThrowsNoException() { + final var dittoHeaders = getDittoHeadersWithCorrelationId(); + Stream.of(HonoAddressAlias.values()) + .filter(honoAddressAlias -> HonoAddressAlias.COMMAND != honoAddressAlias) + .map(HonoAddressAlias::getAliasValue) + .forEach(honoAddressAliasValue -> softly.assertThatCode(() -> underTest.validate( + getConnectionWithSourceAddress(honoAddressAliasValue), + dittoHeaders, + ACTOR_SYSTEM_RESOURCE.getActorSystem(), + CONNECTIVITY_CONFIG + )) + .as(honoAddressAliasValue) + .doesNotThrowAnyException()); } @Test - public void testInvalidSourceAddress() { - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithSource(""), "empty"); - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithSource("command"), - "[command_response, telemetry, event]"); - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithSource("events/"), - "[command_response, telemetry, event]"); - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithSource("hono.telemetry" + - ".c4bc9a62-8516-4232-bb81-dbbfe4d0fa8c_hub"), "[command_response, telemetry, event]"); - verifyConnectionConfigurationInvalidExceptionIsThrown(getConnectionWithSource("ditto*a")); + public void validateWithEmptySourceAddressThrowsException() { + final var dittoHeaders = getDittoHeadersWithCorrelationId(); + assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) + .isThrownBy(() -> underTest.validate(getConnectionWithSourceAddress(""), + dittoHeaders, + ACTOR_SYSTEM_RESOURCE.getActorSystem(), + CONNECTIVITY_CONFIG)) + .withMessage("The provided source address must not be empty.") + .withNoCause() + .satisfies(exception -> assertThat(exception.getDittoHeaders()).isEqualTo(dittoHeaders)); } @Test - public void testInvalidSourceQos() { - verifyConnectionConfigurationInvalidExceptionIsThrown( - ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, ConnectionType.HONO, - ConnectivityStatus.OPEN, "tcp://localhost:999999") - .sources(singletonList(ConnectivityModelFactory.newSourceBuilder() - .address("event") - .authorizationContext(AUTHORIZATION_CONTEXT) - .qos(3) - .build())) - .build()); + public void validateWithInvalidSourceAddressesThrowsException() { + final var dittoHeaders = getDittoHeadersWithCorrelationId(); + + Stream.of( + HonoAddressAlias.COMMAND.getAliasValue(), + "events/", + "hono.telemetry.c4bc9a62-8516-4232-bb81-dbbfe4d0fa8c_hub", + "ditto*a" + ).forEach( + invalidSourceAddress -> softly.assertThatThrownBy( + () -> underTest.validate(getConnectionWithSourceAddress(invalidSourceAddress), + dittoHeaders, + ACTOR_SYSTEM_RESOURCE.getActorSystem(), + CONNECTIVITY_CONFIG) + ) + .as(invalidSourceAddress) + .hasMessageStartingWith("The provided source address <%s> is invalid." + + " It should be one of the defined aliases: ", + invalidSourceAddress) + .hasMessageContainingAll(HonoAddressAlias.aliasValues().toArray(CharSequence[]::new)) + .hasNoCause() + .isInstanceOfSatisfying(ConnectionConfigurationInvalidException.class, + exception -> assertThat(exception.getDittoHeaders()).isEqualTo(dittoHeaders)) + ); } @Test - public void testValidTargetAddress() { - final DittoHeaders emptyDittoHeaders = DittoHeaders.empty(); - underTest.validate(getConnectionWithTarget("command"), emptyDittoHeaders, actorSystem, connectivityConfig); + public void validateWithInvalidSourceQosThrowsException() { + final var invalidQos = 3; + final var dittoHeaders = getDittoHeadersWithCorrelationId(); + + assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) + .isThrownBy(() -> underTest.validate(ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, + ConnectionType.HONO, + ConnectivityStatus.OPEN, + "tcp://localhost:999999") + .sources(singletonList(ConnectivityModelFactory.newSourceBuilder() + .address("event") + .authorizationContext(AUTHORIZATION_CONTEXT) + .qos(invalidQos) + .build())) + .build(), + dittoHeaders, + ACTOR_SYSTEM_RESOURCE.getActorSystem(), + CONNECTIVITY_CONFIG)) + .withMessage("Invalid source 'qos' value <%d>. Supported values are <0> and <1>.", invalidQos) + .withNoCause() + .satisfies(exception -> assertThat(exception.getDittoHeaders()).isEqualTo(dittoHeaders)); } @Test - public void testInvalidTargetAddress() { - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget(""), "empty"); - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("event"), "command"); - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("telemetry"), "command"); - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(getConnectionWithTarget("command_response"), - "command"); - verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage( - getConnectionWithTarget("hono.command.c4bc9a62-8516-4232-bb81-dbbfe4d0fa8c_hub/{{thing:id}}"), - "command"); + public void validateWithValidTargetAddressThrowsNoException() { + assertThatCode( + () -> underTest.validate(getConnectionWithTargetAddress(HonoAddressAlias.COMMAND.getAliasValue()), + getDittoHeadersWithCorrelationId(), + ACTOR_SYSTEM_RESOURCE.getActorSystem(), + CONNECTIVITY_CONFIG) + ).doesNotThrowAnyException(); } - private static Connection getConnectionWithTarget(final String target) { - return ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, ConnectionType.HONO, - ConnectivityStatus.OPEN, "tcp://localhost:1883") - .targets(singletonList(ConnectivityModelFactory.newTargetBuilder() - .address(target) + @Test + public void validateWithEmptyTargetAddressThrowsException() { + final var dittoHeaders = getDittoHeadersWithCorrelationId(); + + assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) + .isThrownBy(() -> underTest.validate(getConnectionWithTargetAddress(""), + dittoHeaders, + ACTOR_SYSTEM_RESOURCE.getActorSystem(), + CONNECTIVITY_CONFIG)) + .withMessage("The provided target address must not be empty.") + .withNoCause() + .satisfies(exception -> assertThat(exception.getDittoHeaders()).isEqualTo(dittoHeaders)); + } + + @Test + public void validateWithInvalidTargetAddressesThrowsException() { + final var dittoHeaders = getDittoHeadersWithCorrelationId(); + + Stream.concat( + Stream.of(HonoAddressAlias.values()) + .filter(honoAddressAlias -> HonoAddressAlias.COMMAND != honoAddressAlias) + .map(HonoAddressAlias::getAliasValue), + Stream.of("hono.command.c4bc9a62-8516-4232-bb81-dbbfe4d0fa8c_hub/{{thing:id}}") + ).forEach( + invalidTargetAddress -> softly.assertThatThrownBy( + () -> underTest.validate(getConnectionWithTargetAddress(invalidTargetAddress), + dittoHeaders, + ACTOR_SYSTEM_RESOURCE.getActorSystem(), + CONNECTIVITY_CONFIG) + ) + .as(invalidTargetAddress) + .hasMessage("The provided target address <%s> is invalid. It should be <%s>.", + invalidTargetAddress, + HonoAddressAlias.COMMAND.getAliasValue()) + .hasNoCause() + .isInstanceOfSatisfying(ConnectionConfigurationInvalidException.class, + exception -> assertThat(exception.getDittoHeaders()).isEqualTo(dittoHeaders)) + ); + } + + private static Connection getConnectionWithSourceEnforcement(final Enforcement sourceEnforcement) { + return ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, + ConnectionType.HONO, + ConnectivityStatus.OPEN, + "tcp://localhost:99999") + .sources(List.of(ConnectivityModelFactory.newSourceBuilder() + .address(HonoAddressAlias.TELEMETRY.getAliasValue()) .authorizationContext(AUTHORIZATION_CONTEXT) + .enforcement(sourceEnforcement) .qos(1) - .topics(Topic.LIVE_EVENTS) .build())) .build(); } - private static Connection getConnectionWithSource(final String sourceAddress) { - return ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, ConnectionType.HONO, - ConnectivityStatus.OPEN, "tcp://localhost:99999") - .sources(singletonList(ConnectivityModelFactory.newSourceBuilder() + private static Connection getConnectionWithSourceAddress(final String sourceAddress) { + return ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, + ConnectionType.HONO, + ConnectivityStatus.OPEN, + "tcp://localhost:99999") + .sources(List.of(ConnectivityModelFactory.newSourceBuilder() .address(sourceAddress) .authorizationContext(AUTHORIZATION_CONTEXT) .qos(1) @@ -144,18 +257,22 @@ private static Connection getConnectionWithSource(final String sourceAddress) { .build(); } - private void verifyConnectionConfigurationInvalidExceptionIsThrown(final Connection connection) { - assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) - .isThrownBy( - () -> underTest.validate(connection, DittoHeaders.empty(), actorSystem, connectivityConfig)); + private static Connection getConnectionWithTargetAddress(final String targetAddress) { + return ConnectivityModelFactory.newConnectionBuilder(CONNECTION_ID, + ConnectionType.HONO, + ConnectivityStatus.OPEN, + "tcp://localhost:1883") + .targets(singletonList(ConnectivityModelFactory.newTargetBuilder() + .address(targetAddress) + .authorizationContext(AUTHORIZATION_CONTEXT) + .qos(1) + .topics(Topic.LIVE_EVENTS) + .build())) + .build(); } - private void verifyConnectionConfigurationInvalidExceptionIsThrownAndMessage(final Connection connection, - String messages) { - assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) - .isThrownBy( - () -> underTest.validate(connection, DittoHeaders.empty(), actorSystem, connectivityConfig)) - .withMessageContaining(messages); + private DittoHeaders getDittoHeadersWithCorrelationId() { + return DittoHeaders.newBuilder().correlationId(testNameCorrelationId.getCorrelationId()).build(); } } From c86312f2ea41cea647745a232f7d2515519f3da8 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Mon, 25 Jul 2022 17:36:11 +0300 Subject: [PATCH 24/65] Unit tests created Signed-off-by: Andrey Balarev --- .../connectivity/model/HonoAddressAlias.java | 2 +- .../model/ImmutableHeaderMapping.java | 4 +- .../DefaultClientActorPropsFactory.java | 2 +- .../hono/DefaultHonoConnectionFactory.java | 17 +- .../messaging/hono/HonoConnectionFactory.java | 113 +++---- .../DefaultClientActorPropsFactoryTest.java | 11 + .../DefaultHonoConnectionFactoryTest.java | 74 +++++ .../hono/HonoConnectionFactoryTest.java | 201 +++++++++++++ .../src/test/resources/test-connection.json | 283 ++++++++++++++++++ .../service/src/test/resources/test.conf | 10 + 10 files changed, 662 insertions(+), 55 deletions(-) create mode 100644 connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java create mode 100644 connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java create mode 100644 connectivity/service/src/test/resources/test-connection.json diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 7e21c69a31..78598ca5f5 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -105,7 +105,7 @@ public static String resolve(String alias, String tenantId, boolean thingSuffix) String suffix = thingSuffix ? "/{{thing:id}}" : ""; return fromName(alias) .map(found -> "hono." + found.getName() + (tenantId.isEmpty() ? "" : "." + tenantId) + suffix) - .orElse(alias); + .orElse(""); } /** diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableHeaderMapping.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableHeaderMapping.java index b789da0f1a..c7ac3812e5 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableHeaderMapping.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableHeaderMapping.java @@ -31,7 +31,7 @@ * Immutable implementation of a {@link HeaderMapping}. */ @Immutable -public final class ImmutableHeaderMapping implements HeaderMapping { +final class ImmutableHeaderMapping implements HeaderMapping { private final Map mapping; @@ -52,7 +52,7 @@ public Map getMapping() { * @throws NullPointerException if {@code jsonObject} is {@code null}. * @throws org.eclipse.ditto.json.JsonParseException if {@code jsonObject} is not an appropriate JSON object. */ - public static HeaderMapping fromJson(final JsonObject jsonObject) { + static HeaderMapping fromJson(final JsonObject jsonObject) { return new ImmutableHeaderMapping(jsonObject.stream() .filter(f -> f.getValue().isString()) .collect(Collectors.toMap(JsonField::getKeyName, jsonField -> jsonField.getValue().asString()))); diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index 21abd71312..8558539385 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -39,7 +39,7 @@ public final class DefaultClientActorPropsFactory implements ClientActorPropsFac @Nullable private static DefaultClientActorPropsFactory instance; - public DefaultClientActorPropsFactory() { + private DefaultClientActorPropsFactory() { super(); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java index 8c816501ba..3cb00011de 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java @@ -1,3 +1,15 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ package org.eclipse.ditto.connectivity.service.messaging.hono; import org.eclipse.ditto.connectivity.model.Connection; @@ -7,7 +19,7 @@ public class DefaultHonoConnectionFactory extends HonoConnectionFactory { - private DefaultHonoConnectionFactory(ActorSystem actorSystem, Connection connection) { + private DefaultHonoConnectionFactory(final ActorSystem actorSystem, final Connection connection) { super(actorSystem, connection); } @@ -21,6 +33,9 @@ protected String getTenantId() { return ""; } + public static DefaultHonoConnectionFactory getInstance(final ActorSystem actorSystem, final Connection connection) { + return new DefaultHonoConnectionFactory(actorSystem, connection); + } public static Connection getEnrichedConnection(ActorSystem actorSystem, Connection connection) { return new DefaultHonoConnectionFactory(actorSystem, connection).enrichConnection(); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index d001ce34b6..8d8bfb0eba 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -1,28 +1,35 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ package org.eclipse.ditto.connectivity.service.messaging.hono; import java.net.URI; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.HonoAddressAlias; -import org.eclipse.ditto.connectivity.model.ImmutableHeaderMapping; import org.eclipse.ditto.connectivity.model.Source; import org.eclipse.ditto.connectivity.model.Target; import org.eclipse.ditto.connectivity.model.Topic; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; import org.eclipse.ditto.connectivity.service.config.HonoConfig; -import org.eclipse.ditto.json.JsonArray; -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonValue; import akka.actor.ActorSystem; public abstract class HonoConnectionFactory { + protected final ActorSystem actorSystem; protected final Connection connection; protected final HonoConfig honoConfig; @@ -34,14 +41,20 @@ protected HonoConnectionFactory(ActorSystem actorSystem, Connection connection) } protected abstract UserPasswordCredentials getCredentials(); + protected abstract String getTenantId(); + private static String getBootstrapServerUrisAsCommaSeparatedListString(final HonoConfig honoConfig) { + return honoConfig.getBootstrapServerUris() + .stream() + .map(URI::toString) + .collect(Collectors.joining(",")); + } + public Connection enrichConnection() { final var connectionId = connection.getId(); - return ConnectivityModelFactory.newConnectionBuilder(connection.getId(), - connection.getConnectionType(), - connection.getConnectionStatus(), - honoConfig.getBaseUri().toString()) + return ConnectivityModelFactory.newConnectionBuilder(connection) + .uri(honoConfig.getBaseUri().toString()) .validateCertificate(honoConfig.isValidateCertificates()) .specificConfig(Map.of( "saslMechanism", honoConfig.getSaslMechanism().toString(), @@ -49,58 +62,58 @@ public Connection enrichConnection() { "groupId", (getTenantId().isEmpty() ? "" : getTenantId() + "_") + connectionId) ) .credentials(getCredentials()) - .sources(connection.getSources() + .setSources(connection.getSources() .stream() - .map(source -> ConnectivityModelFactory.sourceFromJson( - resolveSourceAliases(source, getTenantId()), 1)) + .map(source -> resolveSourceAliases(source, getTenantId())) .toList()) - .targets(connection.getTargets() + .setTargets(connection.getTargets() .stream() - .map(target -> ConnectivityModelFactory.targetFromJson(resolveTargetAlias(target, getTenantId()))) + .map(target -> resolveTargetAlias(target, getTenantId())) .toList()) .build(); } - private static String getBootstrapServerUrisAsCommaSeparatedListString(final HonoConfig honoConfig) { - return honoConfig.getBootstrapServerUris() - .stream() - .map(URI::toString) - .collect(Collectors.joining(",")); - } - - private static JsonObject resolveSourceAliases(final Source source, final String tenantId) { - final var sourceBuilder = JsonFactory.newObjectBuilder(source.toJson()) - .set(Source.JsonFields.ADDRESSES, JsonArray.of(source.getAddresses().stream() + @SuppressWarnings("unchecked") + private static Source resolveSourceAliases(final Source source, final String tenantId) { + final var sourceBuilder = ConnectivityModelFactory.newSourceBuilder(source) + .addresses(source.getAddresses() + .stream() .map(address -> HonoAddressAlias.resolve(address, tenantId)) - .map(JsonValue::of) - .toList())); - source.getReplyTarget().ifPresent(replyTarget -> { - var headerMapping = replyTarget.getHeaderMapping().toJson() - .setValue("correlation-id", "{{ header:correlation-id }}"); - if (HonoAddressAlias.COMMAND.getName().equals(replyTarget.getAddress())) { - headerMapping = headerMapping - .setValue("device_id", "{{ thing:id }}") - .setValue("subject", - "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); - } - sourceBuilder.set("replyTarget", replyTarget.toBuilder() - .address(HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)) - .headerMapping(ImmutableHeaderMapping.fromJson(headerMapping)) - .build().toJson()); - }); + .filter(address -> !address.isEmpty()) + .collect(Collectors.toSet())); + if (source.getAddresses().contains(HonoAddressAlias.TELEMETRY.getName()) + || source.getAddresses().contains(HonoAddressAlias.EVENT.getName())) { + source.getReplyTarget().ifPresent(replyTarget -> { + var headerMapping = replyTarget.getHeaderMapping().toJson() + .setValue("correlation-id", "{{ header:correlation-id }}"); + if (HonoAddressAlias.COMMAND.getName().equals(replyTarget.getAddress())) { + headerMapping = headerMapping + .setValue("device_id", "{{ thing:id }}") + .setValue("subject", + "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); + } + sourceBuilder.replyTarget(replyTarget.toBuilder() + .address(HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)) + .headerMapping(ConnectivityModelFactory.newHeaderMapping(headerMapping)) + .build()); + }); + } if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getName())) { - sourceBuilder.set("headerMapping", source.getHeaderMapping().toJson() - .setValue("correlation-id", "{{ header:correlation-id }}") - .setValue("status", "{{ header:status }}")); + sourceBuilder.headerMapping(ConnectivityModelFactory + .newHeaderMapping(source.getHeaderMapping().toJson() + .setValue("correlation-id", "{{ header:correlation-id }}") + .setValue("status", "{{ header:status }}"))); } return sourceBuilder.build(); } - private static JsonObject resolveTargetAlias(final Target target, final String tenantId) { - final var targetBuilder = JsonFactory.newObjectBuilder(target.toJson()) - .set(Target.JsonFields.ADDRESS, Optional.of(target.getAddress()) - .map(address -> HonoAddressAlias.resolve(address, tenantId, true)) - .orElse(null), jsonField -> !jsonField.getValue().asString().isEmpty()); + private static Target resolveTargetAlias(final Target target, final String tenantId) { + final var resolvedAddress = HonoAddressAlias.resolve(target.getAddress(), tenantId, true); + final var targetBuilder = ConnectivityModelFactory.newTargetBuilder(target); + if (!resolvedAddress.isEmpty()) { + targetBuilder.address(resolvedAddress); + } + var headerMapping = target.getHeaderMapping().toJson() .setValue("device_id", "{{ thing:id }}") .setValue("correlation-id", "{{ header:correlation-id }}") @@ -110,7 +123,7 @@ private static JsonObject resolveTargetAlias(final Target target, final String t topic.getTopic() == Topic.LIVE_COMMANDS)) { headerMapping = headerMapping.setValue("response-required", "{{ header:response-required }}"); } - targetBuilder.set("headerMapping", headerMapping); + targetBuilder.headerMapping(ConnectivityModelFactory.newHeaderMapping(headerMapping)); return targetBuilder.build(); } diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactoryTest.java index d77a8dbd08..a21094c9bc 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactoryTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactoryTest.java @@ -15,6 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.ditto.connectivity.model.ConnectionType.AMQP_091; import static org.eclipse.ditto.connectivity.model.ConnectionType.AMQP_10; +import static org.eclipse.ditto.connectivity.model.ConnectionType.HONO; import static org.eclipse.ditto.connectivity.model.ConnectionType.KAFKA; import static org.eclipse.ditto.connectivity.model.ConnectionType.MQTT; import static org.eclipse.ditto.connectivity.model.ConnectionType.MQTT_5; @@ -110,6 +111,16 @@ public void kafkaActorPropsIsSerializable() { actorPropsIsSerializable(KAFKA); } + /** + * Tests serialization of props of Kafka client actor. The props needs to be serializable because client actors + * may be created on a different connectivity service instance using a local connection object. + */ + @Test + @SuppressWarnings("squid:S2699") + public void honoActorPropsIsSerializable() { + actorPropsIsSerializable(HONO); + } + private void actorPropsIsSerializable(final ConnectionType connectionType) { final Props props = underTest.getActorPropsForType(randomConnection(connectionType), actorSystem.deadLetters(), actorSystem.deadLetters(), actorSystem, DittoHeaders.empty(), ConfigFactory.empty()); diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java new file mode 100644 index 0000000000..cd600fe2c1 --- /dev/null +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.service.messaging.hono; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; +import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; +import org.eclipse.ditto.connectivity.service.config.HonoConfig; +import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; +import org.eclipse.ditto.json.JsonObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import com.typesafe.config.ConfigFactory; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultHonoConnectionFactoryTest { + + private Connection dummyConnection; + private HonoConfig honoConfig; + + @Rule + public final ActorSystemResource actorSystemResource = + ActorSystemResource.newInstance(ConfigFactory.load("test")); + + private DefaultHonoConnectionFactory underTest; + + @Before + public void setup() { + honoConfig = new DefaultHonoConfig(actorSystemResource.getActorSystem()); + dummyConnection = ConnectivityModelFactory.connectionFromJson(getResource("test-connection.json")); + underTest = DefaultHonoConnectionFactory.getInstance(actorSystemResource.getActorSystem(), dummyConnection); + } + + @Test + public void testGetCredentials() { + final var EXPECTED_CREDENTIALS = honoConfig.getUserPasswordCredentials(); + assertEquals(EXPECTED_CREDENTIALS, underTest.getCredentials()); + } + + @Test + public void testGetTenantId() { + var EXPECTED_TENANT_ID = ""; + assertEquals(EXPECTED_TENANT_ID, underTest.getTenantId()); + } + + private static JsonObject getResource(final String fileName) { + try (var resourceStream = DefaultHonoConnectionFactoryTest.class.getClassLoader().getResourceAsStream(fileName)) { + assert resourceStream != null; + return JsonObject.of(new String(resourceStream.readAllBytes(), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Test resource not found: " + fileName); + } + } + +} \ No newline at end of file diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java new file mode 100644 index 0000000000..078d9275be --- /dev/null +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.service.messaging.hono; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; +import org.eclipse.ditto.connectivity.model.HonoAddressAlias; +import org.eclipse.ditto.connectivity.model.Source; +import org.eclipse.ditto.connectivity.model.Target; +import org.eclipse.ditto.connectivity.model.Topic; +import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; +import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; +import org.eclipse.ditto.connectivity.service.config.HonoConfig; +import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; +import org.eclipse.ditto.json.JsonObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import com.typesafe.config.ConfigFactory; + +@RunWith(MockitoJUnitRunner.class) +public class HonoConnectionFactoryTest { + + private static final String TEST_USERNAME = "test-username"; + private static final String TEST_PASSWORD = "test-password"; + private static final String TEST_TENANT_ID = "test-tenant-id"; + @Rule + public final ActorSystemResource actorSystemResource= + ActorSystemResource.newInstance(ConfigFactory.load("test")); + private Connection testConnection; + + private HonoConnectionFactory underTest; + + @Before + public void setup() { + honoConfig = new DefaultHonoConfig(actorSystemResource.getActorSystem()); + testConnection = ConnectivityModelFactory.connectionFromJson(getResource("test-connection.json")); + } + public final UserPasswordCredentials testCredentials = + UserPasswordCredentials.newInstance(TEST_USERNAME, TEST_PASSWORD); + + + private HonoConfig honoConfig; + + @Test + public void testEnrichConnectionWithoutTenantId() { + verifyConnection(true); + } + + @Test + public void testEnrichConnectionWithTenantId() { + verifyConnection(false); + } + + private void verifyConnection(boolean withTenantId) { + underTest = getInstance(withTenantId); + var expected = enrichTestConnection(); + assertEquals(expected, underTest.enrichConnection()); + } + + private HonoConnectionFactory getInstance(boolean withTenantId) { + if (withTenantId) { + return new HonoConnectionFactory(actorSystemResource.getActorSystem(), testConnection) { + @Override + protected UserPasswordCredentials getCredentials() { + return testCredentials; + } + + @Override + protected String getTenantId() { + return TEST_TENANT_ID; + } + }; + } else { + return new HonoConnectionFactory(actorSystemResource.getActorSystem(), testConnection) { + @Override + protected UserPasswordCredentials getCredentials() { + return testCredentials; + } + + @Override + protected String getTenantId() { + return ""; + } + }; + } + } + private static JsonObject getResource(final String fileName) { + try (var resourceStream = HonoConnectionFactoryTest.class.getClassLoader().getResourceAsStream(fileName)) { + assert resourceStream != null; + return JsonObject.of(new String(resourceStream.readAllBytes(), StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Test resource not found: " + fileName); + } + } + + private static String getBootstrapServerUrisAsCommaSeparatedListString(final HonoConfig honoConfig) { + return honoConfig.getBootstrapServerUris() + .stream() + .map(URI::toString) + .collect(Collectors.joining(",")); + } + + public Connection enrichTestConnection() { + final var connectionId = testConnection.getId(); + return ConnectivityModelFactory.newConnectionBuilder(testConnection) + .uri(honoConfig.getBaseUri().toString()) + .validateCertificate(honoConfig.isValidateCertificates()) + .specificConfig(Map.of( + "saslMechanism", honoConfig.getSaslMechanism().toString(), + "bootstrapServers", getBootstrapServerUrisAsCommaSeparatedListString(honoConfig), + "groupId", (underTest.getTenantId().isEmpty() ? "" : underTest.getTenantId() + "_") + connectionId) + ) + .credentials(underTest.getCredentials()) + .setSources(testConnection.getSources() + .stream() + .map(source -> resolveSourceAliases(source, underTest.getTenantId())) + .toList()) + .setTargets(testConnection.getTargets() + .stream() + .map(target -> resolveTargetAlias(target, underTest.getTenantId())) + .toList()) + .build(); + } + + @SuppressWarnings("unchecked") + private static Source resolveSourceAliases(final Source source, final String tenantId) { + final var sourceBuilder = ConnectivityModelFactory.newSourceBuilder(source) + .addresses(source.getAddresses() + .stream() + .map(address -> HonoAddressAlias.resolve(address, tenantId)) + .filter(address -> !address.isEmpty()) + .collect(Collectors.toSet())); + if (source.getAddresses().contains(HonoAddressAlias.TELEMETRY.getName()) + || source.getAddresses().contains(HonoAddressAlias.EVENT.getName())) { + source.getReplyTarget().ifPresent(replyTarget -> { + var headerMapping = replyTarget.getHeaderMapping().toJson() + .setValue("correlation-id", "{{ header:correlation-id }}"); + if (HonoAddressAlias.COMMAND.getName().equals(replyTarget.getAddress())) { + headerMapping = headerMapping + .setValue("device_id", "{{ thing:id }}") + .setValue("subject", + "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); + } + sourceBuilder.replyTarget(replyTarget.toBuilder() + .address(HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)) + .headerMapping(ConnectivityModelFactory.newHeaderMapping(headerMapping)) + .build()); + }); + } + if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getName())) { + sourceBuilder.headerMapping(ConnectivityModelFactory + .newHeaderMapping(source.getHeaderMapping().toJson() + .setValue("correlation-id", "{{ header:correlation-id }}") + .setValue("status", "{{ header:status }}"))); + } + return sourceBuilder.build(); + } + + private static Target resolveTargetAlias(final Target target, final String tenantId) { + final var resolvedAddress = HonoAddressAlias.resolve(target.getAddress(), tenantId, true); + final var targetBuilder = ConnectivityModelFactory.newTargetBuilder(target); + if (!resolvedAddress.isEmpty()) { + targetBuilder.address(resolvedAddress); + } + + var headerMapping = target.getHeaderMapping().toJson() + .setValue("device_id", "{{ thing:id }}") + .setValue("correlation-id", "{{ header:correlation-id }}") + .setValue("subject", "{{ header:subject | fn:default(topic:action-subject) }}"); + if (target.getTopics().stream() + .anyMatch(topic -> topic.getTopic() == Topic.LIVE_MESSAGES || + topic.getTopic() == Topic.LIVE_COMMANDS)) { + headerMapping = headerMapping.setValue("response-required", "{{ header:response-required }}"); + } + targetBuilder.headerMapping(ConnectivityModelFactory.newHeaderMapping(headerMapping)); + return targetBuilder.build(); + } + +} \ No newline at end of file diff --git a/connectivity/service/src/test/resources/test-connection.json b/connectivity/service/src/test/resources/test-connection.json new file mode 100644 index 0000000000..b0a6f91210 --- /dev/null +++ b/connectivity/service/src/test/resources/test-connection.json @@ -0,0 +1,283 @@ +{ + "id": "test-connection-id", + "name": "Things-Hono Test 1", + "connectionType": "hono", + "connectionStatus": "open", + "uri": "ssl://hono-endpoint:1", + "sources": [ + { + "addresses": [ + "telemetry" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "command", + "headerMapping": { + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "event" + ], + "consumerCount": 1, + "qos": 1, + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [] + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "command", + "headerMapping": { + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "command_response" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": { + }, + "payloadMapping": [ + "Ditto" + ], + "replyTarget": { + "enabled": false + } + } + ], + "targets": [ + { + "address": "command", + "topics": [ + "_/_/things/live/messages", + "_/_/things/live/commands" + ], + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ] + }, + { + "address": "command", + "topics": [ + "_/_/things/twin/events", + "_/_/things/live/events" + ], + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "headerMapping": { + } + } + ], + "clientCount": 1, + "failoverEnabled": true, + "validateCertificates": true, + "processorPoolSize": 5, + "specificConfig": { + "groupId": "t943ff0485af04c3b91050151f46c5ebe_hub_{{connection:id}}" + }, + "mappingDefinitions": { + "implicitEdgeThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_copyPolicyFrom": "{{ header:gateway_id }}", + "attributes": { + "Info": { + "gatewayId": "{{ header:gateway_id }}" + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "behindGateway": "fn:filter(header:gateway_id, 'exists')", + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')" + } + }, + "implicitStandaloneThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_policy": { + "entries": { + "DEVICE": { + "subjects": { + "integration:{{solution:id}}:hub": { + "type": "iot-things-integration" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + }, + "DEFAULT": { + "subjects": { + "iot-suite-dev:/service-instance.{{solution:package-service-instance-id}}.iot-things": { + "type": "suite-auth" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + }, + "DEVICE-MANAGEMENT": { + "subjects": { + "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-manager": { + "type": "suite-auth" + }, + "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@developer-console": { + "type": "suite-auth" + }, + "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-rollouts": { + "type": "suite-auth" + }, + "integration:{{ solution:id }}:iot-manager": { + "type": "iot-things-integration" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + } + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')", + "notBehindGateway": "fn:filter(header:gateway_id, 'exists', 'false')" + } + }, + "status": { + "mappingEngine": "ConnectionStatus", + "options": { + "thingId": "{{ header:device_id }}" + } + } + } +} \ No newline at end of file diff --git a/connectivity/service/src/test/resources/test.conf b/connectivity/service/src/test/resources/test.conf index 63051da8d2..cef18da7af 100644 --- a/connectivity/service/src/test/resources/test.conf +++ b/connectivity/service/src/test/resources/test.conf @@ -42,6 +42,16 @@ ditto { user-indicated-errors = [ {exceptionName: "org.apache.qpid.jms.provider.exceptions.ProviderSecurityException", messagePattern: ".*"} ] + + hono { + base-uri = "tcp://localhost:9922" + validate-certificates = false + sasl-mechanism = PLAIN + bootstrap-servers = "tcp://server1:port1,tcp://server2:port2,tcp://server3:port3" + username = test_username + password = test_password + } + connection { // allow localhost in unit tests blocked-hostnames = "" From 111c99a9df0b097e26dec20915af2d96998b52c4 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Wed, 27 Jul 2022 14:02:56 +0200 Subject: [PATCH 25/65] Fixed compilation errors of `HonoConnectionFactoryTest`. `HonoAddressAlias` was refactored beforehand which caused the compilation errors. Signed-off-by: Juergen Fickel --- .../hono/HonoConnectionFactoryTest.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java index 078d9275be..b7884eade8 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java @@ -18,6 +18,7 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import org.eclipse.ditto.connectivity.model.Connection; @@ -149,27 +150,30 @@ private static Source resolveSourceAliases(final Source source, final String ten final var sourceBuilder = ConnectivityModelFactory.newSourceBuilder(source) .addresses(source.getAddresses() .stream() - .map(address -> HonoAddressAlias.resolve(address, tenantId)) - .filter(address -> !address.isEmpty()) + .map(HonoAddressAlias::forAliasValue) + .flatMap(Optional::stream) + .map(honoAddressAlias -> honoAddressAlias.resolveAddress(tenantId)) .collect(Collectors.toSet())); - if (source.getAddresses().contains(HonoAddressAlias.TELEMETRY.getName()) - || source.getAddresses().contains(HonoAddressAlias.EVENT.getName())) { + if (source.getAddresses().contains(HonoAddressAlias.TELEMETRY.getAliasValue()) + || source.getAddresses().contains(HonoAddressAlias.EVENT.getAliasValue())) { source.getReplyTarget().ifPresent(replyTarget -> { var headerMapping = replyTarget.getHeaderMapping().toJson() .setValue("correlation-id", "{{ header:correlation-id }}"); - if (HonoAddressAlias.COMMAND.getName().equals(replyTarget.getAddress())) { + if (HonoAddressAlias.COMMAND.getAliasValue().equals(replyTarget.getAddress())) { headerMapping = headerMapping .setValue("device_id", "{{ thing:id }}") .setValue("subject", "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); } sourceBuilder.replyTarget(replyTarget.toBuilder() - .address(HonoAddressAlias.resolve(replyTarget.getAddress(), tenantId, true)) + .address(HonoAddressAlias.forAliasValue(replyTarget.getAddress()) + .map(honoAddressAlias -> honoAddressAlias.resolveAddressWithThingIdSuffix(tenantId)) + .orElseGet(replyTarget::getAddress)) .headerMapping(ConnectivityModelFactory.newHeaderMapping(headerMapping)) .build()); }); } - if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getName())) { + if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getAliasValue())) { sourceBuilder.headerMapping(ConnectivityModelFactory .newHeaderMapping(source.getHeaderMapping().toJson() .setValue("correlation-id", "{{ header:correlation-id }}") @@ -179,7 +183,9 @@ private static Source resolveSourceAliases(final Source source, final String ten } private static Target resolveTargetAlias(final Target target, final String tenantId) { - final var resolvedAddress = HonoAddressAlias.resolve(target.getAddress(), tenantId, true); + final var resolvedAddress = HonoAddressAlias.forAliasValue(target.getAddress()) + .map(honoAddressAlias -> honoAddressAlias.resolveAddressWithThingIdSuffix(tenantId)) + .orElseGet(target::getAddress); final var targetBuilder = ConnectivityModelFactory.newTargetBuilder(target); if (!resolvedAddress.isEmpty()) { targetBuilder.address(resolvedAddress); From fca7c19460b525d0d5d052954817284136d34120 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Thu, 4 Aug 2022 10:06:11 +0200 Subject: [PATCH 26/65] Cleaned up `HonoConfig`. * Removed unused method `getUri`. * Do not extend `Extension` because it really does not. Signed-off-by: Juergen Fickel --- .../service/config/HonoConfig.java | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java index 3818ab97be..02b1f9dc58 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java @@ -13,20 +13,16 @@ package org.eclipse.ditto.connectivity.service.config; import java.net.URI; -import java.net.URISyntaxException; import java.util.Set; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; -import org.eclipse.ditto.internal.utils.config.DittoConfigError; import org.eclipse.ditto.internal.utils.config.KnownConfigValue; -import akka.actor.Extension; - /** * This interface provides access to the configuration properties Hono connections. * The actual configuration can be obtained via actor system extension. */ -public interface HonoConfig extends Extension { +public interface HonoConfig { /** * Prefix in .conf files @@ -121,21 +117,6 @@ public String getConfigPath() { } - /** - * Validates and gets URI from a string - * - * @param uri A {@link String} to be validated - * @return New {@link URI} from specified string - * @throws DittoConfigError if given string is not a valid URI - */ - static URI getUri(final String uri) throws DittoConfigError { - try { - return new URI(uri); - } catch (final URISyntaxException e) { - throw new DittoConfigError(e); - } - } - enum SaslMechanism { PLAIN("plain"); From 9d9705e371727db55acae9903f6c04f1b793cfc5 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Thu, 4 Aug 2022 12:52:29 +0200 Subject: [PATCH 27/65] Refactored `HonoConnectionFactory`. * Made the factory more abstract by bringing in more aspects of the template method pattern. * Moved dependency to `HonoConfig` and some implementation to default implementation. * Concept of tenant ID is unknown to Ditto, thus removed it. * Moved methods for resolving addresses from `HonoAddressAlias` to `HonoConnectionFactory`. * Extended exception handling of `ConnectionPersistenceActor#startAndAskClientActors` to catch failures related to `HonoConnectionFactory`. Signed-off-by: Juergen Fickel --- .../connectivity/model/HonoAddressAlias.java | 36 --- .../model/HonoAddressAliasTest.java | 39 --- .../DefaultClientActorPropsFactory.java | 11 +- .../hono/DefaultHonoConnectionFactory.java | 76 +++++- .../messaging/hono/HonoConnectionFactory.java | 238 ++++++++++++------ .../ConnectionPersistenceActor.java | 12 +- .../DefaultHonoConnectionFactoryTest.java | 167 +++++++++--- .../hono/HonoConnectionFactoryTest.java | 207 --------------- ...nection.json => test-connection-hono.json} | 0 9 files changed, 376 insertions(+), 410 deletions(-) delete mode 100644 connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java rename connectivity/service/src/test/resources/{test-connection.json => test-connection-hono.json} (100%) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 99dfbfe16a..00d66341c7 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -19,8 +19,6 @@ import javax.annotation.Nullable; -import org.eclipse.ditto.base.model.common.ConditionChecker; - /** * Possible address aliases used by connections of type 'Hono' */ @@ -90,38 +88,4 @@ public String getAliasValue() { return value; } - /** - * Resolves the source or target address of this address alias for the specified tenant ID. - * - * @param tenantId the tenant ID to resolve the address of this alias for. - * @return the resolved address of this address alias for {@code tenantId}. - * @throws NullPointerException if {@code tenantId} is {@code null}. - */ - public String resolveAddress(final CharSequence tenantId) { - ConditionChecker.checkNotNull(tenantId, "tenantId"); - - final String prefix = "hono."; - final String aliasValue = getAliasValue(); - final int tenantIdLength = tenantId.length(); - final StringBuilder sb = new StringBuilder(prefix.length() + aliasValue.length() + 1 + tenantIdLength); - sb.append(prefix).append(aliasValue); - if (0 < tenantIdLength) { - sb.append(".").append(tenantId); - } - return sb.toString(); - } - - /** - * Resolves the source or target address of this address alias for the specified tenant ID and appends a suffix - * for thing ID. - * This is mainly needed for reply target addresses. - * - * @param tenantId the tenant ID to resolve the address of this alias for. - * @return the resolved address of this address alias for {@code tenantId} with {@code "/{{thing:id}}"} appended. - * @throws NullPointerException if {@code tenantId} is {@code null}. - */ - public String resolveAddressWithThingIdSuffix(final CharSequence tenantId) { - return resolveAddress(tenantId) + "/{{thing:id}}"; - } - } diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java index bb399b70ea..dddcbabc4e 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java @@ -13,7 +13,6 @@ package org.eclipse.ditto.connectivity.model; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; import static org.eclipse.ditto.connectivity.model.HonoAddressAlias.COMMAND_RESPONSE; import java.util.Collection; @@ -69,42 +68,4 @@ public void forAliasValueReturnsExpectedForKnownAliasValue() { } } - @Test - public void resolveAddressWithNullTenantIdThrowsException() { - assertThatNullPointerException() - .isThrownBy(() -> HonoAddressAlias.EVENT.resolveAddress(null)) - .withMessage("The tenantId must not be null!") - .withNoCause(); - } - - @Test - public void resolveAddressWithNonEmptyTenantIdReturnsExpected() { - final HonoAddressAlias honoAddressAlias = HonoAddressAlias.EVENT; - final String tenantId = "myTenant"; - - final String resolvedAddress = honoAddressAlias.resolveAddress(tenantId); - - assertThat(resolvedAddress).isEqualTo("hono." + honoAddressAlias.getAliasValue() + "." + tenantId); - } - - @Test - public void resolveAddressWithEmptyTenantIdReturnsExpected() { - final HonoAddressAlias honoAddressAlias = HonoAddressAlias.EVENT; - - final String resolvedAddress = honoAddressAlias.resolveAddress(""); - - assertThat(resolvedAddress).isEqualTo("hono." + honoAddressAlias.getAliasValue()); - } - - @Test - public void resolveAddressWithThingIdSuffixReturnsExpected() { - final HonoAddressAlias honoAddressAlias = HonoAddressAlias.EVENT; - final String tenantId = "myTenant"; - - final String resolvedAddress = honoAddressAlias.resolveAddressWithThingIdSuffix(tenantId); - - assertThat(resolvedAddress) - .isEqualTo("hono." + honoAddressAlias.getAliasValue() + "." + tenantId + "/{{thing:id}}"); - } - } \ No newline at end of file diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index 8558539385..1014cd0e51 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -90,8 +90,7 @@ public Props getActorPropsForType(final Connection connection, connectionActor, dittoHeaders, connectivityConfigOverwrites); - case HONO -> KafkaClientActor.props( - DefaultHonoConnectionFactory.getEnrichedConnection(actorSystem, connection), + case HONO -> KafkaClientActor.props(getResolvedHonoConnectionOrThrow(actorSystem, connection), proxyActor, connectionActor, dittoHeaders, @@ -99,4 +98,12 @@ public Props getActorPropsForType(final Connection connection, }; } + private static Connection getResolvedHonoConnectionOrThrow( + final ActorSystem actorSystem, + final Connection connection + ) { + final var honoConnectionFactory = DefaultHonoConnectionFactory.newInstance(actorSystem, connection); + return honoConnectionFactory.getHonoConnection(); + } + } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java index 3cb00011de..745527a106 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java @@ -12,31 +12,85 @@ */ package org.eclipse.ditto.connectivity.service.messaging.hono; +import java.net.URI; +import java.text.MessageFormat; +import java.util.Set; + import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectionType; +import org.eclipse.ditto.connectivity.model.HonoAddressAlias; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; +import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; +import org.eclipse.ditto.connectivity.service.config.HonoConfig; import akka.actor.ActorSystem; -public class DefaultHonoConnectionFactory extends HonoConnectionFactory { +/** + * Default implementation of {@link HonoConnectionFactory}. + * This implementation uses {@link HonoConfig} to obtain the required properties for creating the Hono connection. + * + * @since 3.0.0 + */ +public final class DefaultHonoConnectionFactory extends HonoConnectionFactory { + + private final HonoConfig honoConfig; + + private DefaultHonoConnectionFactory(final HonoConfig honoConfig, final Connection connection) { + super(connection); + this.honoConfig = honoConfig; + } + + /** + * Returns a new instance of {@code DefaultHonoConnectionFactory} for the specified arguments. + * + * @param actorSystem the actor system that is used to obtain the HonoConfig. + * @param connection the connection that serves as base for the Hono connection this factory returns. + * @return the instance. + * @throws NullPointerException if any argument is {@code null}. + * @throws IllegalArgumentException if the type of {@code connection} is not {@link ConnectionType#HONO}; + */ + public static DefaultHonoConnectionFactory newInstance(final ActorSystem actorSystem, final Connection connection) { + return new DefaultHonoConnectionFactory(new DefaultHonoConfig(actorSystem), connection); + } - private DefaultHonoConnectionFactory(final ActorSystem actorSystem, final Connection connection) { - super(actorSystem, connection); + @Override + public URI getBaseUri() { + return honoConfig.getBaseUri(); } @Override - public UserPasswordCredentials getCredentials() { - return honoConfig.getUserPasswordCredentials(); + public boolean isValidateCertificates() { + return honoConfig.isValidateCertificates(); + } + + @Override + public HonoConfig.SaslMechanism getSaslMechanism() { + return honoConfig.getSaslMechanism(); + } + + @Override + public Set getBootstrapServerUris() { + return honoConfig.getBootstrapServerUris(); + } + + @Override + protected String getGroupId(final Connection connection) { + return connection.getId().toString(); } @Override - protected String getTenantId() { - return ""; + protected UserPasswordCredentials getCredentials() { + return honoConfig.getUserPasswordCredentials(); } - public static DefaultHonoConnectionFactory getInstance(final ActorSystem actorSystem, final Connection connection) { - return new DefaultHonoConnectionFactory(actorSystem, connection); + @Override + protected String resolveSourceAddress(final HonoAddressAlias honoAddressAlias) { + return MessageFormat.format("hono.{0}", honoAddressAlias.getAliasValue()); } - public static Connection getEnrichedConnection(ActorSystem actorSystem, Connection connection) { - return new DefaultHonoConnectionFactory(actorSystem, connection).enrichConnection(); + + @Override + protected String resolveTargetAddress(final HonoAddressAlias honoAddressAlias) { + return MessageFormat.format("hono.{0}/'{{thing:id}}'", honoAddressAlias.getAliasValue()); } + } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index 98d9c25a1e..e6196481bd 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -12,17 +12,25 @@ */ package org.eclipse.ditto.connectivity.service.messaging.hono; +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkArgument; +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + import java.net.URI; +import java.text.MessageFormat; import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import javax.annotation.concurrent.NotThreadSafe; + import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.FilteredTopic; import org.eclipse.ditto.connectivity.model.HeaderMapping; @@ -32,84 +40,112 @@ import org.eclipse.ditto.connectivity.model.Target; import org.eclipse.ditto.connectivity.model.Topic; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; -import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; import org.eclipse.ditto.connectivity.service.config.HonoConfig; -import org.eclipse.ditto.json.JsonFactory; - -import akka.actor.ActorSystem; +/** + * Base implementation of a factory for getting a Hono {@link Connection}. + * The Connection this factory supplies is based on a provided Connection with adjustments of + *
    + *
  • the base URI,
  • + *
  • the "validate certificates" flag,
  • + *
  • the specific config including SASL mechanism, bootstrap server URIs and group ID,
  • + *
  • the credentials and
  • + *
  • the sources and targets.
  • + *
+ * + * @since 3.0.0 + */ public abstract class HonoConnectionFactory { protected final Connection connection; - protected final HonoConfig honoConfig; - protected HonoConnectionFactory(final ActorSystem actorSystem, final Connection connection) { - this.connection = connection; - honoConfig = new DefaultHonoConfig(actorSystem); + /** + * Constructs a {@code HonoConnectionFactory}. + * + * @param connection the connection that serves as base for the Hono connection this factory returns. + * @throws NullPointerException if {@code connection} is {@code null}. + * @throws IllegalArgumentException if the type of {@code connection} is not {@link ConnectionType#HONO}; + */ + protected HonoConnectionFactory(final Connection connection) { + this.connection = checkArgument( + checkNotNull(connection, "connection"), + arg -> ConnectionType.HONO == arg.getConnectionType(), + () -> MessageFormat.format("Expected type of connection to be <{0}> but it was <{1}>.", + ConnectionType.HONO, + connection.getConnectionType()) + ); } - public Connection enrichConnection() { - final var tenantId = getTenantId(); + /** + * Returns a proper Hono Connection for the Connection that was used to create this factory instance. + * + * @return the Hono Connection. + */ + public Connection getHonoConnection() { return ConnectivityModelFactory.newConnectionBuilder(connection) - .uri(honoConfig.getBaseUri().toString()) - .validateCertificate(honoConfig.isValidateCertificates()) - .specificConfig(Map.of( - "saslMechanism", honoConfig.getSaslMechanism().toString(), - "bootstrapServers", getBootstrapServerUrisAsCommaSeparatedListString(honoConfig), - "groupId", (tenantId.isEmpty() ? "" : tenantId + "_") + connection.getId()) - ) + .uri(String.valueOf(getBaseUri())) + .validateCertificate(isValidateCertificates()) + .specificConfig(getSpecificConfig()) .credentials(getCredentials()) - .setSources(getSources(connection.getSources(), tenantId)) - .setTargets(getTargets(connection.getTargets(), tenantId)) + .setSources(getSources(connection.getSources())) + .setTargets(getTargets(connection.getTargets())) .build(); } - protected abstract String getTenantId(); + protected abstract URI getBaseUri(); + + protected abstract boolean isValidateCertificates(); + + private Map getSpecificConfig() { + return Map.of( + "saslMechanism", String.valueOf(getSaslMechanism()), + "bootstrapServers", getAsCommaSeparatedListString(getBootstrapServerUris()), + "groupId", getGroupId(connection) + ); + } + + protected abstract HonoConfig.SaslMechanism getSaslMechanism(); + + protected abstract Set getBootstrapServerUris(); - private static String getBootstrapServerUrisAsCommaSeparatedListString(final HonoConfig honoConfig) { - return honoConfig.getBootstrapServerUris() - .stream() + private static String getAsCommaSeparatedListString(final Collection uris) { + return uris.stream() .map(URI::toString) .collect(Collectors.joining(",")); } + protected abstract String getGroupId(Connection connection); + protected abstract UserPasswordCredentials getCredentials(); - private static List getSources(final Collection originalSources, final CharSequence tenantId) { + private List getSources(final Collection originalSources) { return originalSources.stream() - .map(originalSource -> resolveSourceAliases(originalSource, tenantId)) + .map(originalSource -> ConnectivityModelFactory.newSourceBuilder(originalSource) + .addresses(resolveSourceAddresses(originalSource.getAddresses())) + .replyTarget(getReplyTargetForSource(originalSource).orElse(null)) + .headerMapping(getSourceHeaderMapping(originalSource)) + .build()) .collect(Collectors.toList()); } - private static Source resolveSourceAliases(final Source source, final CharSequence tenantId) { - return ConnectivityModelFactory.newSourceBuilder(source) - .addresses(resolveSourceAddresses(source.getAddresses(), tenantId)) - .replyTarget(getReplyTarget(source, tenantId).orElse(null)) - .headerMapping(getSourceHeaderMapping(source)) - .build(); - } - - private static Set resolveSourceAddresses( - final Collection unresolvedSourceAddresses, - final CharSequence tenantId - ) { + private Set resolveSourceAddresses(final Collection unresolvedSourceAddresses) { return unresolvedSourceAddresses.stream() .map(unresolvedSourceAddress -> HonoAddressAlias.forAliasValue(unresolvedSourceAddress) - .map(honoAddressAlias -> honoAddressAlias.resolveAddress(tenantId)) + .map(this::resolveSourceAddress) .orElse(unresolvedSourceAddress)) - .collect(Collectors.toSet()); + .collect(Collectors.toCollection(LinkedHashSet::new)); } - private static Optional getReplyTarget(final Source source, final CharSequence tenantId) { + protected abstract String resolveSourceAddress(HonoAddressAlias honoAddressAlias); + + private Optional getReplyTargetForSource(final Source source) { final Optional result; if (isApplyReplyTarget(source.getAddresses())) { result = source.getReplyTarget() - .map(replyTarget -> { - final var replyTargetBuilder = replyTarget.toBuilder(); - replyTargetBuilder.address(resolveTargetAddress(replyTarget.getAddress(), tenantId)); - replyTargetBuilder.headerMapping(getReplyTargetHeaderMapping(replyTarget)); - return replyTargetBuilder.build(); - }); + .map(replyTarget -> replyTarget.toBuilder() + .address(resolveTargetAddressOrKeepUnresolved(replyTarget.getAddress())) + .headerMapping(getReplyTargetHeaderMapping(replyTarget)) + .build()); } else { result = Optional.empty(); } @@ -120,43 +156,43 @@ private static boolean isApplyReplyTarget(final Collection sourceAddress final Predicate isTelemetryHonoAddressAlias = HonoAddressAlias.TELEMETRY.getAliasValue()::equals; final Predicate isEventHonoAddressAlias = HonoAddressAlias.EVENT.getAliasValue()::equals; - return sourceAddresses.stream().anyMatch(isTelemetryHonoAddressAlias.or(isEventHonoAddressAlias)); + return sourceAddresses.stream() + .anyMatch(isTelemetryHonoAddressAlias.or(isEventHonoAddressAlias)); } - private static String resolveTargetAddress(final String unresolvedTargetAddress, final CharSequence tenantId) { + private String resolveTargetAddressOrKeepUnresolved(final String unresolvedTargetAddress) { return HonoAddressAlias.forAliasValue(unresolvedTargetAddress) - .map(honoAddressAlias -> honoAddressAlias.resolveAddressWithThingIdSuffix(tenantId)) + .map(this::resolveTargetAddress) .orElse(unresolvedTargetAddress); } + protected abstract String resolveTargetAddress(HonoAddressAlias honoAddressAlias); + private static HeaderMapping getReplyTargetHeaderMapping(final ReplyTarget replyTarget) { - final var originalHeaderMapping = replyTarget.getHeaderMapping(); - final var headerMappingBuilder = JsonFactory.newObjectBuilder(originalHeaderMapping.toJson()) - .set("correlation-id", "{{ header:correlation-id }}"); + final var headerMappingBuilder = HeaderMappingBuilder.of(replyTarget.getHeaderMapping()); + headerMappingBuilder.putCorrelationId(); if (isCommandHonoAddressAlias(replyTarget.getAddress())) { - headerMappingBuilder.set("device_id", "{{ thing:id }}"); - headerMappingBuilder.set("subject", - "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); + headerMappingBuilder.putDeviceId(); + headerMappingBuilder.putSubject( + "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response" + ); } - return ConnectivityModelFactory.newHeaderMapping(headerMappingBuilder.build()); + return headerMappingBuilder.build(); } private static boolean isCommandHonoAddressAlias(final String replyTargetAddress) { - return Objects.equals(HonoAddressAlias.COMMAND.getAliasValue(), replyTargetAddress); + return replyTargetAddress.equals(HonoAddressAlias.COMMAND.getAliasValue()); } private static HeaderMapping getSourceHeaderMapping(final Source source) { final HeaderMapping result; - final var originalHeaderMapping = source.getHeaderMapping(); if (isAdjustSourceHeaderMapping(source.getAddresses())) { - result = ConnectivityModelFactory.newHeaderMapping( - JsonFactory.newObjectBuilder(originalHeaderMapping.toJson()) - .set("correlation-id", "{{ header:correlation-id }}") - .set("status", "{{ header:status }}") - .build() - ); + result = HeaderMappingBuilder.of(source.getHeaderMapping()) + .putCorrelationId() + .putEntry("status", "{{ header:status }}") + .build(); } else { - result = originalHeaderMapping; + result = source.getHeaderMapping(); } return result; } @@ -165,31 +201,28 @@ private static boolean isAdjustSourceHeaderMapping(final Collection sour return sourceAddresses.contains(HonoAddressAlias.COMMAND_RESPONSE.getAliasValue()); } - private static List getTargets(final Collection originalTargets, final CharSequence tenantId) { + private List getTargets(final Collection originalTargets) { return originalTargets.stream() - .map(originalTarget -> resolveTargetAlias(originalTarget, tenantId)) + .map(originalTarget -> ConnectivityModelFactory.newTargetBuilder(originalTarget) + .address(resolveTargetAddressOrKeepUnresolved(originalTarget.getAddress())) + .headerMapping(getTargetHeaderMapping(originalTarget)) + .build()) .collect(Collectors.toList()); } - private static Target resolveTargetAlias(final Target target, final CharSequence tenantId) { - return ConnectivityModelFactory.newTargetBuilder(target) - .address(resolveTargetAddress(target.getAddress(), tenantId)) - .headerMapping(getTargetHeaderMapping(target)) - .build(); - } - private static HeaderMapping getTargetHeaderMapping(final Target target) { - final var headerMappingBuilder = JsonFactory.newObjectBuilder(target.getHeaderMapping().toJson()) - .set("device_id", "{{ thing:id }}") - .set("correlation-id", "{{ header:correlation-id }}") - .set("subject", "{{ header:subject | fn:default(topic:action-subject) }}"); - if (isSetResponseRequiredHeader(target.getTopics())) { - headerMappingBuilder.set("response-required", "{{ header:response-required }}"); + final var headerMappingBuilder = HeaderMappingBuilder.of(target.getHeaderMapping()) + .putDeviceId() + .putCorrelationId() + .putSubject("{{ header:subject | fn:default(topic:action-subject) }}"); + + if (isPutResponseRequiredHeaderMapping(target.getTopics())) { + headerMappingBuilder.putEntry("response-required", "{{ header:response-required }}"); } - return ConnectivityModelFactory.newHeaderMapping(headerMappingBuilder.build()); + return headerMappingBuilder.build(); } - private static boolean isSetResponseRequiredHeader(final Collection targetTopics) { + private static boolean isPutResponseRequiredHeaderMapping(final Collection targetTopics) { final Predicate isLiveMessages = topic -> Topic.LIVE_MESSAGES == topic; final Predicate isLiveCommands = topic -> Topic.LIVE_COMMANDS == topic; @@ -198,4 +231,43 @@ private static boolean isSetResponseRequiredHeader(final Collection headerMappingDefinition; + + private HeaderMappingBuilder(final HeaderMapping existingHeaderMapping) { + headerMappingDefinition = new LinkedHashMap<>(existingHeaderMapping.getMapping()); + } + + static HeaderMappingBuilder of(final HeaderMapping existingHeaderMapping) { + return new HeaderMappingBuilder(checkNotNull(existingHeaderMapping, "existingHeaderMapping")); + } + + HeaderMappingBuilder putCorrelationId() { + headerMappingDefinition.put("correlation-id", "{{ header:correlation-id }}"); + return this; + } + + HeaderMappingBuilder putDeviceId() { + headerMappingDefinition.put("device_id", "{{ thing:id }}"); + return this; + } + + HeaderMappingBuilder putSubject(final String subjectValue) { + headerMappingDefinition.put("subject", subjectValue); + return this; + } + + HeaderMappingBuilder putEntry(final String key, final String value) { + headerMappingDefinition.put(key, value); + return this; + } + + HeaderMapping build() { + return ConnectivityModelFactory.newHeaderMapping(headerMappingDefinition); + } + + } + } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index de07f2830c..667f57d467 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -912,10 +912,14 @@ private void respondWithEmptyLogs(final WithDittoHeaders command, final ActorRef } private CompletionStage startAndAskClientActors(final SignalWithEntityId cmd, final int clientCount) { - startClientActorsIfRequired(clientCount, cmd.getDittoHeaders()); - final Object msg = consistentHashableEnvelope(cmd, cmd.getEntityId().toString()); - - return processClientAskResult(Patterns.ask(clientActorRouter, msg, clientActorAskTimeout)); + try { + startClientActorsIfRequired(clientCount, cmd.getDittoHeaders()); + return processClientAskResult(Patterns.ask(clientActorRouter, + consistentHashableEnvelope(cmd, cmd.getEntityId().toString()), + clientActorAskTimeout)); + } catch (final Exception e) { + return CompletableFuture.failedStage(e); + } } private static Object consistentHashableEnvelope(final Object message, final String hashKey) { diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java index cd600fe2c1..32cfc4ac94 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java @@ -12,63 +12,174 @@ */ package org.eclipse.ditto.connectivity.service.messaging.hono; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.ditto.connectivity.model.HonoAddressAlias.COMMAND_RESPONSE; +import static org.eclipse.ditto.connectivity.model.HonoAddressAlias.EVENT; +import static org.eclipse.ditto.connectivity.model.HonoAddressAlias.TELEMETRY; import java.io.IOException; -import java.nio.charset.StandardCharsets; +import java.io.InputStreamReader; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; +import org.eclipse.ditto.connectivity.model.HonoAddressAlias; +import org.eclipse.ditto.connectivity.model.ReplyTarget; +import org.eclipse.ditto.connectivity.model.Source; import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; import org.eclipse.ditto.connectivity.service.config.HonoConfig; import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; +import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.Mockito; +import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; -@RunWith(MockitoJUnitRunner.class) -public class DefaultHonoConnectionFactoryTest { +/** + * Unit test for {@link DefaultHonoConnectionFactory}. + */ +public final class DefaultHonoConnectionFactoryTest { - private Connection dummyConnection; - private HonoConfig honoConfig; + private static final Config TEST_CONFIG = ConfigFactory.load("test"); + + private static JsonObject honoTestConnectionJsonObject; @Rule - public final ActorSystemResource actorSystemResource = - ActorSystemResource.newInstance(ConfigFactory.load("test")); + public final ActorSystemResource actorSystemResource = ActorSystemResource.newInstance(TEST_CONFIG); + + private HonoConfig honoConfig; - private DefaultHonoConnectionFactory underTest; + @BeforeClass + public static void beforeClass() throws IOException { + final var testClassLoader = DefaultHonoConnectionFactoryTest.class.getClassLoader(); + try (final var testConnectionJsonFileStreamReader = new InputStreamReader( + testClassLoader.getResourceAsStream("test-connection-hono.json") + )) { + honoTestConnectionJsonObject = JsonFactory.readFrom(testConnectionJsonFileStreamReader).asObject(); + } + } @Before - public void setup() { + public void before() { honoConfig = new DefaultHonoConfig(actorSystemResource.getActorSystem()); - dummyConnection = ConnectivityModelFactory.connectionFromJson(getResource("test-connection.json")); - underTest = DefaultHonoConnectionFactory.getInstance(actorSystemResource.getActorSystem(), dummyConnection); } @Test - public void testGetCredentials() { - final var EXPECTED_CREDENTIALS = honoConfig.getUserPasswordCredentials(); - assertEquals(EXPECTED_CREDENTIALS, underTest.getCredentials()); + public void newInstanceWithNullActorSystemThrowsException() { + Assertions.assertThatNullPointerException() + .isThrownBy(() -> DefaultHonoConnectionFactory.newInstance(null, Mockito.mock(Connection.class))) + .withMessage("The actorSystem must not be null!") + .withNoCause(); } @Test - public void testGetTenantId() { - var EXPECTED_TENANT_ID = ""; - assertEquals(EXPECTED_TENANT_ID, underTest.getTenantId()); + public void newInstanceWithNullConnectionThrowsException() { + Assertions.assertThatNullPointerException() + .isThrownBy(() -> DefaultHonoConnectionFactory.newInstance(actorSystemResource.getActorSystem(), null)) + .withMessage("The connection must not be null!") + .withNoCause(); } - private static JsonObject getResource(final String fileName) { - try (var resourceStream = DefaultHonoConnectionFactoryTest.class.getClassLoader().getResourceAsStream(fileName)) { - assert resourceStream != null; - return JsonObject.of(new String(resourceStream.readAllBytes(), StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new RuntimeException("Test resource not found: " + fileName); - } + @Test + public void getHonoConnectionReturnsExpected() { + final var userProvidedHonoConnection = + ConnectivityModelFactory.connectionFromJson(honoTestConnectionJsonObject); + + final var underTest = DefaultHonoConnectionFactory.newInstance(actorSystemResource.getActorSystem(), + userProvidedHonoConnection); + + assertThat(underTest.getHonoConnection()).isEqualTo(getExpectedHonoConnection(userProvidedHonoConnection)); + } + + private Connection getExpectedHonoConnection(final Connection originalConnection) { + final var sourcesByAddress = getSourcesByAddress(originalConnection.getSources()); + final var commandReplyTargetHeaderMapping = ConnectivityModelFactory.newHeaderMapping(Map.of( + "correlation-id", "{{ header:correlation-id }}", + "device_id", "{{ thing:id }}", + "subject", + "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response" + )); + final var targets = originalConnection.getTargets(); + final var basicAdditionalTargetHeaderMappingEntries = Map.of( + "device_id", "{{ thing:id }}", + "correlation-id", "{{ header:correlation-id }}", + "subject", "{{ header:subject | fn:default(topic:action-subject) }}" + ); + return ConnectivityModelFactory.newConnectionBuilder(originalConnection) + .uri(honoConfig.getBaseUri().toString()) + .validateCertificate(honoConfig.isValidateCertificates()) + .specificConfig(Map.of( + "saslMechanism", honoConfig.getSaslMechanism().toString(), + "bootstrapServers", TEST_CONFIG.getString(HonoConfig.PREFIX + ".bootstrap-servers"), + "groupId", originalConnection.getId().toString()) + ) + .setSources(List.of( + ConnectivityModelFactory.newSourceBuilder(sourcesByAddress.get(TELEMETRY.getAliasValue())) + .addresses(Set.of(getExpectedResolvedSourceAddress(TELEMETRY))) + .replyTarget(ReplyTarget.newBuilder() + .address(getExpectedResolvedCommandTargetAddress()) + .headerMapping(commandReplyTargetHeaderMapping) + .build()) + .build(), + ConnectivityModelFactory.newSourceBuilder(sourcesByAddress.get(EVENT.getAliasValue())) + .addresses(Set.of(getExpectedResolvedSourceAddress(EVENT))) + .replyTarget(ReplyTarget.newBuilder() + .address(getExpectedResolvedCommandTargetAddress()) + .headerMapping(commandReplyTargetHeaderMapping) + .build()) + .build(), + ConnectivityModelFactory.newSourceBuilder(sourcesByAddress.get(COMMAND_RESPONSE.getAliasValue())) + .addresses(Set.of(getExpectedResolvedSourceAddress(COMMAND_RESPONSE))) + .headerMapping(ConnectivityModelFactory.newHeaderMapping(Map.of( + "correlation-id", "{{ header:correlation-id }}", + "status", "{{ header:status }}" + ))) + .build() + )) + .setTargets(List.of( + ConnectivityModelFactory.newTargetBuilder(targets.get(0)) + .address(getExpectedResolvedCommandTargetAddress()) + .headerMapping(ConnectivityModelFactory.newHeaderMapping( + Stream.concat( + basicAdditionalTargetHeaderMappingEntries.entrySet().stream(), + Stream.of(Map.entry("response-required", "{{ header:response-required }}")) + ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + )) + .build(), + ConnectivityModelFactory.newTargetBuilder(targets.get(1)) + .address(getExpectedResolvedCommandTargetAddress()) + .headerMapping(ConnectivityModelFactory.newHeaderMapping( + basicAdditionalTargetHeaderMappingEntries + )) + .build() + )) + .credentials(honoConfig.getUserPasswordCredentials()) + .build(); + } + + private static Map getSourcesByAddress(final Iterable sources) { + final var result = new LinkedHashMap(); + sources.forEach(source -> source.getAddresses().forEach(address -> result.put(address, source))); + return result; + } + + private static String getExpectedResolvedSourceAddress(final HonoAddressAlias honoAddressAlias) { + return "hono." + honoAddressAlias.getAliasValue(); + } + + private static String getExpectedResolvedCommandTargetAddress() { + return "hono." + HonoAddressAlias.COMMAND.getAliasValue() + "/{{thing:id}}"; } -} \ No newline at end of file +} diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java deleted file mode 100644 index b7884eade8..0000000000 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactoryTest.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (c) 2022 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.connectivity.service.messaging.hono; - -import static org.junit.Assert.assertEquals; - -import java.io.IOException; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.eclipse.ditto.connectivity.model.Connection; -import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; -import org.eclipse.ditto.connectivity.model.HonoAddressAlias; -import org.eclipse.ditto.connectivity.model.Source; -import org.eclipse.ditto.connectivity.model.Target; -import org.eclipse.ditto.connectivity.model.Topic; -import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; -import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; -import org.eclipse.ditto.connectivity.service.config.HonoConfig; -import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; -import org.eclipse.ditto.json.JsonObject; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -import com.typesafe.config.ConfigFactory; - -@RunWith(MockitoJUnitRunner.class) -public class HonoConnectionFactoryTest { - - private static final String TEST_USERNAME = "test-username"; - private static final String TEST_PASSWORD = "test-password"; - private static final String TEST_TENANT_ID = "test-tenant-id"; - @Rule - public final ActorSystemResource actorSystemResource= - ActorSystemResource.newInstance(ConfigFactory.load("test")); - private Connection testConnection; - - private HonoConnectionFactory underTest; - - @Before - public void setup() { - honoConfig = new DefaultHonoConfig(actorSystemResource.getActorSystem()); - testConnection = ConnectivityModelFactory.connectionFromJson(getResource("test-connection.json")); - } - public final UserPasswordCredentials testCredentials = - UserPasswordCredentials.newInstance(TEST_USERNAME, TEST_PASSWORD); - - - private HonoConfig honoConfig; - - @Test - public void testEnrichConnectionWithoutTenantId() { - verifyConnection(true); - } - - @Test - public void testEnrichConnectionWithTenantId() { - verifyConnection(false); - } - - private void verifyConnection(boolean withTenantId) { - underTest = getInstance(withTenantId); - var expected = enrichTestConnection(); - assertEquals(expected, underTest.enrichConnection()); - } - - private HonoConnectionFactory getInstance(boolean withTenantId) { - if (withTenantId) { - return new HonoConnectionFactory(actorSystemResource.getActorSystem(), testConnection) { - @Override - protected UserPasswordCredentials getCredentials() { - return testCredentials; - } - - @Override - protected String getTenantId() { - return TEST_TENANT_ID; - } - }; - } else { - return new HonoConnectionFactory(actorSystemResource.getActorSystem(), testConnection) { - @Override - protected UserPasswordCredentials getCredentials() { - return testCredentials; - } - - @Override - protected String getTenantId() { - return ""; - } - }; - } - } - private static JsonObject getResource(final String fileName) { - try (var resourceStream = HonoConnectionFactoryTest.class.getClassLoader().getResourceAsStream(fileName)) { - assert resourceStream != null; - return JsonObject.of(new String(resourceStream.readAllBytes(), StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new RuntimeException("Test resource not found: " + fileName); - } - } - - private static String getBootstrapServerUrisAsCommaSeparatedListString(final HonoConfig honoConfig) { - return honoConfig.getBootstrapServerUris() - .stream() - .map(URI::toString) - .collect(Collectors.joining(",")); - } - - public Connection enrichTestConnection() { - final var connectionId = testConnection.getId(); - return ConnectivityModelFactory.newConnectionBuilder(testConnection) - .uri(honoConfig.getBaseUri().toString()) - .validateCertificate(honoConfig.isValidateCertificates()) - .specificConfig(Map.of( - "saslMechanism", honoConfig.getSaslMechanism().toString(), - "bootstrapServers", getBootstrapServerUrisAsCommaSeparatedListString(honoConfig), - "groupId", (underTest.getTenantId().isEmpty() ? "" : underTest.getTenantId() + "_") + connectionId) - ) - .credentials(underTest.getCredentials()) - .setSources(testConnection.getSources() - .stream() - .map(source -> resolveSourceAliases(source, underTest.getTenantId())) - .toList()) - .setTargets(testConnection.getTargets() - .stream() - .map(target -> resolveTargetAlias(target, underTest.getTenantId())) - .toList()) - .build(); - } - - @SuppressWarnings("unchecked") - private static Source resolveSourceAliases(final Source source, final String tenantId) { - final var sourceBuilder = ConnectivityModelFactory.newSourceBuilder(source) - .addresses(source.getAddresses() - .stream() - .map(HonoAddressAlias::forAliasValue) - .flatMap(Optional::stream) - .map(honoAddressAlias -> honoAddressAlias.resolveAddress(tenantId)) - .collect(Collectors.toSet())); - if (source.getAddresses().contains(HonoAddressAlias.TELEMETRY.getAliasValue()) - || source.getAddresses().contains(HonoAddressAlias.EVENT.getAliasValue())) { - source.getReplyTarget().ifPresent(replyTarget -> { - var headerMapping = replyTarget.getHeaderMapping().toJson() - .setValue("correlation-id", "{{ header:correlation-id }}"); - if (HonoAddressAlias.COMMAND.getAliasValue().equals(replyTarget.getAddress())) { - headerMapping = headerMapping - .setValue("device_id", "{{ thing:id }}") - .setValue("subject", - "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response"); - } - sourceBuilder.replyTarget(replyTarget.toBuilder() - .address(HonoAddressAlias.forAliasValue(replyTarget.getAddress()) - .map(honoAddressAlias -> honoAddressAlias.resolveAddressWithThingIdSuffix(tenantId)) - .orElseGet(replyTarget::getAddress)) - .headerMapping(ConnectivityModelFactory.newHeaderMapping(headerMapping)) - .build()); - }); - } - if (source.getAddresses().contains(HonoAddressAlias.COMMAND_RESPONSE.getAliasValue())) { - sourceBuilder.headerMapping(ConnectivityModelFactory - .newHeaderMapping(source.getHeaderMapping().toJson() - .setValue("correlation-id", "{{ header:correlation-id }}") - .setValue("status", "{{ header:status }}"))); - } - return sourceBuilder.build(); - } - - private static Target resolveTargetAlias(final Target target, final String tenantId) { - final var resolvedAddress = HonoAddressAlias.forAliasValue(target.getAddress()) - .map(honoAddressAlias -> honoAddressAlias.resolveAddressWithThingIdSuffix(tenantId)) - .orElseGet(target::getAddress); - final var targetBuilder = ConnectivityModelFactory.newTargetBuilder(target); - if (!resolvedAddress.isEmpty()) { - targetBuilder.address(resolvedAddress); - } - - var headerMapping = target.getHeaderMapping().toJson() - .setValue("device_id", "{{ thing:id }}") - .setValue("correlation-id", "{{ header:correlation-id }}") - .setValue("subject", "{{ header:subject | fn:default(topic:action-subject) }}"); - if (target.getTopics().stream() - .anyMatch(topic -> topic.getTopic() == Topic.LIVE_MESSAGES || - topic.getTopic() == Topic.LIVE_COMMANDS)) { - headerMapping = headerMapping.setValue("response-required", "{{ header:response-required }}"); - } - targetBuilder.headerMapping(ConnectivityModelFactory.newHeaderMapping(headerMapping)); - return targetBuilder.build(); - } - -} \ No newline at end of file diff --git a/connectivity/service/src/test/resources/test-connection.json b/connectivity/service/src/test/resources/test-connection-hono.json similarity index 100% rename from connectivity/service/src/test/resources/test-connection.json rename to connectivity/service/src/test/resources/test-connection-hono.json From 766b2dc3ae3084743b58229827a2203690a91394 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Wed, 10 Aug 2022 06:31:24 +0200 Subject: [PATCH 28/65] Reordered import. Signed-off-by: Juergen Fickel --- .../connectivity/service/messaging/hono/HonoValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java index 3c82e28408..5d864339b3 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidator.java @@ -25,7 +25,6 @@ import javax.annotation.concurrent.Immutable; import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.connectivity.api.placeholders.ConnectivityPlaceholders; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionConfigurationInvalidException; import org.eclipse.ditto.connectivity.model.ConnectionType; @@ -34,6 +33,7 @@ import org.eclipse.ditto.connectivity.model.Target; import org.eclipse.ditto.connectivity.service.config.ConnectivityConfig; import org.eclipse.ditto.connectivity.service.messaging.validation.AbstractProtocolValidator; +import org.eclipse.ditto.connectivity.service.placeholders.ConnectivityPlaceholders; import org.eclipse.ditto.placeholders.PlaceholderFactory; import akka.actor.ActorSystem; From 059f61923bdd468b6ae0014360fe3a1d1b8aa596 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Wed, 10 Aug 2022 07:22:08 +0200 Subject: [PATCH 29/65] Further simplified `HonoAddressAlias`. * Removed method `aliasValues` from `HonoAddressAlias` as it was only used in tests. * Removed method `forAliasValue` from `HonoAddressAlias` because it was only used in `HonoConnectionFactory`. * Adjusted `HonoAddressAliasTest` accordingly. * Added mapping from Hono address alias values to `HonoAddress` to `HonoConnectionFactory`. Signed-off-by: Juergen Fickel --- .../connectivity/model/HonoAddressAlias.java | 36 --------------- .../model/HonoAddressAliasTest.java | 45 +++---------------- .../messaging/hono/HonoConnectionFactory.java | 14 +++++- .../messaging/hono/HonoValidatorTest.java | 6 ++- 4 files changed, 22 insertions(+), 79 deletions(-) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 00d66341c7..1fedb660a5 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -12,13 +12,6 @@ */ package org.eclipse.ditto.connectivity.model; -import java.util.Locale; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Stream; - -import javax.annotation.Nullable; - /** * Possible address aliases used by connections of type 'Hono' */ @@ -50,35 +43,6 @@ private HonoAddressAlias(final String value) { this.value = value; } - /** - * Returns all defined HonoAddressAlias values. - * - * @return a stream with HonoAddressAlias values. - */ - public static Stream aliasValues() { - return Stream.of(values()).map(HonoAddressAlias::getAliasValue); - } - - /** - * Returns the HonoAddressAlias to which the given alias value is mapped. - * This method is fault-tolerant for its parameter to some degree: - *
    - *
  • it accepts {@code null},
  • - *
  • it trims white spaces and
  • - *
  • it converts the specified string to lower case.
  • - *
- * - * @param aliasValue the aliasValue of the supposed HonoAddressAlias. - * @return an Optional containing the HonoAddressAlias which matches {@code aliasValue} or an empty Optional if none - * matches. - */ - public static Optional forAliasValue(@Nullable final String aliasValue) { - return Stream.of(values()) - .filter(alias -> null != aliasValue && - Objects.equals(alias.getAliasValue(), aliasValue.trim().toLowerCase(Locale.ENGLISH))) - .findAny(); - } - /** * Gets the value of the alias. * diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java index dddcbabc4e..0abb75ff01 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java @@ -12,12 +12,7 @@ */ package org.eclipse.ditto.connectivity.model; -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.ditto.connectivity.model.HonoAddressAlias.COMMAND_RESPONSE; - -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.UUID; +import java.util.Locale; import org.assertj.core.api.AutoCloseableSoftAssertions; import org.junit.Test; @@ -28,42 +23,12 @@ public final class HonoAddressAliasTest { @Test - public void aliasValuesReturnsExpected() { - final HonoAddressAlias[] honoAddressAliases = HonoAddressAlias.values(); - final Collection expectedAliasValues = new LinkedHashSet<>(honoAddressAliases.length); - for (final HonoAddressAlias honoAddressAlias : honoAddressAliases) { - expectedAliasValues.add(honoAddressAlias.getAliasValue()); - } - assertThat(HonoAddressAlias.aliasValues()).hasSameElementsAs(expectedAliasValues); - } - - @Test - public void forAliasValueWithNullAliasValueReturnsEmptyOptional() { - assertThat(HonoAddressAlias.forAliasValue(null)).isEmpty(); - } - - @Test - public void forAliasValueWithUnknownAliasValueReturnsEmptyOptional() { - assertThat(HonoAddressAlias.forAliasValue(String.valueOf(UUID.randomUUID()))).isEmpty(); - } - - @Test - public void forAliasValueIsTolerantForSmallDiscrepancyInSpecifiedAliasValue() { - try (final AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { - softly.assertThat(HonoAddressAlias.forAliasValue(" TeLeMeTrY ")).hasValue(HonoAddressAlias.TELEMETRY); - softly.assertThat(HonoAddressAlias.forAliasValue(COMMAND_RESPONSE.name())) - .as(COMMAND_RESPONSE.name()) - .hasValue(COMMAND_RESPONSE); - } - } - - @Test - public void forAliasValueReturnsExpectedForKnownAliasValue() { + public void getAliasValueReturnsExpected() { try (final AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { for (final HonoAddressAlias honoAddressAlias : HonoAddressAlias.values()) { - softly.assertThat(HonoAddressAlias.forAliasValue(honoAddressAlias.getAliasValue())) - .as(honoAddressAlias.getAliasValue()) - .hasValue(honoAddressAlias); + softly.assertThat(honoAddressAlias.getAliasValue()) + .as(honoAddressAlias.name()) + .isEqualTo(honoAddressAlias.name().toLowerCase(Locale.ENGLISH)); } } } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index e6196481bd..7080dbf3d9 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -24,8 +24,10 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.concurrent.NotThreadSafe; @@ -57,6 +59,10 @@ */ public abstract class HonoConnectionFactory { + private static final Map HONO_ADDRESS_ALIASES_BY_ALIAS_VALUE = + Stream.of(HonoAddressAlias.values()) + .collect(Collectors.toUnmodifiableMap(HonoAddressAlias::getAliasValue, Function.identity())); + protected final Connection connection; /** @@ -130,11 +136,15 @@ private List getSources(final Collection originalSources) { private Set resolveSourceAddresses(final Collection unresolvedSourceAddresses) { return unresolvedSourceAddresses.stream() - .map(unresolvedSourceAddress -> HonoAddressAlias.forAliasValue(unresolvedSourceAddress) + .map(unresolvedSourceAddress -> getHonoAddressAliasByAliasValue(unresolvedSourceAddress) .map(this::resolveSourceAddress) .orElse(unresolvedSourceAddress)) .collect(Collectors.toCollection(LinkedHashSet::new)); } + + private static Optional getHonoAddressAliasByAliasValue(final String aliasValue) { + return Optional.ofNullable(HONO_ADDRESS_ALIASES_BY_ALIAS_VALUE.get(aliasValue)); + } protected abstract String resolveSourceAddress(HonoAddressAlias honoAddressAlias); @@ -161,7 +171,7 @@ private static boolean isApplyReplyTarget(final Collection sourceAddress } private String resolveTargetAddressOrKeepUnresolved(final String unresolvedTargetAddress) { - return HonoAddressAlias.forAliasValue(unresolvedTargetAddress) + return getHonoAddressAliasByAliasValue(unresolvedTargetAddress) .map(this::resolveTargetAddress) .orElse(unresolvedTargetAddress); } diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java index 1b698e855f..cabe43aaef 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java @@ -149,7 +149,11 @@ public void validateWithInvalidSourceAddressesThrowsException() { .hasMessageStartingWith("The provided source address <%s> is invalid." + " It should be one of the defined aliases: ", invalidSourceAddress) - .hasMessageContainingAll(HonoAddressAlias.aliasValues().toArray(CharSequence[]::new)) + .hasMessageContainingAll("Foo") + .hasMessageContainingAll(Stream.of(HonoAddressAlias.values()) + .filter(honoAddressAlias -> HonoAddressAlias.COMMAND !=honoAddressAlias) + .map(HonoAddressAlias::getAliasValue) + .toArray(CharSequence[]::new)) .hasNoCause() .isInstanceOfSatisfying(ConnectionConfigurationInvalidException.class, exception -> assertThat(exception.getDittoHeaders()).isEqualTo(dittoHeaders)) From 6e73c74e08b4b84e7db07016af87dda0102dc114 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Wed, 10 Aug 2022 09:02:05 +0200 Subject: [PATCH 30/65] Fixed unit test. Signed-off-by: Juergen Fickel --- .../connectivity/service/messaging/hono/HonoValidatorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java index cabe43aaef..7f9a09aaff 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java @@ -149,7 +149,6 @@ public void validateWithInvalidSourceAddressesThrowsException() { .hasMessageStartingWith("The provided source address <%s> is invalid." + " It should be one of the defined aliases: ", invalidSourceAddress) - .hasMessageContainingAll("Foo") .hasMessageContainingAll(Stream.of(HonoAddressAlias.values()) .filter(honoAddressAlias -> HonoAddressAlias.COMMAND !=honoAddressAlias) .map(HonoAddressAlias::getAliasValue) From b46e3332d6b4195b9d9afa014376f73be38a9783 Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Wed, 10 Aug 2022 11:37:28 +0200 Subject: [PATCH 31/65] Moved back method for getting `HonoAddressAlias` by alias value to the enum itself. Signed-off-by: Juergen Fickel --- .../connectivity/model/HonoAddressAlias.java | 26 ++++++++++ .../model/HonoAddressAliasTest.java | 51 ++++++++++++++++--- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 1fedb660a5..5dc0d74340 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -12,6 +12,15 @@ */ package org.eclipse.ditto.connectivity.model; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + /** * Possible address aliases used by connections of type 'Hono' */ @@ -37,12 +46,29 @@ public enum HonoAddressAlias { */ COMMAND_RESPONSE("command_response"); + private static final Map HONO_ADDRESS_ALIASES_BY_ALIAS_VALUE = + Collections.unmodifiableMap( + Stream.of(HonoAddressAlias.values()) + .collect(Collectors.toMap(HonoAddressAlias::getAliasValue, Function.identity())) + ); + private final String value; private HonoAddressAlias(final String value) { this.value = value; } + /** + * Returns the HonoAddressAlias to which the given alias value is mapped. + * + * @param aliasValue the aliasValue of the supposed HonoAddressAlias. + * @return an Optional containing the HonoAddressAlias which matches {@code aliasValue} or an empty Optional if none + * matches. + */ + public static Optional forAliasValue(@Nullable final String aliasValue) { + return Optional.ofNullable(HONO_ADDRESS_ALIASES_BY_ALIAS_VALUE.get(aliasValue)); + } + /** * Gets the value of the alias. * diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java index 0abb75ff01..2d97de4dad 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoAddressAliasTest.java @@ -12,9 +12,14 @@ */ package org.eclipse.ditto.connectivity.model; +import static org.assertj.core.api.Assertions.assertThat; + import java.util.Locale; +import java.util.UUID; +import java.util.stream.Stream; -import org.assertj.core.api.AutoCloseableSoftAssertions; +import org.assertj.core.api.JUnitSoftAssertions; +import org.junit.Rule; import org.junit.Test; /** @@ -22,15 +27,47 @@ */ public final class HonoAddressAliasTest { + @Rule + public final JUnitSoftAssertions softly = new JUnitSoftAssertions(); + + @Test + public void forAliasValueWithNullAliasValueReturnsEmptyOptional() { + assertThat(HonoAddressAlias.forAliasValue(null)).isEmpty(); + } + + @Test + public void forAliasValueWithUnknownAliasValueReturnsEmptyOptional() { + assertThat(HonoAddressAlias.forAliasValue(String.valueOf(UUID.randomUUID()))).isEmpty(); + } + @Test public void getAliasValueReturnsExpected() { - try (final AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) { - for (final HonoAddressAlias honoAddressAlias : HonoAddressAlias.values()) { - softly.assertThat(honoAddressAlias.getAliasValue()) - .as(honoAddressAlias.name()) - .isEqualTo(honoAddressAlias.name().toLowerCase(Locale.ENGLISH)); - } + for (final HonoAddressAlias honoAddressAlias : HonoAddressAlias.values()) { + softly.assertThat(honoAddressAlias.getAliasValue()) + .as(honoAddressAlias.name()) + .isEqualTo(honoAddressAlias.name().toLowerCase(Locale.ENGLISH)); } } + @Test + public void forAliasValueReturnsExpectedForKnownAliasValue() { + for (final HonoAddressAlias honoAddressAlias : HonoAddressAlias.values()) { + softly.assertThat(HonoAddressAlias.forAliasValue(honoAddressAlias.getAliasValue())) + .as(honoAddressAlias.getAliasValue()) + .hasValue(honoAddressAlias); + } + } + + @Test + public void forInvalidAliasValueReturnsEmptyOptional() { + Stream.concat( + Stream.of(HonoAddressAlias.values()).map(HonoAddressAlias::name), + Stream.of("Telemetry", " command") + ).forEach(invalidAliasValue -> { + softly.assertThat(HonoAddressAlias.forAliasValue(invalidAliasValue)) + .as(invalidAliasValue) + .isEmpty();; + }); + } + } \ No newline at end of file From 2c01aef819f27b7680cdef44214e5dde1ba4b7da Mon Sep 17 00:00:00 2001 From: Juergen Fickel Date: Wed, 10 Aug 2022 14:52:50 +0200 Subject: [PATCH 32/65] Made `HonoConnectionFactory` a `DittoExtensionPoint`. Signed-off-by: Juergen Fickel --- .../DefaultClientActorPropsFactory.java | 21 ++-- .../hono/DefaultHonoConnectionFactory.java | 22 ++--- .../messaging/hono/HonoConnectionFactory.java | 98 ++++++++++++++----- .../src/main/resources/connectivity.conf | 2 + .../DefaultHonoConnectionFactoryTest.java | 18 +--- .../service/src/test/resources/test.conf | 1 + 6 files changed, 101 insertions(+), 61 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java index dd21b01dfa..14bc5a34f1 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactory.java @@ -14,10 +14,11 @@ import javax.annotation.concurrent.Immutable; +import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; import org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpClientActor; -import org.eclipse.ditto.connectivity.service.messaging.hono.DefaultHonoConnectionFactory; +import org.eclipse.ditto.connectivity.service.messaging.hono.HonoConnectionFactory; import org.eclipse.ditto.connectivity.service.messaging.httppush.HttpPushClientActor; import org.eclipse.ditto.connectivity.service.messaging.kafka.KafkaClientActor; import org.eclipse.ditto.connectivity.service.messaging.mqtt.hivemq.MqttClientActor; @@ -36,8 +37,10 @@ @Immutable public final class DefaultClientActorPropsFactory implements ClientActorPropsFactory { + private final HonoConnectionFactory honoConnectionFactory; + public DefaultClientActorPropsFactory(final ActorSystem actorSystem, final Config config) { - super(); + honoConnectionFactory = HonoConnectionFactory.get(actorSystem, config); } @Override @@ -75,7 +78,7 @@ public Props getActorPropsForType(final Connection connection, connectionActor, dittoHeaders, connectivityConfigOverwrites); - case HONO -> KafkaClientActor.props(getResolvedHonoConnectionOrThrow(actorSystem, connection), + case HONO -> KafkaClientActor.props(getResolvedHonoConnectionOrThrow(connection, dittoHeaders), commandForwarderActor, connectionActor, dittoHeaders, @@ -83,12 +86,12 @@ public Props getActorPropsForType(final Connection connection, }; } - private static Connection getResolvedHonoConnectionOrThrow( - final ActorSystem actorSystem, - final Connection connection - ) { - final var honoConnectionFactory = DefaultHonoConnectionFactory.newInstance(actorSystem, connection); - return honoConnectionFactory.getHonoConnection(); + private Connection getResolvedHonoConnectionOrThrow(final Connection connection, final DittoHeaders dittoHeaders) { + try { + return honoConnectionFactory.getHonoConnection(connection); + } catch (final DittoRuntimeException e) { + throw e.setDittoHeaders(dittoHeaders); + } } } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java index 745527a106..516150057c 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java @@ -17,12 +17,13 @@ import java.util.Set; import org.eclipse.ditto.connectivity.model.Connection; -import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.HonoAddressAlias; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; import org.eclipse.ditto.connectivity.service.config.HonoConfig; +import com.typesafe.config.Config; + import akka.actor.ActorSystem; /** @@ -35,22 +36,15 @@ public final class DefaultHonoConnectionFactory extends HonoConnectionFactory { private final HonoConfig honoConfig; - private DefaultHonoConnectionFactory(final HonoConfig honoConfig, final Connection connection) { - super(connection); - this.honoConfig = honoConfig; - } - /** - * Returns a new instance of {@code DefaultHonoConnectionFactory} for the specified arguments. + * Constructs a {@code DefaultHonoConnectionFactory} for the specified arguments. * - * @param actorSystem the actor system that is used to obtain the HonoConfig. - * @param connection the connection that serves as base for the Hono connection this factory returns. - * @return the instance. - * @throws NullPointerException if any argument is {@code null}. - * @throws IllegalArgumentException if the type of {@code connection} is not {@link ConnectionType#HONO}; + * @param actorSystem the actor system in which to load the factory. + * @param config configuration properties for this factory. + * @throws NullPointerException if {@code actorSystem} is {@code null}. */ - public static DefaultHonoConnectionFactory newInstance(final ActorSystem actorSystem, final Connection connection) { - return new DefaultHonoConnectionFactory(new DefaultHonoConfig(actorSystem), connection); + public DefaultHonoConnectionFactory(final ActorSystem actorSystem, final Config config) { + honoConfig = new DefaultHonoConfig(actorSystem); } @Override diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index 7080dbf3d9..1696592552 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -24,10 +24,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.concurrent.NotThreadSafe; @@ -43,6 +41,12 @@ import org.eclipse.ditto.connectivity.model.Topic; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.connectivity.service.config.HonoConfig; +import org.eclipse.ditto.internal.utils.extension.DittoExtensionIds; +import org.eclipse.ditto.internal.utils.extension.DittoExtensionPoint; + +import com.typesafe.config.Config; + +import akka.actor.ActorSystem; /** * Base implementation of a factory for getting a Hono {@link Connection}. @@ -57,52 +61,85 @@ * * @since 3.0.0 */ -public abstract class HonoConnectionFactory { +public abstract class HonoConnectionFactory implements DittoExtensionPoint { - private static final Map HONO_ADDRESS_ALIASES_BY_ALIAS_VALUE = - Stream.of(HonoAddressAlias.values()) - .collect(Collectors.toUnmodifiableMap(HonoAddressAlias::getAliasValue, Function.identity())); + /** + * Constructs a {@code HonoConnectionFactory}. + */ + protected HonoConnectionFactory() { + super(); + } - protected final Connection connection; + /** + * Loads the implementation of {@code HonoConnectionFactory} which is configured for the specified + * {@code ActorSystem}. + * + * @param actorSystem the actorSystem in which the {@code HonoConnectionFactory} should be loaded. + * @param config the configuration for this extension. + * @return the {@code HonoConnectionFactory} implementation. + * @throws NullPointerException if any argument is {@code null}. + */ + public static HonoConnectionFactory get(final ActorSystem actorSystem, final Config config) { + checkNotNull(actorSystem, "actorSystem"); + checkNotNull(config, "config"); + + return DittoExtensionIds.get(actorSystem) + .computeIfAbsent( + DittoExtensionPoint.ExtensionId.ExtensionIdConfig.of(HonoConnectionFactory.class, + config, + ExtensionId.CONFIG_KEY), + ExtensionId::new + ) + .get(actorSystem); + } /** - * Constructs a {@code HonoConnectionFactory}. + * Returns a proper Hono Connection for the Connection that was used to create this factory instance. * * @param connection the connection that serves as base for the Hono connection this factory returns. + * @return the Hono Connection. * @throws NullPointerException if {@code connection} is {@code null}. * @throws IllegalArgumentException if the type of {@code connection} is not {@link ConnectionType#HONO}; + * @throws org.eclipse.ditto.base.model.exceptions.DittoRuntimeException if converting {@code connection} to a + * Hono connection failed for some reason. */ - protected HonoConnectionFactory(final Connection connection) { - this.connection = checkArgument( + public Connection getHonoConnection(final Connection connection) { + checkArgument( checkNotNull(connection, "connection"), arg -> ConnectionType.HONO == arg.getConnectionType(), () -> MessageFormat.format("Expected type of connection to be <{0}> but it was <{1}>.", ConnectionType.HONO, connection.getConnectionType()) ); - } - /** - * Returns a proper Hono Connection for the Connection that was used to create this factory instance. - * - * @return the Hono Connection. - */ - public Connection getHonoConnection() { + preConversion(connection); + return ConnectivityModelFactory.newConnectionBuilder(connection) .uri(String.valueOf(getBaseUri())) .validateCertificate(isValidateCertificates()) - .specificConfig(getSpecificConfig()) + .specificConfig(getSpecificConfig(connection)) .credentials(getCredentials()) .setSources(getSources(connection.getSources())) .setTargets(getTargets(connection.getTargets())) .build(); } + /** + * User overridable callback. + * This method is called before the actual conversion of the specified {@code Connection} is performed. + * Empty default implementation. + * + * @param honoConnection the connection that is guaranteed to have type {@link ConnectionType#HONO}. + */ + protected void preConversion(final Connection honoConnection) { + // Do nothing by default. + } + protected abstract URI getBaseUri(); protected abstract boolean isValidateCertificates(); - private Map getSpecificConfig() { + private Map getSpecificConfig(final Connection connection) { return Map.of( "saslMechanism", String.valueOf(getSaslMechanism()), "bootstrapServers", getAsCommaSeparatedListString(getBootstrapServerUris()), @@ -136,15 +173,11 @@ private List getSources(final Collection originalSources) { private Set resolveSourceAddresses(final Collection unresolvedSourceAddresses) { return unresolvedSourceAddresses.stream() - .map(unresolvedSourceAddress -> getHonoAddressAliasByAliasValue(unresolvedSourceAddress) + .map(unresolvedSourceAddress -> HonoAddressAlias.forAliasValue(unresolvedSourceAddress) .map(this::resolveSourceAddress) .orElse(unresolvedSourceAddress)) .collect(Collectors.toCollection(LinkedHashSet::new)); } - - private static Optional getHonoAddressAliasByAliasValue(final String aliasValue) { - return Optional.ofNullable(HONO_ADDRESS_ALIASES_BY_ALIAS_VALUE.get(aliasValue)); - } protected abstract String resolveSourceAddress(HonoAddressAlias honoAddressAlias); @@ -171,7 +204,7 @@ private static boolean isApplyReplyTarget(final Collection sourceAddress } private String resolveTargetAddressOrKeepUnresolved(final String unresolvedTargetAddress) { - return getHonoAddressAliasByAliasValue(unresolvedTargetAddress) + return HonoAddressAlias.forAliasValue(unresolvedTargetAddress) .map(this::resolveTargetAddress) .orElse(unresolvedTargetAddress); } @@ -241,6 +274,21 @@ private static boolean isPutResponseRequiredHeaderMapping(final Collection { + + private static final String CONFIG_KEY = "hono-connection-factory"; + + private ExtensionId(final ExtensionIdConfig extensionIdConfig) { + super(extensionIdConfig); + } + + @Override + protected String getConfigKey() { + return CONFIG_KEY; + } + + } + @NotThreadSafe private static final class HeaderMappingBuilder { diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index faebf85768..985669cc82 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -13,6 +13,8 @@ ditto { connection-priority-provider-factory = "org.eclipse.ditto.connectivity.service.messaging.persistence.UsageBasedPriorityProviderFactory" # Factory for custom client actor props. client-actor-props-factory = "org.eclipse.ditto.connectivity.service.messaging.DefaultClientActorPropsFactory" + # Factory for getting a fully-fledged Hono factory for a particular base connection. + hono-connection-factory = "org.eclipse.ditto.connectivity.service.messaging.hono.DefaultHonoConnectionFactory" # Extension for custom message mapper behaviour message-mapper-extension = "org.eclipse.ditto.connectivity.service.mapping.NoOpMessageMapperExtension" diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java index 32cfc4ac94..915f68b729 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java @@ -41,7 +41,6 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; -import org.mockito.Mockito; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; @@ -78,28 +77,21 @@ public void before() { @Test public void newInstanceWithNullActorSystemThrowsException() { Assertions.assertThatNullPointerException() - .isThrownBy(() -> DefaultHonoConnectionFactory.newInstance(null, Mockito.mock(Connection.class))) + .isThrownBy(() -> new DefaultHonoConnectionFactory(null, ConfigFactory.empty())) .withMessage("The actorSystem must not be null!") .withNoCause(); } - @Test - public void newInstanceWithNullConnectionThrowsException() { - Assertions.assertThatNullPointerException() - .isThrownBy(() -> DefaultHonoConnectionFactory.newInstance(actorSystemResource.getActorSystem(), null)) - .withMessage("The connection must not be null!") - .withNoCause(); - } - @Test public void getHonoConnectionReturnsExpected() { final var userProvidedHonoConnection = ConnectivityModelFactory.connectionFromJson(honoTestConnectionJsonObject); - final var underTest = DefaultHonoConnectionFactory.newInstance(actorSystemResource.getActorSystem(), - userProvidedHonoConnection); + final var underTest = + new DefaultHonoConnectionFactory(actorSystemResource.getActorSystem(), ConfigFactory.empty()); - assertThat(underTest.getHonoConnection()).isEqualTo(getExpectedHonoConnection(userProvidedHonoConnection)); + assertThat(underTest.getHonoConnection(userProvidedHonoConnection)) + .isEqualTo(getExpectedHonoConnection(userProvidedHonoConnection)); } private Connection getExpectedHonoConnection(final Connection originalConnection) { diff --git a/connectivity/service/src/test/resources/test.conf b/connectivity/service/src/test/resources/test.conf index d0b03e06ff..62a12afc35 100644 --- a/connectivity/service/src/test/resources/test.conf +++ b/connectivity/service/src/test/resources/test.conf @@ -23,6 +23,7 @@ ditto { updated-connection-tester = org.eclipse.ditto.connectivity.service.messaging.persistence.AlwaysFailingUpdatedConnectionTester connection-priority-provider-factory = org.eclipse.ditto.connectivity.service.messaging.persistence.UsageBasedPriorityProviderFactory client-actor-props-factory = org.eclipse.ditto.connectivity.service.messaging.DefaultClientActorPropsFactory + hono-connection-factory = "org.eclipse.ditto.connectivity.service.messaging.hono.DefaultHonoConnectionFactory" message-mapper-extension = "org.eclipse.ditto.connectivity.service.mapping.NoOpMessageMapperExtension" signal-enrichment-provider { extension-class = org.eclipse.ditto.connectivity.service.mapping.DefaultConnectivitySignalEnrichmentProvider From 0bfe49051f56fd513f27cc2c658d4cf97c2da4ff Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Fri, 12 Aug 2022 17:40:53 +0300 Subject: [PATCH 33/65] newInstance() method added to UserPasswordCredentials class Signed-off-by: Andrey Balarev --- .../connectivity/model/UserPasswordCredentials.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java index a2654e3ad7..220c28e026 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java @@ -110,6 +110,16 @@ public static UserPasswordCredentials newInstance(final String username, final S return new UserPasswordCredentials(username, password); } + /** + * Create credentials from JsonObject + * + * @param jsonObject the jsonObject + * @return credentials. + */ + public static UserPasswordCredentials newInstance(final JsonObject jsonObject) { + return UserPasswordCredentials.fromJson(jsonObject); + } + /** * JSON field definitions. */ From 69ee5af5cefa54c3c760110cd2be4d9a288c173f Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 31 Aug 2022 19:09:33 +0300 Subject: [PATCH 34/65] replyTargetEnabled bug in ImmutableSource fixed --- .../org/eclipse/ditto/connectivity/model/ImmutableSource.java | 1 + .../service/messaging/hono/HonoConnectionFactory.java | 1 + 2 files changed, 2 insertions(+) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableSource.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableSource.java index 91d8b10bc4..1ef25b6783 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableSource.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableSource.java @@ -420,6 +420,7 @@ public Builder(final Source source) { .qos(source.getQos().orElse(null)) .acknowledgementRequests(source.getAcknowledgementRequests().orElse(null)) .replyTarget(source.getReplyTarget().orElse(null)) + .replyTargetEnabled(source.isReplyTargetEnabled()) .declaredAcknowledgementLabels(source.getDeclaredAcknowledgementLabels()) .payloadMapping(source.getPayloadMapping()) .index(source.getIndex()) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index 1696592552..13ad36547e 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -161,6 +161,7 @@ private static String getAsCommaSeparatedListString(final Collection uris) protected abstract UserPasswordCredentials getCredentials(); + @SuppressWarnings("unchecked") private List getSources(final Collection originalSources) { return originalSources.stream() .map(originalSource -> ConnectivityModelFactory.newSourceBuilder(originalSource) From 136af983f7bcf7cf897493f9e9205703ea4ae2f5 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Fri, 2 Sep 2022 14:48:27 +0300 Subject: [PATCH 35/65] Custom `headerMappings` and custom `groupId` in `specificConfig` allowed. Messaging credentials embedded in URI. `originalAddress` resolved too. Signed-off-by: Andrey Balarev --- .../messaging/hono/HonoConnectionFactory.java | 119 +++---- .../DefaultHonoConnectionFactoryTest.java | 46 ++- .../hono-connection-custom-expected.json | 307 ++++++++++++++++++ .../hono-connection-custom-test.json | 295 +++++++++++++++++ ...json => hono-connection-default-test.json} | 3 - 5 files changed, 696 insertions(+), 74 deletions(-) create mode 100644 connectivity/service/src/test/resources/hono-connection-custom-expected.json create mode 100644 connectivity/service/src/test/resources/hono-connection-custom-test.json rename connectivity/service/src/test/resources/{test-connection-hono.json => hono-connection-default-test.json} (98%) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index 13ad36547e..613795d275 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -115,12 +115,11 @@ public Connection getHonoConnection(final Connection connection) { preConversion(connection); return ConnectivityModelFactory.newConnectionBuilder(connection) - .uri(String.valueOf(getBaseUri())) + .uri(combineUriWithCredentials(String.valueOf(getBaseUri()), getCredentials())) .validateCertificate(isValidateCertificates()) - .specificConfig(getSpecificConfig(connection)) - .credentials(getCredentials()) - .setSources(getSources(connection.getSources())) - .setTargets(getTargets(connection.getTargets())) + .specificConfig(makeupSpecificConfig(connection)) + .setSources(makeupSources(connection.getSources())) + .setTargets(makeupTargets(connection.getTargets())) .build(); } @@ -139,30 +138,41 @@ protected void preConversion(final Connection honoConnection) { protected abstract boolean isValidateCertificates(); - private Map getSpecificConfig(final Connection connection) { + protected abstract HonoConfig.SaslMechanism getSaslMechanism(); + + protected abstract Set getBootstrapServerUris(); + + protected abstract String getGroupId(Connection connection); + + protected abstract UserPasswordCredentials getCredentials(); + + protected abstract String resolveSourceAddress(HonoAddressAlias honoAddressAlias); + + protected abstract String resolveTargetAddress(HonoAddressAlias honoAddressAlias); + + private String combineUriWithCredentials(final String uri, final UserPasswordCredentials credentials) { + return uri.replaceFirst("(\\S+://)(\\S+)", + "$1" + credentials.getUsername() + ":" + credentials.getPassword() + "@$2"); + } + + private Map makeupSpecificConfig(final Connection connection) { + var groupId = Optional.ofNullable( + connection.getSpecificConfig().get("groupId")).orElse(getGroupId(connection)); return Map.of( "saslMechanism", String.valueOf(getSaslMechanism()), "bootstrapServers", getAsCommaSeparatedListString(getBootstrapServerUris()), - "groupId", getGroupId(connection) + "groupId", groupId ); } - protected abstract HonoConfig.SaslMechanism getSaslMechanism(); - - protected abstract Set getBootstrapServerUris(); - private static String getAsCommaSeparatedListString(final Collection uris) { return uris.stream() .map(URI::toString) .collect(Collectors.joining(",")); } - protected abstract String getGroupId(Connection connection); - - protected abstract UserPasswordCredentials getCredentials(); - @SuppressWarnings("unchecked") - private List getSources(final Collection originalSources) { + private List makeupSources(final Collection originalSources) { return originalSources.stream() .map(originalSource -> ConnectivityModelFactory.newSourceBuilder(originalSource) .addresses(resolveSourceAddresses(originalSource.getAddresses())) @@ -172,6 +182,16 @@ private List getSources(final Collection originalSources) { .collect(Collectors.toList()); } + private List makeupTargets(final Collection originalTargets) { + return originalTargets.stream() + .map(originalTarget -> ConnectivityModelFactory.newTargetBuilder(originalTarget) + .address(resolveTargetAddressOrKeepUnresolved(originalTarget.getAddress())) + .originalAddress(resolveTargetAddressOrKeepUnresolved(originalTarget.getOriginalAddress())) + .headerMapping(getTargetHeaderMapping(originalTarget)) + .build()) + .collect(Collectors.toList()); + } + private Set resolveSourceAddresses(final Collection unresolvedSourceAddresses) { return unresolvedSourceAddresses.stream() .map(unresolvedSourceAddress -> HonoAddressAlias.forAliasValue(unresolvedSourceAddress) @@ -180,7 +200,11 @@ private Set resolveSourceAddresses(final Collection unresolvedSo .collect(Collectors.toCollection(LinkedHashSet::new)); } - protected abstract String resolveSourceAddress(HonoAddressAlias honoAddressAlias); + private String resolveTargetAddressOrKeepUnresolved(final String unresolvedTargetAddress) { + return HonoAddressAlias.forAliasValue(unresolvedTargetAddress) + .map(this::resolveTargetAddress) + .orElse(unresolvedTargetAddress); + } private Optional getReplyTargetForSource(final Source source) { final Optional result; @@ -196,22 +220,6 @@ private Optional getReplyTargetForSource(final Source source) { return result; } - private static boolean isApplyReplyTarget(final Collection sourceAddresses) { - final Predicate isTelemetryHonoAddressAlias = HonoAddressAlias.TELEMETRY.getAliasValue()::equals; - final Predicate isEventHonoAddressAlias = HonoAddressAlias.EVENT.getAliasValue()::equals; - - return sourceAddresses.stream() - .anyMatch(isTelemetryHonoAddressAlias.or(isEventHonoAddressAlias)); - } - - private String resolveTargetAddressOrKeepUnresolved(final String unresolvedTargetAddress) { - return HonoAddressAlias.forAliasValue(unresolvedTargetAddress) - .map(this::resolveTargetAddress) - .orElse(unresolvedTargetAddress); - } - - protected abstract String resolveTargetAddress(HonoAddressAlias honoAddressAlias); - private static HeaderMapping getReplyTargetHeaderMapping(final ReplyTarget replyTarget) { final var headerMappingBuilder = HeaderMappingBuilder.of(replyTarget.getHeaderMapping()); headerMappingBuilder.putCorrelationId(); @@ -224,10 +232,6 @@ private static HeaderMapping getReplyTargetHeaderMapping(final ReplyTarget reply return headerMappingBuilder.build(); } - private static boolean isCommandHonoAddressAlias(final String replyTargetAddress) { - return replyTargetAddress.equals(HonoAddressAlias.COMMAND.getAliasValue()); - } - private static HeaderMapping getSourceHeaderMapping(final Source source) { final HeaderMapping result; if (isAdjustSourceHeaderMapping(source.getAddresses())) { @@ -241,32 +245,35 @@ private static HeaderMapping getSourceHeaderMapping(final Source source) { return result; } - private static boolean isAdjustSourceHeaderMapping(final Collection sourceAddresses) { - return sourceAddresses.contains(HonoAddressAlias.COMMAND_RESPONSE.getAliasValue()); - } - - private List getTargets(final Collection originalTargets) { - return originalTargets.stream() - .map(originalTarget -> ConnectivityModelFactory.newTargetBuilder(originalTarget) - .address(resolveTargetAddressOrKeepUnresolved(originalTarget.getAddress())) - .headerMapping(getTargetHeaderMapping(originalTarget)) - .build()) - .collect(Collectors.toList()); - } - private static HeaderMapping getTargetHeaderMapping(final Target target) { final var headerMappingBuilder = HeaderMappingBuilder.of(target.getHeaderMapping()) .putDeviceId() .putCorrelationId() .putSubject("{{ header:subject | fn:default(topic:action-subject) }}"); - if (isPutResponseRequiredHeaderMapping(target.getTopics())) { + if (isResponseRequiredHeaderMapping(target.getTopics())) { headerMappingBuilder.putEntry("response-required", "{{ header:response-required }}"); } return headerMappingBuilder.build(); } - private static boolean isPutResponseRequiredHeaderMapping(final Collection targetTopics) { + private static boolean isApplyReplyTarget(final Collection sourceAddresses) { + final Predicate isTelemetryHonoAddressAlias = HonoAddressAlias.TELEMETRY.getAliasValue()::equals; + final Predicate isEventHonoAddressAlias = HonoAddressAlias.EVENT.getAliasValue()::equals; + + return sourceAddresses.stream() + .anyMatch(isTelemetryHonoAddressAlias.or(isEventHonoAddressAlias)); + } + + private static boolean isCommandHonoAddressAlias(final String replyTargetAddress) { + return replyTargetAddress.equals(HonoAddressAlias.COMMAND.getAliasValue()); + } + + private static boolean isAdjustSourceHeaderMapping(final Collection sourceAddresses) { + return sourceAddresses.contains(HonoAddressAlias.COMMAND_RESPONSE.getAliasValue()); + } + + private static boolean isResponseRequiredHeaderMapping(final Collection targetTopics) { final Predicate isLiveMessages = topic -> Topic.LIVE_MESSAGES == topic; final Predicate isLiveCommands = topic -> Topic.LIVE_COMMANDS == topic; @@ -304,22 +311,22 @@ static HeaderMappingBuilder of(final HeaderMapping existingHeaderMapping) { } HeaderMappingBuilder putCorrelationId() { - headerMappingDefinition.put("correlation-id", "{{ header:correlation-id }}"); + headerMappingDefinition.putIfAbsent("correlation-id", "{{ header:correlation-id }}"); return this; } HeaderMappingBuilder putDeviceId() { - headerMappingDefinition.put("device_id", "{{ thing:id }}"); + headerMappingDefinition.putIfAbsent("device_id", "{{ thing:id }}"); return this; } HeaderMappingBuilder putSubject(final String subjectValue) { - headerMappingDefinition.put("subject", subjectValue); + headerMappingDefinition.putIfAbsent("subject", subjectValue); return this; } HeaderMappingBuilder putEntry(final String key, final String value) { - headerMappingDefinition.put(key, value); + headerMappingDefinition.putIfAbsent(key, value); return this; } diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java index 915f68b729..6a72dd5017 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactoryTest.java @@ -36,9 +36,7 @@ import org.eclipse.ditto.connectivity.service.config.HonoConfig; import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonObject; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -52,20 +50,18 @@ public final class DefaultHonoConnectionFactoryTest { private static final Config TEST_CONFIG = ConfigFactory.load("test"); - private static JsonObject honoTestConnectionJsonObject; - @Rule public final ActorSystemResource actorSystemResource = ActorSystemResource.newInstance(TEST_CONFIG); private HonoConfig honoConfig; - @BeforeClass - public static void beforeClass() throws IOException { + private static Connection generateConnectionObjectFromJsonFile( String fileName) throws IOException { final var testClassLoader = DefaultHonoConnectionFactoryTest.class.getClassLoader(); - try (final var testConnectionJsonFileStreamReader = new InputStreamReader( - testClassLoader.getResourceAsStream("test-connection-hono.json") + try (final var connectionJsonFileStreamReader = new InputStreamReader( + testClassLoader.getResourceAsStream(fileName) )) { - honoTestConnectionJsonObject = JsonFactory.readFrom(testConnectionJsonFileStreamReader).asObject(); + return ConnectivityModelFactory.connectionFromJson( + JsonFactory.readFrom(connectionJsonFileStreamReader).asObject()); } } @@ -83,9 +79,22 @@ public void newInstanceWithNullActorSystemThrowsException() { } @Test - public void getHonoConnectionReturnsExpected() { + public void getHonoConnectionWithCustomMappingsReturnsExpected() throws IOException { + final var userProvidedHonoConnection = + generateConnectionObjectFromJsonFile("hono-connection-custom-test.json"); + final var expectedHonoConnection = + generateConnectionObjectFromJsonFile("hono-connection-custom-expected.json"); + + final var underTest = + new DefaultHonoConnectionFactory(actorSystemResource.getActorSystem(), ConfigFactory.empty()); + + assertThat(underTest.getHonoConnection(userProvidedHonoConnection)).isEqualTo(expectedHonoConnection); + } + + @Test + public void getHonoConnectionWithDefaultMappingReturnsExpected() throws IOException { final var userProvidedHonoConnection = - ConnectivityModelFactory.connectionFromJson(honoTestConnectionJsonObject); + generateConnectionObjectFromJsonFile("hono-connection-default-test.json"); final var underTest = new DefaultHonoConnectionFactory(actorSystemResource.getActorSystem(), ConfigFactory.empty()); @@ -94,6 +103,7 @@ public void getHonoConnectionReturnsExpected() { .isEqualTo(getExpectedHonoConnection(userProvidedHonoConnection)); } + @SuppressWarnings("unchecked") private Connection getExpectedHonoConnection(final Connection originalConnection) { final var sourcesByAddress = getSourcesByAddress(originalConnection.getSources()); final var commandReplyTargetHeaderMapping = ConnectivityModelFactory.newHeaderMapping(Map.of( @@ -109,7 +119,10 @@ private Connection getExpectedHonoConnection(final Connection originalConnection "subject", "{{ header:subject | fn:default(topic:action-subject) }}" ); return ConnectivityModelFactory.newConnectionBuilder(originalConnection) - .uri(honoConfig.getBaseUri().toString()) + .uri(honoConfig.getBaseUri().toString().replaceFirst("(\\S+://)(\\S+)", + "$1" + honoConfig.getUserPasswordCredentials().getUsername() + + ":" + honoConfig.getUserPasswordCredentials().getPassword() + + "@$2")) .validateCertificate(honoConfig.isValidateCertificates()) .specificConfig(Map.of( "saslMechanism", honoConfig.getSaslMechanism().toString(), @@ -131,7 +144,8 @@ private Connection getExpectedHonoConnection(final Connection originalConnection .headerMapping(commandReplyTargetHeaderMapping) .build()) .build(), - ConnectivityModelFactory.newSourceBuilder(sourcesByAddress.get(COMMAND_RESPONSE.getAliasValue())) + ConnectivityModelFactory.newSourceBuilder( + sourcesByAddress.get(COMMAND_RESPONSE.getAliasValue())) .addresses(Set.of(getExpectedResolvedSourceAddress(COMMAND_RESPONSE))) .headerMapping(ConnectivityModelFactory.newHeaderMapping(Map.of( "correlation-id", "{{ header:correlation-id }}", @@ -142,21 +156,23 @@ private Connection getExpectedHonoConnection(final Connection originalConnection .setTargets(List.of( ConnectivityModelFactory.newTargetBuilder(targets.get(0)) .address(getExpectedResolvedCommandTargetAddress()) + .originalAddress(getExpectedResolvedCommandTargetAddress()) .headerMapping(ConnectivityModelFactory.newHeaderMapping( Stream.concat( basicAdditionalTargetHeaderMappingEntries.entrySet().stream(), - Stream.of(Map.entry("response-required", "{{ header:response-required }}")) + Stream.of(Map.entry("response-required", + "{{ header:response-required }}")) ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) )) .build(), ConnectivityModelFactory.newTargetBuilder(targets.get(1)) .address(getExpectedResolvedCommandTargetAddress()) + .originalAddress(getExpectedResolvedCommandTargetAddress()) .headerMapping(ConnectivityModelFactory.newHeaderMapping( basicAdditionalTargetHeaderMappingEntries )) .build() )) - .credentials(honoConfig.getUserPasswordCredentials()) .build(); } diff --git a/connectivity/service/src/test/resources/hono-connection-custom-expected.json b/connectivity/service/src/test/resources/hono-connection-custom-expected.json new file mode 100644 index 0000000000..2679545f97 --- /dev/null +++ b/connectivity/service/src/test/resources/hono-connection-custom-expected.json @@ -0,0 +1,307 @@ +{ + "id": "test-connection-id", + "name": "Things-Hono Test 1", + "connectionType": "hono", + "connectionStatus": "open", + "uri": "tcp://test_username:test_password@localhost:9922", + "sources": [ + { + "addresses": [ + "hono.telemetry" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "hono.command/{{thing:id}}", + "headerMapping": { + "device_id": "custom_value1", + "user_key1": "user_value1", + "subject": "{{ header:subject | fn:default(topic:action-subject) | fn:default(topic:criterion) }}-response", + "correlation-id": "{{ header:correlation-id }}" + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "hono.event" + ], + "consumerCount": 1, + "qos": 1, + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [] + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "hono.command/{{thing:id}}", + "headerMapping": { + "device_id": "{{ thing:id }}", + "subject": "custom_value2", + "user_key2": "user_value2", + "correlation-id": "{{ header:correlation-id }}" + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "hono.command_response" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": { + "status": "custom_value3", + "user_key3": "user_value3", + "correlation-id": "{{ header:correlation-id }}" + }, + "payloadMapping": [ + "Ditto" + ], + "replyTarget": { + "enabled": false + } + } + ], + "targets": [ + { + "address": "hono.command/{{thing:id}}", + "topics": [ + "_/_/things/live/messages", + "_/_/things/live/commands" + ], + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "headerMapping": { + "user_key4": "user_value4", + "device_id": "{{ thing:id }}", + "response-required": "custom_value4", + "subject": "{{ header:subject | fn:default(topic:action-subject) }}", + "correlation-id": "{{ header:correlation-id }}" + } + }, + { + "address": "hono.command/{{thing:id}}", + "topics": [ + "_/_/things/twin/events", + "_/_/things/live/events" + ], + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "headerMapping": { + "user_key5": "user_value5", + "device_id": "{{ thing:id }}", + "subject": "{{ header:subject | fn:default(topic:action-subject) }}", + "correlation-id": "custom_value5" + } + } + ], + "clientCount": 1, + "failoverEnabled": true, + "validateCertificates": false, + "processorPoolSize": 5, + "specificConfig": { + "saslMechanism": "plain", + "bootstrapServers": "tcp://server1:port1,tcp://server2:port2,tcp://server3:port3", + "groupId": "custom_groupId" + }, + "mappingDefinitions": { + "implicitEdgeThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_copyPolicyFrom": "{{ header:gateway_id }}", + "attributes": { + "Info": { + "gatewayId": "{{ header:gateway_id }}" + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "behindGateway": "fn:filter(header:gateway_id, 'exists')", + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')" + } + }, + "implicitStandaloneThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_policy": { + "entries": { + "DEVICE": { + "subjects": { + "integration:{{solution:id}}:hub": { + "type": "iot-things-integration" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + }, + "DEFAULT": { + "subjects": { + "iot-suite-dev:/service-instance.{{solution:package-service-instance-id}}.iot-things": { + "type": "suite-auth" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + }, + "DEVICE-MANAGEMENT": { + "subjects": { + "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-manager": { + "type": "suite-auth" + }, + "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@developer-console": { + "type": "suite-auth" + }, + "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-rollouts": { + "type": "suite-auth" + }, + "integration:{{ solution:id }}:iot-manager": { + "type": "iot-things-integration" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + } + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')", + "notBehindGateway": "fn:filter(header:gateway_id, 'exists', 'false')" + } + }, + "status": { + "mappingEngine": "ConnectionStatus", + "options": { + "thingId": "{{ header:device_id }}" + } + } + } +} \ No newline at end of file diff --git a/connectivity/service/src/test/resources/hono-connection-custom-test.json b/connectivity/service/src/test/resources/hono-connection-custom-test.json new file mode 100644 index 0000000000..4f482e4e39 --- /dev/null +++ b/connectivity/service/src/test/resources/hono-connection-custom-test.json @@ -0,0 +1,295 @@ +{ + "id": "test-connection-id", + "name": "Things-Hono Test 1", + "connectionType": "hono", + "connectionStatus": "open", + "uri": "ssl://hono-endpoint:1", + "sources": [ + { + "addresses": [ + "telemetry" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "command", + "headerMapping": { + "user_key1": "user_value1", + "device_id": "custom_value1" + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "event" + ], + "consumerCount": 1, + "qos": 1, + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [] + }, + "headerMapping": {}, + "payloadMapping": [ + "Ditto", + "status", + "implicitEdgeThingCreation", + "implicitStandaloneThingCreation" + ], + "replyTarget": { + "address": "command", + "headerMapping": { + "user_key2": "user_value2", + "subject": "custom_value2" + }, + "expectedResponseTypes": [ + "response", + "error" + ], + "enabled": true + } + }, + { + "addresses": [ + "command_response" + ], + "consumerCount": 1, + "qos": 0, + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": [ + "{{ entity:id }}" + ] + }, + "acknowledgementRequests": { + "includes": [], + "filter": "fn:delete()" + }, + "headerMapping": { + "user_key3": "user_value3", + "status": "custom_value3" + }, + "payloadMapping": [ + "Ditto" + ], + "replyTarget": { + "enabled": false + } + } + ], + "targets": [ + { + "address": "command", + "topics": [ + "_/_/things/live/messages", + "_/_/things/live/commands" + ], + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "headerMapping": { + "user_key4": "user_value4", + "response-required": "custom_value4" + } + }, + { + "address": "command", + "topics": [ + "_/_/things/twin/events", + "_/_/things/live/events" + ], + "authorizationContext": [ + "integration:{{solution.id}}:hub" + ], + "headerMapping": { + "user_key5": "user_value5", + "correlation-id": "custom_value5" + } + } + ], + "clientCount": 1, + "failoverEnabled": true, + "validateCertificates": true, + "processorPoolSize": 5, + "specificConfig": { + "groupId": "custom_groupId" + }, + "mappingDefinitions": { + "implicitEdgeThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_copyPolicyFrom": "{{ header:gateway_id }}", + "attributes": { + "Info": { + "gatewayId": "{{ header:gateway_id }}" + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "behindGateway": "fn:filter(header:gateway_id, 'exists')", + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')" + } + }, + "implicitStandaloneThingCreation": { + "mappingEngine": "ImplicitThingCreation", + "options": { + "thing": { + "thingId": "{{ header:device_id }}", + "_policy": { + "entries": { + "DEVICE": { + "subjects": { + "integration:{{solution:id}}:hub": { + "type": "iot-things-integration" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + }, + "DEFAULT": { + "subjects": { + "iot-suite-dev:/service-instance.{{solution:package-service-instance-id}}.iot-things": { + "type": "suite-auth" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + }, + "DEVICE-MANAGEMENT": { + "subjects": { + "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-manager": { + "type": "suite-auth" + }, + "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@developer-console": { + "type": "suite-auth" + }, + "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-rollouts": { + "type": "suite-auth" + }, + "integration:{{ solution:id }}:iot-manager": { + "type": "iot-things-integration" + } + }, + "resources": { + "policy:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "thing:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + }, + "message:/": { + "revoke": [], + "grant": [ + "READ", + "WRITE" + ] + } + } + } + } + } + }, + "commandHeaders": {} + }, + "incomingConditions": { + "honoRegistration": "fn:filter(header:hono_registration_status, 'eq', 'NEW')", + "notBehindGateway": "fn:filter(header:gateway_id, 'exists', 'false')" + } + }, + "status": { + "mappingEngine": "ConnectionStatus", + "options": { + "thingId": "{{ header:device_id }}" + } + } + } +} \ No newline at end of file diff --git a/connectivity/service/src/test/resources/test-connection-hono.json b/connectivity/service/src/test/resources/hono-connection-default-test.json similarity index 98% rename from connectivity/service/src/test/resources/test-connection-hono.json rename to connectivity/service/src/test/resources/hono-connection-default-test.json index b0a6f91210..418c605863 100644 --- a/connectivity/service/src/test/resources/test-connection-hono.json +++ b/connectivity/service/src/test/resources/hono-connection-default-test.json @@ -135,9 +135,6 @@ "failoverEnabled": true, "validateCertificates": true, "processorPoolSize": 5, - "specificConfig": { - "groupId": "t943ff0485af04c3b91050151f46c5ebe_hub_{{connection:id}}" - }, "mappingDefinitions": { "implicitEdgeThingCreation": { "mappingEngine": "ImplicitThingCreation", From 91b48044033cbe6d4ff5b521bf58a1a8522be1d9 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Thu, 15 Sep 2022 19:09:16 +0300 Subject: [PATCH 36/65] groupId handling changed Signed-off-by: Andrey Balarev --- .../messaging/hono/DefaultHonoConnectionFactory.java | 5 ++--- .../service/messaging/hono/HonoConnectionFactory.java | 7 ++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java index 516150057c..62a2ec2780 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java @@ -16,7 +16,6 @@ import java.text.MessageFormat; import java.util.Set; -import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.HonoAddressAlias; import org.eclipse.ditto.connectivity.model.UserPasswordCredentials; import org.eclipse.ditto.connectivity.service.config.DefaultHonoConfig; @@ -68,8 +67,8 @@ public Set getBootstrapServerUris() { } @Override - protected String getGroupId(final Connection connection) { - return connection.getId().toString(); + protected String getGroupId(final String suffix) { + return suffix; } @Override diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index 613795d275..fc545a0948 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -142,7 +142,7 @@ protected void preConversion(final Connection honoConnection) { protected abstract Set getBootstrapServerUris(); - protected abstract String getGroupId(Connection connection); + protected abstract String getGroupId(String suffix); protected abstract UserPasswordCredentials getCredentials(); @@ -156,8 +156,9 @@ private String combineUriWithCredentials(final String uri, final UserPasswordCre } private Map makeupSpecificConfig(final Connection connection) { - var groupId = Optional.ofNullable( - connection.getSpecificConfig().get("groupId")).orElse(getGroupId(connection)); + var groupId = getGroupId(Optional + .ofNullable(connection.getSpecificConfig().get("groupId")) + .orElse(connection.getId().toString())); return Map.of( "saslMechanism", String.valueOf(getSaslMechanism()), "bootstrapServers", getAsCommaSeparatedListString(getBootstrapServerUris()), From fb0ae41be8efa6a78d0b6810a138f576acf94bad Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Tue, 4 Oct 2022 18:14:28 +0300 Subject: [PATCH 37/65] Hono-credentials added to connectivity-extension.conf Signed-off-by: Andrey Balarev --- .../src/main/resources/connectivity-extension.conf | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/connectivity/service/src/main/resources/connectivity-extension.conf b/connectivity/service/src/main/resources/connectivity-extension.conf index 0047b8fda8..de025ddb35 100644 --- a/connectivity/service/src/main/resources/connectivity-extension.conf +++ b/connectivity/service/src/main/resources/connectivity-extension.conf @@ -1 +1,10 @@ -ditto.mapping-strategy.implementation = "org.eclipse.ditto.connectivity.api.ConnectivityMappingStrategies" +ditto { + mapping-strategy.implementation = "org.eclipse.ditto.connectivity.api.ConnectivityMappingStrategies" + connectivity.hono.credentials { + username = "honoUsername" + username = ${?HONO_CONNECTION_HONO_USERNAME} + + password = "honoPassword" + password = ${?HONO_CONNECTION_HONO_PASSWORD} + } +} \ No newline at end of file From 26902a5a2dd2cc8d183bb6a1ade4a3496a64c87a Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Tue, 4 Oct 2022 18:21:54 +0300 Subject: [PATCH 38/65] retrieveHonoConnection piggyback command introduced. A new piggyback command 'retrieveHonoConnection' is implemented which retrieves a 'real' connection of type 'Hono' (with already resolved address aliases, added header mappings , specificConfig etc.) Signed-off-by: Andrey Balarev --- .../query/RetrieveHonoConnection.java | 167 +++++++++++++++ .../query/RetrieveHonoConnectionResponse.java | 201 ++++++++++++++++++ .../model/signals/commands/TestConstants.java | 10 +- .../RetrieveHonoConnectionResponseTest.java | 104 +++++++++ .../query/RetrieveHonoConnectionTest.java | 121 +++++++++++ .../ConnectionPersistenceActor.java | 5 +- .../commands/ConnectionCreatedStrategies.java | 3 +- .../RetrieveHonoConnectionStrategy.java | 59 +++++ internal/utils/aggregator/pom.xml | 4 + .../commands/CommandStrategy.java | 13 +- .../commands/DefaultContext.java | 26 ++- policies/service/pom.xml | 6 + .../actors/PolicyPersistenceActor.java | 4 +- .../AbstractPolicyCommandStrategyTest.java | 21 +- .../commands/PolicyConflictStrategyTest.java | 11 +- things/service/pom.xml | 6 + .../actors/ThingPersistenceActor.java | 2 +- .../commands/AbstractCommandStrategyTest.java | 9 +- .../commands/ThingConflictStrategyTest.java | 9 +- 19 files changed, 750 insertions(+), 31 deletions(-) create mode 100644 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnection.java create mode 100644 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java create mode 100644 connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponseTest.java create mode 100644 connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionTest.java create mode 100644 connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveHonoConnectionStrategy.java diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnection.java new file mode 100644 index 0000000000..9cfb29a798 --- /dev/null +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnection.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model.signals.commands.query; + +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + +import java.util.Objects; +import java.util.function.Predicate; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.base.model.json.JsonParsableCommand; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; +import org.eclipse.ditto.base.model.signals.SignalWithEntityId; +import org.eclipse.ditto.base.model.signals.commands.AbstractCommand; +import org.eclipse.ditto.base.model.signals.commands.CommandJsonDeserializer; +import org.eclipse.ditto.connectivity.model.ConnectionId; +import org.eclipse.ditto.connectivity.model.WithConnectionId; +import org.eclipse.ditto.connectivity.model.signals.commands.ConnectivityCommand; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonField; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; + +/** + * Command which retrieves a {@link org.eclipse.ditto.connectivity.model.Connection} of type 'hono' + * after resolving its aliases and with its additional properties like header mappings and specific config. + */ +@Immutable +@JsonParsableCommand(typePrefix = ConnectivityCommand.TYPE_PREFIX, name = RetrieveHonoConnection.NAME) +public final class RetrieveHonoConnection extends AbstractCommand + implements ConnectivityQueryCommand, WithConnectionId, SignalWithEntityId { + + /** + * Name of this command. + */ + public static final String NAME = "retrieveHonoConnection"; + + /** + * Type of this command. + */ + public static final String TYPE = ConnectivityCommand.TYPE_PREFIX + NAME; + + private final ConnectionId connectionId; + + private RetrieveHonoConnection(final ConnectionId connectionId, final DittoHeaders dittoHeaders) { + super(TYPE, dittoHeaders); + this.connectionId = connectionId; + } + + /** + * Returns a new instance of {@code RetrieveHonoConnection}. + * + * @param connectionId the identifier of the connection to be retrieved. + * @param dittoHeaders the headers of the request. + * @return a new RetrieveHonoConnection command. + * @throws NullPointerException if any argument is {@code null}. + */ + public static RetrieveHonoConnection of(final ConnectionId connectionId, final DittoHeaders dittoHeaders) { + checkNotNull(connectionId, "connectionId"); + return new RetrieveHonoConnection(connectionId, dittoHeaders); + } + + /** + * Creates a new {@code RetrieveHonoConnection} from a JSON string. + * + * @param jsonString the JSON string of which the command is to be retrieved. + * @param dittoHeaders the headers of the command. + * @return the command. + * @throws NullPointerException if {@code jsonString} is {@code null}. + * @throws IllegalArgumentException if {@code jsonString} is empty. + * @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonString} was not in the expected + * @throws org.eclipse.ditto.json.JsonMissingFieldException if connectionId is missing in the passed in {@code jsonString} + */ + public static RetrieveHonoConnection fromJson(final String jsonString, final DittoHeaders dittoHeaders) { + return fromJson(JsonFactory.newObject(jsonString), dittoHeaders); + } + + /** + * Creates a new {@code RetrieveHonoConnection} from a JSON object. + * + * @param jsonObject the JSON object of which the command is to be created. + * @param dittoHeaders the headers of the command. + * @return the command. + * @throws NullPointerException if {@code jsonObject} is {@code null}. + * @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonObject} was not in the expected + * format. + */ + public static RetrieveHonoConnection fromJson(final JsonObject jsonObject, final DittoHeaders dittoHeaders) { + return new CommandJsonDeserializer(TYPE, jsonObject).deserialize(() -> { + final String readConnectionId = jsonObject.getValueOrThrow(ConnectivityCommand.JsonFields.JSON_CONNECTION_ID); + final ConnectionId connectionId = ConnectionId.of(readConnectionId); + + return of(connectionId, dittoHeaders); + }); + } + + @Override + protected void appendPayload(final JsonObjectBuilder jsonObjectBuilder, final JsonSchemaVersion schemaVersion, + final Predicate thePredicate) { + + final Predicate predicate = schemaVersion.and(thePredicate); + jsonObjectBuilder.set(ConnectivityCommand.JsonFields.JSON_CONNECTION_ID, String.valueOf(connectionId), + predicate); + } + + @Override + public ConnectionId getEntityId() { + return connectionId; + } + + @Override + public Category getCategory() { + return Category.QUERY; + } + + @Override + public RetrieveHonoConnection setDittoHeaders(final DittoHeaders dittoHeaders) { + return of(connectionId, dittoHeaders); + } + + @Override + protected boolean canEqual(@Nullable final Object other) { + return other instanceof RetrieveHonoConnection; + } + + @Override + public boolean equals(@Nullable final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + final RetrieveHonoConnection that = (RetrieveHonoConnection) o; + return Objects.equals(connectionId, that.connectionId); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), connectionId); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + super.toString() + + ", connectionId=" + connectionId + + "]"; + } + +} diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java new file mode 100644 index 0000000000..d1254ba427 --- /dev/null +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model.signals.commands.query; + +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkArgument; +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + +import java.util.Collections; +import java.util.Objects; +import java.util.function.Predicate; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import org.eclipse.ditto.base.model.common.HttpStatus; +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.base.model.json.FieldType; +import org.eclipse.ditto.base.model.json.JsonParsableCommandResponse; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; +import org.eclipse.ditto.base.model.signals.SignalWithEntityId; +import org.eclipse.ditto.base.model.signals.commands.AbstractCommandResponse; +import org.eclipse.ditto.base.model.signals.commands.CommandResponseHttpStatusValidator; +import org.eclipse.ditto.base.model.signals.commands.CommandResponseJsonDeserializer; +import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectionId; +import org.eclipse.ditto.connectivity.model.ConnectionType; +import org.eclipse.ditto.connectivity.model.WithConnectionId; +import org.eclipse.ditto.json.JsonField; +import org.eclipse.ditto.json.JsonFieldDefinition; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; +import org.eclipse.ditto.json.JsonValue; + +/** + * Response to a {@link RetrieveConnection} command. + */ +@Immutable +@JsonParsableCommandResponse(type = RetrieveHonoConnectionResponse.TYPE) +public final class RetrieveHonoConnectionResponse extends AbstractCommandResponse + implements ConnectivityQueryCommandResponse, WithConnectionId, + SignalWithEntityId { + + /** + * Type of this response. + */ + public static final String TYPE = TYPE_PREFIX + RetrieveHonoConnection.NAME; + + static final JsonFieldDefinition JSON_CONNECTION = + JsonFieldDefinition.ofJsonObject("connection", FieldType.REGULAR, JsonSchemaVersion.V_2); + + private static final HttpStatus HTTP_STATUS = HttpStatus.OK; + + private static final CommandResponseJsonDeserializer JSON_DESERIALIZER = + CommandResponseJsonDeserializer.newInstance(TYPE, + context -> { + final JsonObject jsonObject = context.getJsonObject(); + return new RetrieveHonoConnectionResponse(jsonObject.getValueOrThrow(JSON_CONNECTION), + context.getDeserializedHttpStatus(), + context.getDittoHeaders()); + }); + + private final JsonObject connection; + + private RetrieveHonoConnectionResponse(final JsonObject connection, + final HttpStatus httpStatus, + final DittoHeaders dittoHeaders) { + + super(TYPE, + CommandResponseHttpStatusValidator.validateHttpStatus(httpStatus, + Collections.singleton(HTTP_STATUS), + RetrieveHonoConnectionResponse.class), + dittoHeaders); + this.connection = checkNotNull(connection, "connection"); + checkArgument(connection.getValueOrThrow(Connection.JsonFields.CONNECTION_TYPE), + ConnectionType.HONO.getName()::equals, + () -> "The connection must be of type 'Hono'!"); + } + + /** + * Returns a new instance of {@code RetrieveHonoConnectionResponse}. + * + * @param connection the retrieved jsonObject. + * @param dittoHeaders the headers of the request. + * @return a new RetrieveHonoConnectionResponse response. + * @throws NullPointerException if any argument is {@code null}. + */ + public static RetrieveHonoConnectionResponse of(final JsonObject connection, final DittoHeaders dittoHeaders) { + return new RetrieveHonoConnectionResponse(connection, HTTP_STATUS, dittoHeaders); + } + + /** + * Creates a new {@code RetrieveHonoConnectionResponse} from a JSON string. + * + * @param jsonString the JSON string of which the response is to be retrieved. + * @param dittoHeaders the headers of the response. + * @return the response. + * @throws NullPointerException if {@code jsonString} is {@code null}. + * @throws IllegalArgumentException if {@code jsonString} is empty. + * @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonString} was not in the expected + * format. + * @throws org.eclipse.ditto.json.JsonMissingFieldException if connectionId is missing in the passed in {@code jsonString} + */ + public static RetrieveHonoConnectionResponse fromJson(final String jsonString, final DittoHeaders dittoHeaders) { + return fromJson(JsonObject.of(jsonString), dittoHeaders); + } + + /** + * Creates a new {@code RetrieveHonoConnectionResponse} from a JSON object. + * + * @param jsonObject the JSON object of which the response is to be created. + * @param dittoHeaders the headers of the response. + * @return the response. + * @throws NullPointerException if {@code jsonObject} is {@code null}. + * @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonObject} was not in the expected + * format. + * @throws org.eclipse.ditto.json.JsonMissingFieldException if connectionId is missing in the passed in {@code jsonString} + */ + public static RetrieveHonoConnectionResponse fromJson(final JsonObject jsonObject, + final DittoHeaders dittoHeaders) { + return JSON_DESERIALIZER.deserialize(jsonObject, dittoHeaders); + } + + @Override + protected void appendPayload(final JsonObjectBuilder jsonObjectBuilder, final JsonSchemaVersion schemaVersion, + final Predicate thePredicate) { + + final Predicate predicate = schemaVersion.and(thePredicate); + jsonObjectBuilder.set(JSON_CONNECTION, connection, predicate); + } + + /** + * @return the {@code JsonObject} of the connection. + */ + public JsonObject getJsonObject() { + return connection; + } + + @Override + public ConnectionId getEntityId() { + return ConnectionId.of(connection.getValueOrThrow(Connection.JsonFields.ID)); + } + + @Override + public RetrieveHonoConnectionResponse setEntity(final JsonValue entity) { + return of(entity.asObject(), getDittoHeaders()); + } + + @Override + public JsonValue getEntity(final JsonSchemaVersion schemaVersion) { + return connection; + } + + @Override + public RetrieveHonoConnectionResponse setDittoHeaders(final DittoHeaders dittoHeaders) { + return of(connection, dittoHeaders); + } + + @Override + protected boolean canEqual(@Nullable final Object other) { + return other instanceof RetrieveHonoConnectionResponse; + } + + @Override + public boolean equals(@Nullable final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + final RetrieveHonoConnectionResponse that = (RetrieveHonoConnectionResponse) o; + return Objects.equals(connection, that.connection); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), connection); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + super.toString() + + ", jsonObject=" + connection + + "]"; + } + +} diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/TestConstants.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/TestConstants.java index 8aa8b01b44..765cd84077 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/TestConstants.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/TestConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2017-2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -65,6 +65,7 @@ public final class TestConstants { public static final String TIMESTAMP = "2019-05-21T11:06:54.210Z"; public static final ConnectionType TYPE = ConnectionType.AMQP_10; + public static final ConnectionType HONO_TYPE = ConnectionType.HONO; public static final ConnectivityStatus STATUS = ConnectivityStatus.OPEN; private static final String URI = "amqps://username:password@my.endpoint:443"; @@ -135,6 +136,13 @@ public final class TestConstants { .mappingContext(MAPPING_CONTEXT) .build(); + public static final Connection HONO_CONNECTION = + ConnectivityModelFactory.newConnectionBuilder(ID, HONO_TYPE, STATUS, URI) + .sources(SOURCES) + .targets(TARGETS) + .mappingContext(MAPPING_CONTEXT) + .build(); + public static final class Metrics { private static final Instant LAST_MESSAGE_AT = Instant.now(); diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponseTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponseTest.java new file mode 100644 index 0000000000..18e212fd5d --- /dev/null +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponseTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model.signals.commands.query; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mutabilitydetector.unittesting.AllowedReason.provided; +import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; +import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; + +import org.eclipse.ditto.base.model.common.HttpStatus; +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.base.model.signals.commands.CommandResponse; +import org.eclipse.ditto.connectivity.model.MappingContext; +import org.eclipse.ditto.connectivity.model.signals.commands.TestConstants; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.assertions.DittoJsonAssertions; +import org.junit.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +/** + * Unit test for {@link RetrieveHonoConnectionResponse}. + */ +public final class RetrieveHonoConnectionResponseTest { + + private static final JsonObject KNOWN_JSON = JsonObject.newBuilder() + .set(CommandResponse.JsonFields.TYPE, RetrieveHonoConnectionResponse.TYPE) + .set(CommandResponse.JsonFields.STATUS, HttpStatus.OK.getCode()) + .set(RetrieveHonoConnectionResponse.JSON_CONNECTION, TestConstants.HONO_CONNECTION.toJson()) + .build(); + + @Test + public void testHashCodeAndEquals() { + EqualsVerifier.forClass(RetrieveHonoConnectionResponse.class) + .usingGetClass() + .verify(); + } + + @Test + public void assertImmutability() { + assertInstancesOf(RetrieveHonoConnectionResponse.class, + areImmutable(), + provided(JsonObject.class, MappingContext.class).isAlsoImmutable()); + } + + @Test + public void retrieveInstanceWithNullConnection() { + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> RetrieveHonoConnectionResponse.of(null, DittoHeaders.empty())) + .withMessage("The %s must not be null!", "connection") + .withNoCause(); + } + + @Test + public void retrieveInstanceWithNonHonoConnection() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> RetrieveHonoConnectionResponse.of(TestConstants.CONNECTION.toJson(), DittoHeaders.empty())) + .withMessage("The %s must be of type 'Hono'!", "connection") + .withNoCause(); + } + + @Test + public void fromJsonReturnsExpected() { + final RetrieveHonoConnectionResponse expected = + RetrieveHonoConnectionResponse.of(TestConstants.HONO_CONNECTION.toJson(), DittoHeaders.empty()); + + final RetrieveHonoConnectionResponse actual = + RetrieveHonoConnectionResponse.fromJson(KNOWN_JSON, DittoHeaders.empty()); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void toJsonReturnsExpected() { + final RetrieveHonoConnectionResponse underTest = + RetrieveHonoConnectionResponse.of(TestConstants.HONO_CONNECTION.toJson(), DittoHeaders.empty()); + + assertThat(underTest.toJson()).isEqualTo(KNOWN_JSON); + } + + @Test + public void getResourcePathReturnsExpected() { + final JsonPointer expectedResourcePath = JsonFactory.emptyPointer(); + + final RetrieveHonoConnectionResponse underTest = + RetrieveHonoConnectionResponse.of(TestConstants.HONO_CONNECTION.toJson(), DittoHeaders.empty()); + + DittoJsonAssertions.assertThat(underTest.getResourcePath()).isEqualTo(expectedResourcePath); + } + +} diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionTest.java new file mode 100644 index 0000000000..01b39aa3a6 --- /dev/null +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model.signals.commands.query; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mutabilitydetector.unittesting.AllowedReason.provided; +import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; +import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.ditto.base.model.headers.DittoHeaders; +import org.eclipse.ditto.base.model.signals.commands.Command; +import org.eclipse.ditto.connectivity.model.ConnectionId; +import org.eclipse.ditto.connectivity.model.signals.commands.ConnectivityCommand; +import org.eclipse.ditto.connectivity.model.signals.commands.TestConstants; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.assertions.DittoJsonAssertions; +import org.junit.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +/** + * Unit test for {@link RetrieveHonoConnection}. + */ +public final class RetrieveHonoConnectionTest { + + private static final JsonObject KNOWN_JSON = JsonObject.newBuilder() + .set(Command.JsonFields.TYPE, RetrieveHonoConnection.TYPE) + .set(ConnectivityCommand.JsonFields.JSON_CONNECTION_ID, TestConstants.ID.toString()) + .build(); + + @Test + public void testHashCodeAndEquals() { + EqualsVerifier.forClass(RetrieveHonoConnection.class) + .usingGetClass() + .verify(); + } + + @Test + public void assertImmutability() { + assertInstancesOf(RetrieveHonoConnection.class, + areImmutable(), + provided(ConnectionId.class).isAlsoImmutable()); + } + + @Test + public void createInstanceWithNullConnectionId() { + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> RetrieveHonoConnection.of(null, DittoHeaders.empty())) + .withMessage("The %s must not be null!", "connectionId") + .withNoCause(); + } + + @Test + public void fromJsonReturnsExpected() { + final RetrieveHonoConnection expected = + RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); + + final RetrieveHonoConnection actual = + RetrieveHonoConnection.fromJson(KNOWN_JSON, DittoHeaders.empty()); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void toJsonReturnsExpected() { + final JsonObject actual = + RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()).toJson(); + + assertThat(actual).isEqualTo(KNOWN_JSON); + } + + @Test + public void getEntityIdReturnsExpected() { + final RetrieveHonoConnection actual = + RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); + + assertThat((CharSequence) actual.getEntityId()).isEqualTo(ConnectionId.of(TestConstants.ID)); + } + + @Test + public void setDittoHeadersReturnsExpected() { + Map map = new HashMap<>(); + map.put("header1_key", "header1_value"); + map.put("header2_key", "header2_value"); + map.put("header3_key", "header3_value"); + final DittoHeaders EXPECTED_DITTO_HEADERS = DittoHeaders.of(map); + final RetrieveHonoConnection actual = + RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); + + assertThat(actual.getDittoHeaders()).isEmpty(); + RetrieveHonoConnection changed = actual.setDittoHeaders(EXPECTED_DITTO_HEADERS); + assertThat(changed.getDittoHeaders()).isEqualTo(EXPECTED_DITTO_HEADERS); + } + + @Test + public void getResourcePathReturnsExpected() { + final JsonPointer expectedResourcePath = JsonFactory.emptyPointer(); + + final RetrieveHonoConnection underTest = + RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); + + DittoJsonAssertions.assertThat(underTest.getResourcePath()).isEqualTo(expectedResourcePath); + } + +} diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index da8af700e2..9cf9dbb9ea 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2017-2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -313,7 +313,8 @@ protected Class getEventClass() { @Override protected CommandStrategy.Context getStrategyContext() { return DefaultContext.getInstance( - ConnectionState.of(entityId, connectionLoggerRegistry, connectionLogger, commandValidator), log); + ConnectionState.of(entityId, connectionLoggerRegistry, connectionLogger, commandValidator), + log, getContext().getSystem()); } @Override diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java index 3526df0a87..536cc742fc 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2019-2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -59,6 +59,7 @@ private static ConnectionCreatedStrategies newCreatedStrategies() { strategies.addStrategy(new RetrieveConnectionLogsStrategy()); strategies.addStrategy(new ResetConnectionLogsStrategy()); strategies.addStrategy(new RetrieveConnectionStrategy()); + strategies.addStrategy(new RetrieveHonoConnectionStrategy()); strategies.addStrategy(new RetrieveConnectionStatusStrategy()); strategies.addStrategy(new RetrieveConnectionMetricsStrategy()); strategies.addStrategy(new LoggingExpiredStrategy()); diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveHonoConnectionStrategy.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveHonoConnectionStrategy.java new file mode 100644 index 0000000000..b9a59c7686 --- /dev/null +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveHonoConnectionStrategy.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.service.messaging.persistence.strategies.commands; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.entity.metadata.Metadata; +import org.eclipse.ditto.connectivity.model.Connection; +import org.eclipse.ditto.connectivity.model.ConnectionType; +import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnection; +import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnectionResponse; +import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveHonoConnection; +import org.eclipse.ditto.connectivity.model.signals.events.ConnectivityEvent; +import org.eclipse.ditto.connectivity.service.messaging.hono.HonoConnectionFactory; +import org.eclipse.ditto.connectivity.service.messaging.persistence.stages.ConnectionState; +import org.eclipse.ditto.internal.utils.persistentactors.results.Result; +import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory; + +/** + * This strategy handles the {@link RetrieveConnection} command. + */ +final class RetrieveHonoConnectionStrategy extends AbstractConnectivityCommandStrategy { + + RetrieveHonoConnectionStrategy() { + super(RetrieveHonoConnection.class); + } + + @Override + protected Result> doApply(final Context context, + @Nullable final Connection entity, + final long nextRevision, + final RetrieveHonoConnection command, + @Nullable final Metadata metadata) { + + final Result> result; + if (entity != null && entity.getConnectionType() == ConnectionType.HONO) { + final var actorSystem = context.getActorSystem(); + final var honoConnectionFactory = HonoConnectionFactory.get( + actorSystem, actorSystem.settings().config()); + final var json = honoConnectionFactory.getHonoConnection(entity).toJson(); + + result = ResultFactory.newQueryResult(command, + RetrieveConnectionResponse.of(json, command.getDittoHeaders())); + } else { + result = ResultFactory.newErrorResult(notAccessible(context, command), command); + } + return result; + } +} diff --git a/internal/utils/aggregator/pom.xml b/internal/utils/aggregator/pom.xml index 8248e3db83..e0893461c5 100755 --- a/internal/utils/aggregator/pom.xml +++ b/internal/utils/aggregator/pom.xml @@ -14,6 +14,10 @@ 4.0.0 + + 17 + 17 + org.eclipse.ditto diff --git a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/CommandStrategy.java b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/CommandStrategy.java index 8df5a8bfa6..82cdacf9dd 100644 --- a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/CommandStrategy.java +++ b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/CommandStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2019-2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -16,10 +16,12 @@ import javax.annotation.Nullable; -import org.eclipse.ditto.internal.utils.akka.logging.DittoDiagnosticLoggingAdapter; -import org.eclipse.ditto.internal.utils.persistentactors.results.Result; import org.eclipse.ditto.base.model.signals.commands.Command; import org.eclipse.ditto.base.model.signals.events.Event; +import org.eclipse.ditto.internal.utils.akka.logging.DittoDiagnosticLoggingAdapter; +import org.eclipse.ditto.internal.utils.persistentactors.results.Result; + +import akka.actor.ActorSystem; /** * The CommandStrategy interface. @@ -108,6 +110,11 @@ interface Context { */ DittoDiagnosticLoggingAdapter getLog(); + /** + * @return reference to actorSystem + */ + ActorSystem getActorSystem(); + } } diff --git a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/DefaultContext.java b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/DefaultContext.java index e6e64f7a15..afb78ae432 100644 --- a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/DefaultContext.java +++ b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/DefaultContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Contributors to the Eclipse Foundation + * Copyright (c) 2019-2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -20,6 +20,8 @@ import org.eclipse.ditto.internal.utils.akka.logging.DittoDiagnosticLoggingAdapter; +import akka.actor.ActorSystem; + /** * Holds the context required to execute the * {@link CommandStrategy}s. @@ -32,10 +34,13 @@ public final class DefaultContext implements CommandStrategy.Context { private final K state; private final DittoDiagnosticLoggingAdapter log; - private DefaultContext(final K state, final DittoDiagnosticLoggingAdapter log) { + private final ActorSystem actorSystem; + + private DefaultContext(final K state, final DittoDiagnosticLoggingAdapter log, final ActorSystem actorSystem) { this.state = checkNotNull(state, "state"); this.log = checkNotNull(log, "log"); + this.actorSystem = checkNotNull(actorSystem, "actorSystem"); } /** @@ -46,8 +51,9 @@ private DefaultContext(final K state, final DittoDiagnosticLoggingAdapter log) { * @return the instance. * @throws NullPointerException if any argument is {@code null}. */ - public static DefaultContext getInstance(final K state, final DittoDiagnosticLoggingAdapter log) { - return new DefaultContext<>(state, log); + public static DefaultContext getInstance(final K state, final DittoDiagnosticLoggingAdapter log, + final ActorSystem actorSystem) { + return new DefaultContext<>(state, log, actorSystem); } @Override @@ -60,6 +66,11 @@ public DittoDiagnosticLoggingAdapter getLog() { return log; } + @Override + public ActorSystem getActorSystem() { + return actorSystem; + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -69,12 +80,14 @@ public boolean equals(final Object o) { return false; } final DefaultContext that = (DefaultContext) o; - return Objects.equals(state, that.state) && Objects.equals(log, that.log); + return Objects.equals(state, that.state) + && Objects.equals(log, that.log) + && Objects.equals(actorSystem, that.actorSystem); } @Override public int hashCode() { - return Objects.hash(state, log); + return Objects.hash(state, log, actorSystem); } @Override @@ -82,6 +95,7 @@ public String toString() { return getClass().getSimpleName() + " [" + "state=" + state + ", log=" + log + + ", actorSystem=" + actorSystem + "]"; } diff --git a/policies/service/pom.xml b/policies/service/pom.xml index e28aeb8cd9..af1d24ddb6 100644 --- a/policies/service/pom.xml +++ b/policies/service/pom.xml @@ -122,6 +122,12 @@ akka-testkit_${scala.version} test + + org.eclipse.ditto + ditto-internal-utils-akka + test-jar + test + org.eclipse.ditto ditto-internal-utils-test diff --git a/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java b/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java index f7344eda23..ac040606d9 100755 --- a/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java +++ b/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2017-2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -144,7 +144,7 @@ protected Class getEventClass() { @Override protected CommandStrategy.Context getStrategyContext() { - return DefaultContext.getInstance(entityId, log); + return DefaultContext.getInstance(entityId, log, getContext().getSystem()); } @Override diff --git a/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/AbstractPolicyCommandStrategyTest.java b/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/AbstractPolicyCommandStrategyTest.java index 9a3c62d203..dcb6296f99 100644 --- a/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/AbstractPolicyCommandStrategyTest.java +++ b/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/AbstractPolicyCommandStrategyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Contributors to the Eclipse Foundation + * Copyright (c) 2020-2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -31,20 +31,22 @@ import org.eclipse.ditto.base.model.headers.DittoHeaders; import org.eclipse.ditto.base.model.headers.WithDittoHeaders; import org.eclipse.ditto.base.model.headers.entitytag.EntityTag; -import org.eclipse.ditto.policies.model.Policy; -import org.eclipse.ditto.policies.model.PolicyId; -import org.eclipse.ditto.policies.service.persistence.TestConstants; +import org.eclipse.ditto.base.model.signals.commands.Command; +import org.eclipse.ditto.base.model.signals.commands.CommandResponse; +import org.eclipse.ditto.base.model.signals.events.Event; +import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; import org.eclipse.ditto.internal.utils.akka.logging.DittoDiagnosticLoggingAdapter; import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy; import org.eclipse.ditto.internal.utils.persistentactors.commands.DefaultContext; import org.eclipse.ditto.internal.utils.persistentactors.results.Result; import org.eclipse.ditto.internal.utils.persistentactors.results.ResultVisitor; -import org.eclipse.ditto.base.model.signals.commands.Command; -import org.eclipse.ditto.base.model.signals.commands.CommandResponse; +import org.eclipse.ditto.policies.model.Policy; +import org.eclipse.ditto.policies.model.PolicyId; import org.eclipse.ditto.policies.model.signals.commands.PolicyCommandResponse; -import org.eclipse.ditto.base.model.signals.events.Event; import org.eclipse.ditto.policies.model.signals.events.PolicyEvent; +import org.eclipse.ditto.policies.service.persistence.TestConstants; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -57,6 +59,9 @@ public abstract class AbstractPolicyCommandStrategyTest { protected static DittoDiagnosticLoggingAdapter logger; + @ClassRule + public static final ActorSystemResource ACTOR_SYSTEM_RESOURCE = ActorSystemResource.newInstance(); + @BeforeClass public static void initTestConstants() { logger = Mockito.mock(DittoDiagnosticLoggingAdapter.class); @@ -66,7 +71,7 @@ public static void initTestConstants() { } protected static CommandStrategy.Context getDefaultContext() { - return DefaultContext.getInstance(TestConstants.Policy.POLICY_ID, logger); + return DefaultContext.getInstance(TestConstants.Policy.POLICY_ID, logger, ACTOR_SYSTEM_RESOURCE.getActorSystem()); } protected static DittoHeaders buildActivateTokenIntegrationHeaders() { diff --git a/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/PolicyConflictStrategyTest.java b/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/PolicyConflictStrategyTest.java index d6d39e6a8e..1ed8869d64 100644 --- a/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/PolicyConflictStrategyTest.java +++ b/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/PolicyConflictStrategyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Contributors to the Eclipse Foundation + * Copyright (c) 2020-2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -23,6 +23,7 @@ import org.eclipse.ditto.base.model.headers.WithDittoHeaders; import org.eclipse.ditto.base.model.headers.entitytag.EntityTagMatchers; import org.eclipse.ditto.base.model.signals.commands.Command; +import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; import org.eclipse.ditto.internal.utils.akka.logging.DittoDiagnosticLoggingAdapter; import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy; import org.eclipse.ditto.internal.utils.persistentactors.commands.DefaultContext; @@ -36,6 +37,7 @@ import org.eclipse.ditto.policies.model.signals.commands.modify.CreatePolicy; import org.eclipse.ditto.policies.model.signals.events.PolicyEvent; import org.eclipse.ditto.policies.service.common.config.DefaultPolicyConfig; +import org.junit.ClassRule; import org.junit.Test; import org.mockito.Mockito; @@ -47,6 +49,9 @@ @SuppressWarnings({"rawtypes", "java:S3740"}) public final class PolicyConflictStrategyTest { + @ClassRule + public static final ActorSystemResource ACTOR_SYSTEM_RESOURCE = ActorSystemResource.newInstance(); + @Test public void assertImmutability() { assertInstancesOf(PolicyConflictStrategy.class, areImmutable()); @@ -59,7 +64,7 @@ public void createConflictResultWithoutPrecondition() { final PolicyId policyId = PolicyId.of("policy:id"); final Policy policy = PoliciesModelFactory.newPolicyBuilder(policyId).setRevision(25L).build(); final CommandStrategy.Context context = DefaultContext.getInstance(policyId, - mockLoggingAdapter()); + mockLoggingAdapter(), ACTOR_SYSTEM_RESOURCE.getActorSystem()); final CreatePolicy command = CreatePolicy.of(policy, DittoHeaders.empty()); final Result> result = underTest.apply(context, policy, 26L, command); result.accept(new ExpectErrorVisitor(PolicyConflictException.class)); @@ -72,7 +77,7 @@ public void createPreconditionFailedResultWithPrecondition() { final PolicyId policyId = PolicyId.of("policy:id"); final Policy policy = PoliciesModelFactory.newPolicyBuilder(policyId).setRevision(25L).build(); final CommandStrategy.Context context = DefaultContext.getInstance(policyId, - mockLoggingAdapter()); + mockLoggingAdapter(), ACTOR_SYSTEM_RESOURCE.getActorSystem()); final CreatePolicy command = CreatePolicy.of(policy, DittoHeaders.newBuilder() .ifNoneMatch(EntityTagMatchers.fromStrings("*")) .build()); diff --git a/things/service/pom.xml b/things/service/pom.xml index 849ab74e9d..04d6d0e764 100644 --- a/things/service/pom.xml +++ b/things/service/pom.xml @@ -179,6 +179,12 @@ test-jar test + + org.eclipse.ditto + ditto-internal-utils-akka + test + test-jar + org.eclipse.ditto ditto-internal-utils-test diff --git a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/ThingPersistenceActor.java b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/ThingPersistenceActor.java index 6067eeefbc..7fbbfe2dd9 100755 --- a/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/ThingPersistenceActor.java +++ b/things/service/src/main/java/org/eclipse/ditto/things/service/persistence/actors/ThingPersistenceActor.java @@ -141,7 +141,7 @@ protected Class getEventClass() { @Override protected CommandStrategy.Context getStrategyContext() { - return DefaultContext.getInstance(entityId, log); + return DefaultContext.getInstance(entityId, log, getContext().getSystem()); } @Override diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractCommandStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractCommandStrategyTest.java index f86e3535a9..cabd270f84 100644 --- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractCommandStrategyTest.java +++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractCommandStrategyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2017-2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -30,6 +30,7 @@ import org.eclipse.ditto.base.model.headers.WithDittoHeaders; import org.eclipse.ditto.base.model.signals.commands.Command; import org.eclipse.ditto.base.model.signals.commands.CommandResponse; +import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; import org.eclipse.ditto.internal.utils.akka.logging.DittoDiagnosticLoggingAdapter; import org.eclipse.ditto.internal.utils.persistentactors.commands.AbstractCommandStrategy; import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy; @@ -41,6 +42,7 @@ import org.eclipse.ditto.things.model.signals.events.ThingEvent; import org.eclipse.ditto.things.model.signals.events.ThingModifiedEvent; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -56,6 +58,9 @@ public abstract class AbstractCommandStrategyTest { protected static DittoDiagnosticLoggingAdapter logger; + @ClassRule + public static final ActorSystemResource ACTOR_SYSTEM_RESOURCE = ActorSystemResource.newInstance(); + @BeforeClass public static void initTestConstants() { logger = Mockito.mock(DittoDiagnosticLoggingAdapter.class); @@ -65,7 +70,7 @@ public static void initTestConstants() { } protected static CommandStrategy.Context getDefaultContext() { - return DefaultContext.getInstance(THING_ID, logger); + return DefaultContext.getInstance(THING_ID, logger, ACTOR_SYSTEM_RESOURCE.getActorSystem()); } protected static , T extends ThingModifiedEvent> T assertModificationResult( diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategyTest.java index 1a67e361b3..028caf5a5e 100644 --- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategyTest.java +++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/ThingConflictStrategyTest.java @@ -23,6 +23,7 @@ import org.eclipse.ditto.base.model.headers.WithDittoHeaders; import org.eclipse.ditto.base.model.headers.entitytag.EntityTagMatchers; import org.eclipse.ditto.base.model.signals.commands.Command; +import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; import org.eclipse.ditto.internal.utils.akka.logging.DittoDiagnosticLoggingAdapter; import org.eclipse.ditto.internal.utils.persistentactors.commands.CommandStrategy; import org.eclipse.ditto.internal.utils.persistentactors.commands.DefaultContext; @@ -35,6 +36,7 @@ import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingPreconditionFailedException; import org.eclipse.ditto.things.model.signals.commands.modify.CreateThing; import org.eclipse.ditto.things.model.signals.events.ThingEvent; +import org.junit.ClassRule; import org.junit.Test; import org.mockito.Mockito; @@ -44,6 +46,9 @@ @SuppressWarnings({"rawtypes", "java:S3740"}) public final class ThingConflictStrategyTest { + @ClassRule + public static final ActorSystemResource ACTOR_SYSTEM_RESOURCE = ActorSystemResource.newInstance(); + @Test public void assertImmutability() { assertInstancesOf(ThingConflictStrategy.class, areImmutable()); @@ -55,7 +60,7 @@ public void createConflictResultWithoutPrecondition() { final ThingId thingId = ThingId.of("thing:id"); final Thing thing = ThingsModelFactory.newThingBuilder().setId(thingId).setRevision(25L).build(); final CommandStrategy.Context context = DefaultContext.getInstance(thingId, - mockLoggingAdapter()); + mockLoggingAdapter(), ACTOR_SYSTEM_RESOURCE.getActorSystem()); final CreateThing command = CreateThing.of(thing, null, DittoHeaders.empty()); final Result> result = underTest.apply(context, thing, 26L, command); result.accept(new ExpectErrorVisitor(ThingConflictException.class)); @@ -67,7 +72,7 @@ public void createPreconditionFailedResultWithPrecondition() { final ThingId thingId = ThingId.of("thing:id"); final Thing thing = ThingsModelFactory.newThingBuilder().setId(thingId).setRevision(25L).build(); final CommandStrategy.Context context = DefaultContext.getInstance(thingId, - mockLoggingAdapter()); + mockLoggingAdapter(), ACTOR_SYSTEM_RESOURCE.getActorSystem()); final CreateThing command = CreateThing.of(thing, null, DittoHeaders.newBuilder() .ifNoneMatch(EntityTagMatchers.fromStrings("*")) .build()); From b1845b6bb4d42b58d1409bb4ee5ffcb72cc892ac Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Tue, 4 Oct 2022 18:23:33 +0300 Subject: [PATCH 39/65] Automatic reconnect of 'hono' connection after credentials are changed. On Hub-credentials changes, ConnectionSupervisorActor gets notified by SolutionModifiedActor in order to restart the potentially active 'Hono'-connection with the new parameters. Signed-off-by: Andrey Balarev --- .../config/ConnectionConfigProvider.java | 5 +-- .../ConnectivityConfigModifiedBehavior.java | 15 ++----- .../config/DittoConnectionConfigProvider.java | 5 +-- .../ConnectionSupervisorActor.java | 41 ++++++++++++++++--- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java index 0b85deacd6..bed7376d5b 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java @@ -13,7 +13,6 @@ package org.eclipse.ditto.connectivity.service.config; -import java.util.Optional; import java.util.concurrent.CompletionStage; import javax.annotation.Nullable; @@ -69,8 +68,8 @@ CompletionStage registerForConnectivityConfigChanges(ConnectionId connecti * Uses the given {@code event} to create a config which should overwrite the default connectivity config. * * @param event the event used to create a config which should overwrite the default connectivity config. - * @return Potentially empty config which holds the overwrites for the default connectivity config. + * In case of Hub params changed, a null value is passed, which will invoke unconditional connection restart */ - Optional handleEvent(Event event); + void handleEvent(Event event); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java index b2c8276ac9..5aeea71401 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java @@ -25,8 +25,9 @@ */ public interface ConnectivityConfigModifiedBehavior extends Actor { - /** + /** * Injectable behavior to handle an {@code Event} that transports config changes. + * This involves modified Hub parameters (credentials) for 'Hono'-connections as well. * * @return behavior to handle an {@code Event} that transports config changes. */ @@ -37,13 +38,12 @@ default AbstractActor.Receive connectivityConfigModifiedBehavior() { } /** - * Handles the received event by converting it to a {@link Config} and passing it to - * {@link #onConnectivityConfigModified(Config)}. + * Handles the received event by converting it to a {@link Config}. * * @param event the received event */ default void handleEvent(final Event event) { - getConnectivityConfigProvider().handleEvent(event).ifPresent(this::onConnectivityConfigModified); + getConnectivityConfigProvider().handleEvent(event); } /** @@ -53,11 +53,4 @@ default ConnectionConfigProvider getConnectivityConfigProvider() { return ConnectionConfigProviderFactory.getInstance(context().system()); } - /** - * This method is called when a config modification is received. Implementations must handle the modified config - * appropriately i.e. check if any relevant config has changed and re-initialize state if necessary. - * - * @param connectivityConfigOverwrites the modified config - */ - void onConnectivityConfigModified(Config connectivityConfigOverwrites); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java index 6afd796a6e..c1ca4b292a 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java @@ -12,7 +12,6 @@ */ package org.eclipse.ditto.connectivity.service.config; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -58,8 +57,8 @@ public boolean canHandle(final Event event) { } @Override - public Optional handleEvent(final Event event) { - return Optional.empty(); + public void handleEvent(final Event event) { + // By default not handled in Ditto } } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java index c8eeefbffc..bc45bb1719 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java @@ -16,6 +16,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeUnit; @@ -119,7 +120,7 @@ public static Props props(final ActorRef commandForwarder, @Override protected ConnectionId getEntityId() throws Exception { - return ConnectionId.of(URLDecoder.decode(getSelf().path().name(), StandardCharsets.UTF_8.name())); + return ConnectionId.of(URLDecoder.decode(getSelf().path().name(), StandardCharsets.UTF_8)); } @Override @@ -141,6 +142,8 @@ protected Receive activeBehaviour( .match(Config.class, this::onConnectivityConfigModified) .match(CheckForOverwritesConfig.class, checkForOverwrites -> initConfigOverwrites(getEntityId(), checkForOverwrites.dittoHeaders)) + .match(RestartConnection.class, restartConnection -> Optional.ofNullable(restartConnection + .getModifiedConfig()).ifPresentOrElse(this::onConnectivityConfigModified, this::restartChild)) .matchEquals(Control.REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL, c -> { log.debug("Successfully registered for connectivity config changes."); isRegisteredForConnectivityConfigChanges = true; @@ -161,7 +164,8 @@ protected boolean shouldBecomeTwinSignalProcessingAwaiting(final Signal signa protected void handleMessagesDuringStartup(final Object message) { if (message instanceof WithDittoHeaders withDittoHeaders) { initConfigOverwrites(entityId, withDittoHeaders.getDittoHeaders()); - } else if (message != Control.REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL) { + } else if (message != Control.REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL + && message != Control.REGISTRATION_FOR_HUB_PARAMS_CHANGES_SUCCESSFUL) { initConfigOverwrites(entityId, null); } super.handleMessagesDuringStartup(message); @@ -202,8 +206,11 @@ public SupervisorStrategy supervisorStrategy() { return SUPERVISOR_STRATEGY; } - @Override - public void onConnectivityConfigModified(final Config modifiedConfig) { + /* + * This method is called when a config modification is received. Implementations must handle the modified config + * appropriately i.e. check if any relevant config has changed and re-initialize state if necessary. + */ + private void onConnectivityConfigModified(Config modifiedConfig) { if (Objects.equals(connectivityConfigOverwrites, modifiedConfig)) { log.debug("Received modified config is unchanged, not restarting persistence actor."); } else { @@ -268,7 +275,8 @@ private Config handleConfigRetrievalException(final Throwable throwable, } private enum Control { - REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL + REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL, + REGISTRATION_FOR_HUB_PARAMS_CHANGES_SUCCESSFUL } private static class CheckForOverwritesConfig { @@ -279,4 +287,27 @@ private CheckForOverwritesConfig(@Nullable final DittoHeaders dittoHeaders) { this.dittoHeaders = dittoHeaders; } } + + /** + * Command to restart the connection. Used in case the config or hub params ('hono'-connections only) are modified. + */ + public static class RestartConnection { + + @Nullable + private final Config modifiedConfig; + + private RestartConnection(@Nullable final Config modifiedConfig) { + this.modifiedConfig = modifiedConfig; + } + + public static RestartConnection of(@Nullable Config modifiedConfig) { + return new RestartConnection(modifiedConfig); + } + + @Nullable + public Config getModifiedConfig() { + return modifiedConfig; + } + + } } From 36e1e13da21d08ba1a79beb6c8f904ea185459a7 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 5 Oct 2022 09:52:56 +0300 Subject: [PATCH 40/65] Copyright headers fixes Signed-off-by: Andrey Balarev --- .../connectivity/model/signals/commands/TestConstants.java | 2 +- .../messaging/persistence/ConnectionPersistenceActor.java | 2 +- .../strategies/commands/ConnectionCreatedStrategies.java | 2 +- .../utils/persistentactors/commands/CommandStrategy.java | 2 +- .../utils/persistentactors/commands/DefaultContext.java | 2 +- .../service/persistence/actors/PolicyPersistenceActor.java | 2 +- .../strategies/commands/AbstractPolicyCommandStrategyTest.java | 2 +- .../actors/strategies/commands/PolicyConflictStrategyTest.java | 2 +- .../actors/strategies/commands/AbstractCommandStrategyTest.java | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/TestConstants.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/TestConstants.java index 765cd84077..527088572d 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/TestConstants.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/TestConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index 9cf9dbb9ea..8760567a4c 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java index 536cc742fc..e099189886 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/CommandStrategy.java b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/CommandStrategy.java index 82cdacf9dd..fd815acb7e 100644 --- a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/CommandStrategy.java +++ b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/CommandStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/DefaultContext.java b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/DefaultContext.java index afb78ae432..9428a3228a 100644 --- a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/DefaultContext.java +++ b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/commands/DefaultContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java b/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java index ac040606d9..79e7408540 100755 --- a/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java +++ b/policies/service/src/main/java/org/eclipse/ditto/policies/service/persistence/actors/PolicyPersistenceActor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/AbstractPolicyCommandStrategyTest.java b/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/AbstractPolicyCommandStrategyTest.java index dcb6296f99..a7beb40187 100644 --- a/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/AbstractPolicyCommandStrategyTest.java +++ b/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/AbstractPolicyCommandStrategyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/PolicyConflictStrategyTest.java b/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/PolicyConflictStrategyTest.java index 1ed8869d64..42c38c7e74 100644 --- a/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/PolicyConflictStrategyTest.java +++ b/policies/service/src/test/java/org/eclipse/ditto/policies/service/persistence/actors/strategies/commands/PolicyConflictStrategyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractCommandStrategyTest.java b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractCommandStrategyTest.java index cabd270f84..2e27c326e9 100644 --- a/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractCommandStrategyTest.java +++ b/things/service/src/test/java/org/eclipse/ditto/things/service/persistence/actors/strategies/commands/AbstractCommandStrategyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. From 5079f38a6535ba0f4f1e1d235897d488aef4f513 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 5 Oct 2022 15:02:32 +0300 Subject: [PATCH 41/65] Javadoc improved. Signed-off-by: Andrey Balarev --- .../commands/query/RetrieveHonoConnectionResponse.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java index d1254ba427..9c8bb72974 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java @@ -93,6 +93,7 @@ private RetrieveHonoConnectionResponse(final JsonObject connection, * @param dittoHeaders the headers of the request. * @return a new RetrieveHonoConnectionResponse response. * @throws NullPointerException if any argument is {@code null}. + * @throws IllegalArgumentException if the connection type is not 'Hono'. */ public static RetrieveHonoConnectionResponse of(final JsonObject connection, final DittoHeaders dittoHeaders) { return new RetrieveHonoConnectionResponse(connection, HTTP_STATUS, dittoHeaders); @@ -105,7 +106,7 @@ public static RetrieveHonoConnectionResponse of(final JsonObject connection, fin * @param dittoHeaders the headers of the response. * @return the response. * @throws NullPointerException if {@code jsonString} is {@code null}. - * @throws IllegalArgumentException if {@code jsonString} is empty. + * @throws IllegalArgumentException if {@code jsonString} is empty or if the connection type is not 'Hono'. * @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonString} was not in the expected * format. * @throws org.eclipse.ditto.json.JsonMissingFieldException if connectionId is missing in the passed in {@code jsonString} @@ -121,6 +122,7 @@ public static RetrieveHonoConnectionResponse fromJson(final String jsonString, f * @param dittoHeaders the headers of the response. * @return the response. * @throws NullPointerException if {@code jsonObject} is {@code null}. + * @throws IllegalArgumentException if the connection type is not 'Hono'. * @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonObject} was not in the expected * format. * @throws org.eclipse.ditto.json.JsonMissingFieldException if connectionId is missing in the passed in {@code jsonString} From 5b7f41539fc6c8d50eb854ab2eff0cfe946497ab Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Thu, 6 Oct 2022 12:45:12 +0300 Subject: [PATCH 42/65] some fixes after merge Signed-off-by: Andrey Balarev --- .../messaging/persistence/ConnectionPersistenceActor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index 4fabb4c7ba..d2958f960c 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -80,6 +80,7 @@ import org.eclipse.ditto.connectivity.service.messaging.ClientActorPropsFactory; import org.eclipse.ditto.connectivity.service.messaging.ClientActorRefs; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpValidator; +import org.eclipse.ditto.connectivity.service.messaging.hono.HonoValidator; import org.eclipse.ditto.connectivity.service.messaging.httppush.HttpPushValidator; import org.eclipse.ditto.connectivity.service.messaging.kafka.KafkaValidator; import org.eclipse.ditto.connectivity.service.messaging.monitoring.logs.ConnectionLogger; From ec67d807397f4b88ff80b38eb21060dbdd019bf8 Mon Sep 17 00:00:00 2001 From: "Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM)" Date: Thu, 6 Oct 2022 16:58:07 +0300 Subject: [PATCH 43/65] Integrate Hono connection in ditto ConnectionRoute --- .../routes/connections/ConnectionsRoute.java | 105 ++++++++++++------ 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java index e6878d25b1..546177d123 100644 --- a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java +++ b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java @@ -27,6 +27,7 @@ import org.eclipse.ditto.base.model.signals.commands.Command; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionId; +import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.ConnectivityStatus; import org.eclipse.ditto.connectivity.model.signals.commands.ConnectivityCommand; @@ -306,51 +307,89 @@ private static ConnectivityCommand buildConnectivityCommand(final String comm } private static Connection buildConnectionForPost(final String connectionJson) { - final JsonObject connectionJsonObject = wrapJsonRuntimeException(() -> JsonFactory.newObject(connectionJson)); + final var connectionJsonObject = getConnectionJsonObjectOrThrow(connectionJson); + if (isContainingId(connectionJsonObject)) { + throw ConnectionIdNotExplicitlySettableException.newBuilder().build(); + } else { + return ConnectivityModelFactory.connectionFromJson( + setUriForHonoConnectionType( + setConnectionId( + setConnectionStatus(connectionJsonObject), + ConnectionId.generateRandom() + ) + ) + ); + } + } + + private static Connection buildConnectionForTest(final String connectionJson) { + final var connectionJsonObject = getConnectionJsonObjectOrThrow(connectionJson); + final var temporaryTestConnectionId = UUID.randomUUID() + "-dry-run"; + return ConnectivityModelFactory.connectionFromJson( + setUriForHonoConnectionType( + getConnectionId(connectionJsonObject) + .map(connectionId -> connectionJson.replace(connectionId, temporaryTestConnectionId)) + .map(ConnectionsRoute::getConnectionJsonObjectOrThrow) + .orElseGet(() -> setConnectionId(connectionJsonObject, temporaryTestConnectionId)) + ) + ); + } - if (connectionJsonObject.contains(Connection.JsonFields.ID.getPointer())) { + private static Connection buildConnectionForPut(final ConnectionId connectionId, final String connectionJson) { + var connectionJsonObject = getConnectionJsonObjectOrThrow(connectionJson); + final var actualConnectionIdOptional = getConnectionId(connectionJsonObject); + if (actualConnectionIdOptional.isEmpty()) { + connectionJsonObject = setConnectionId(connectionJsonObject, connectionId); + } else if (!connectionId.equals(actualConnectionIdOptional.get())) { throw ConnectionIdNotExplicitlySettableException.newBuilder().build(); } - final JsonObjectBuilder jsonObjectBuilder = connectionJsonObject.toBuilder(); - jsonObjectBuilder.set(Connection.JsonFields.ID, UUID.randomUUID().toString()); - final String connectionStatus = connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_STATUS) - .orElse(ConnectivityStatus.UNKNOWN.getName()); - jsonObjectBuilder.set(Connection.JsonFields.CONNECTION_STATUS, connectionStatus); + return ConnectivityModelFactory.connectionFromJson(setUriForHonoConnectionType(connectionJsonObject)); + } - return ConnectivityModelFactory.connectionFromJson(jsonObjectBuilder.build()); + private static boolean isHonoConnectionType(final JsonObject connectionJsonObject) { + return connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_TYPE) + .flatMap(ConnectionType::forName) + .filter(ConnectionType.HONO::equals) + .isPresent(); } - private static Connection buildConnectionForTest(final String connectionJson) { - final JsonObject connectionJsonObject; - final JsonObject connectionJsonObjectBeforeReplacement = - wrapJsonRuntimeException(() -> JsonFactory.newObject(connectionJson)); - final Optional optionalConnectionId = - connectionJsonObjectBeforeReplacement.getValue(Connection.JsonFields.ID); - final String temporaryTestConnectionId = UUID.randomUUID() + "-dry-run"; - if (optionalConnectionId.isPresent()) { - final String temporaryConnectionJson = - connectionJson.replace(optionalConnectionId.get(), temporaryTestConnectionId); - connectionJsonObject = wrapJsonRuntimeException(() -> JsonFactory.newObject(temporaryConnectionJson)); + private static JsonObject setUriForHonoConnectionType(final JsonObject connectionJsonObject) { + final JsonObject result; + if (isHonoConnectionType(connectionJsonObject)) { + result = connectionJsonObject.set(Connection.JsonFields.URI, "ssl://hono-endpoint:9094"); } else { - final JsonObjectBuilder jsonObjectBuilder = connectionJsonObjectBeforeReplacement.toBuilder(); - connectionJsonObject = jsonObjectBuilder.set(Connection.JsonFields.ID, temporaryTestConnectionId).build(); + result = connectionJsonObject; } - return ConnectivityModelFactory.connectionFromJson(connectionJsonObject); + return result; } - private static Connection buildConnectionForPut(final ConnectionId connectionId, final String connectionJson) { - final JsonObject connectionJsonObject = wrapJsonRuntimeException(() -> JsonFactory.newObject(connectionJson)); - if (connectionJsonObject.contains(Connection.JsonFields.ID.getPointer()) - && - !connectionId.toString().equals(connectionJsonObject.getValue(Connection.JsonFields.ID).orElse(null))) { - throw ConnectionIdNotExplicitlySettableException.newBuilder().build(); - } + private static JsonObject getConnectionJsonObjectOrThrow(final String connectionJsonString) { + return wrapJsonRuntimeException(() -> JsonObject.of(connectionJsonString)); + } + + private static boolean isContainingId(final JsonObject connectionJsonObject) { + return connectionJsonObject.contains(Connection.JsonFields.ID.getPointer()); + } - final JsonObjectBuilder jsonObjectBuilder = connectionJsonObject.toBuilder(); - jsonObjectBuilder.set(Connection.JsonFields.ID, connectionId.toString()); + private static JsonObject setConnectionStatus(final JsonObject connectionJsonObject) { + return connectionJsonObject.set( + Connection.JsonFields.CONNECTION_STATUS, + getConnectionStatusOrElseUnknown(connectionJsonObject) + ); + } + + private static String getConnectionStatusOrElseUnknown(final JsonObject connectionJsonObject) { + return connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_STATUS) + .orElseGet(ConnectivityStatus.UNKNOWN::getName); + } + + private static JsonObject setConnectionId(final JsonObject connectionJsonObject, final CharSequence connectionId) { + return connectionJsonObject.set(Connection.JsonFields.ID, connectionId.toString()); + } - return ConnectivityModelFactory.connectionFromJson(jsonObjectBuilder.build()); + private static Optional getConnectionId(final JsonObject connectionJsonObject) { + return connectionJsonObject.getValue(Connection.JsonFields.ID).map(ConnectionId::of); } -} +} \ No newline at end of file From 60ea7fd9e71d20c5bd847382ccb7dff33eb3c629 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 12 Oct 2022 08:09:22 +0300 Subject: [PATCH 44/65] Hono-connection resolving added for TestConnection (dry-run) Signed-off-by: Andrey Balarev --- .../service/config/HonoConfig.java | 6 ++- .../ConnectionPersistenceActor.java | 13 +++++- .../ConnectionPersistenceActorTest.java | 46 ++++++++++++++++++- .../routes/connections/ConnectionsRoute.java | 20 ++++---- 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java index 02b1f9dc58..18114f7b7e 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/HonoConfig.java @@ -119,7 +119,11 @@ public String getConfigPath() { enum SaslMechanism { - PLAIN("plain"); + PLAIN("plain"), + + SCRAM_SHA_256("scram-sha-256"), + + SCRAM_SHA_512("scram-sha-512"); private final String value; diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index d2958f960c..59759ef772 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -49,6 +49,7 @@ import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.ConnectionLifecycle; import org.eclipse.ditto.connectivity.model.ConnectionMetrics; +import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.ConnectivityStatus; import org.eclipse.ditto.connectivity.model.signals.commands.ConnectivityCommandInterceptor; @@ -80,6 +81,7 @@ import org.eclipse.ditto.connectivity.service.messaging.ClientActorPropsFactory; import org.eclipse.ditto.connectivity.service.messaging.ClientActorRefs; import org.eclipse.ditto.connectivity.service.messaging.amqp.AmqpValidator; +import org.eclipse.ditto.connectivity.service.messaging.hono.HonoConnectionFactory; import org.eclipse.ditto.connectivity.service.messaging.hono.HonoValidator; import org.eclipse.ditto.connectivity.service.messaging.httppush.HttpPushValidator; import org.eclipse.ditto.connectivity.service.messaging.kafka.KafkaValidator; @@ -188,6 +190,7 @@ public final class ConnectionPersistenceActor private final ConnectivityCommandInterceptor commandValidator; private int subscriptionCounter = 0; private Instant connectionClosedAt = Instant.now(); + private HonoConnectionFactory honoConnectionFactory; @Nullable private Instant loggingEnabledUntil; @Nullable private ActorRef clientActorRouter; @Nullable private ActorRef clientActorRefsAggregationActor; @@ -221,6 +224,7 @@ public final class ConnectionPersistenceActor loggingEnabledDuration = monitoringConfig.logger().logDuration(); checkLoggingActiveInterval = monitoringConfig.logger().loggingActiveCheckInterval(); + honoConnectionFactory = HonoConnectionFactory.get(actorSystem, actorSystem.settings().config()); // Make duration fuzzy to avoid all connections getting updated at once. final Duration fuzzyPriorityUpdateInterval = makeFuzzy(connectivityConfig.getConnectionConfig().getPriorityUpdateInterval()); @@ -802,7 +806,6 @@ private void testConnection(final StagedCommand command) { .toBuilder() .dryRun(true) .build(); - final TestConnection testConnection = (TestConnection) command.getCommand().setDittoHeaders(headersWithDryRun); if (clientActorRouter != null) { // client actor is already running, so either another TestConnection command is currently executed or the @@ -810,6 +813,14 @@ private void testConnection(final StagedCommand command) { // prevent strange behavior. origin.tell(TestConnectionResponse.alreadyCreated(entityId, command.getDittoHeaders()), self); } else { + final TestConnection testConnection; + TestConnection connection = (TestConnection) command.getCommand().setDittoHeaders(headersWithDryRun); + if (connection.getConnection().getConnectionType() == ConnectionType.HONO) { + testConnection = TestConnection.of( + honoConnectionFactory.getHonoConnection(connection.getConnection()), headersWithDryRun); + } else { + testConnection = connection; + } // no need to start more than 1 client for tests // set connection status to CLOSED so that client actors will not try to connect on startup setConnectionStatusClosedForTestConnection(); diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java index e24fa95957..2ca5242b78 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java @@ -17,6 +17,8 @@ import static org.eclipse.ditto.connectivity.service.messaging.MockClientActorPropsFactory.mockClientActorProbe; import static org.eclipse.ditto.connectivity.service.messaging.TestConstants.INSTANT; +import java.io.IOException; +import java.io.InputStreamReader; import java.time.Instant; import java.util.Collections; import java.util.List; @@ -72,11 +74,13 @@ import org.eclipse.ditto.connectivity.service.messaging.MockCommandValidator; import org.eclipse.ditto.connectivity.service.messaging.TestConstants; import org.eclipse.ditto.connectivity.service.messaging.WithMockServers; +import org.eclipse.ditto.connectivity.service.messaging.hono.DefaultHonoConnectionFactoryTest; import org.eclipse.ditto.internal.utils.akka.ActorSystemResource; import org.eclipse.ditto.internal.utils.akka.PingCommand; import org.eclipse.ditto.internal.utils.akka.controlflow.WithSender; import org.eclipse.ditto.internal.utils.persistentactors.AbstractPersistenceSupervisor; import org.eclipse.ditto.internal.utils.test.Retry; +import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.thingsearch.model.signals.commands.subscription.CreateSubscription; import org.junit.Before; import org.junit.Rule; @@ -107,11 +111,17 @@ public final class ConnectionPersistenceActorTest extends WithMockServers { "ditto.extensions.client-actor-props-factory.extension-class", MockClientActorPropsFactory.class.getName(), "ditto.connectivity.connection.allowed-hostnames", - ConfigValueFactory.fromAnyRef("127.0.0.1"), + ConfigValueFactory.fromAnyRef("127.0.0.1,hono-endpoint"), "ditto.connectivity.connection.blocked-hostnames", ConfigValueFactory.fromAnyRef("127.0.0.2"), "ditto.extensions.custom-connectivity-command-interceptor-provider", - MockCommandValidator.class.getName() + MockCommandValidator.class.getName(), + "ditto.extensions.hono-connection-factory", + ConfigValueFactory.fromAnyRef("org.eclipse.ditto.connectivity.service.messaging.hono.DefaultHonoConnectionFactory"), + "ditto.connectivity.hono.base-uri", ConfigValueFactory.fromAnyRef("tcp://localhost:9922"), + "ditto.connectivity.hono.validate-certificates", ConfigValueFactory.fromAnyRef("false"), + "ditto.connectivity.hono.sasl-mechanism", ConfigValueFactory.fromAnyRef("PLAIN"), + "ditto.connectivity.hono.bootstrap-servers", ConfigValueFactory.fromAnyRef("tcp://server1:port1,tcp://server2:port2,tcp://server3:port3") )).withFallback(TestConstants.CONFIG)); @Rule @@ -185,6 +195,38 @@ public void testConnection() { testProbe.expectMsg(TestConnectionResponse.success(connectionId, "mock", testConnection.getDittoHeaders())); } + @Test + public void testConnectionTypeHono() throws IOException { + //GIVEN + final var honoConnection = generateConnectionObjectFromJsonFile("hono-connection-custom-test.json"); + final var expectedHonoConnection = generateConnectionObjectFromJsonFile("hono-connection-custom-expected.json"); + final var testConnection = TestConnection.of(honoConnection, dittoHeadersWithCorrelationId); + final var testProbe = actorSystemResource1.newTestProbe(); + final var connectionSupervisorActor = createSupervisor(); + + //WHEN + connectionSupervisorActor.tell(testConnection, testProbe.ref()); + + //THEN + var testConnectionWithDryRunHeader = TestConnection.of(expectedHonoConnection, dittoHeadersWithCorrelationId + .toBuilder() + .dryRun(true) + .build()); + expectMockClientActorMessage(testConnectionWithDryRunHeader); + mockClientActorProbe.reply(new Status.Success("mock")); + testProbe.expectMsg(TestConnectionResponse.success(honoConnection.getId(), "mock", testConnection.getDittoHeaders())); + } + + private static Connection generateConnectionObjectFromJsonFile( String fileName) throws IOException { + final var testClassLoader = DefaultHonoConnectionFactoryTest.class.getClassLoader(); + try (final var connectionJsonFileStreamReader = new InputStreamReader( + testClassLoader.getResourceAsStream(fileName) + )) { + return ConnectivityModelFactory.connectionFromJson( + JsonFactory.readFrom(connectionJsonFileStreamReader).asObject()); + } + } + @Test public void testConnectionCausingFailure() { //GIVEN diff --git a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java index 546177d123..b4190e8d56 100644 --- a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java +++ b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java @@ -51,9 +51,7 @@ import org.eclipse.ditto.gateway.service.endpoints.directives.auth.DevopsAuthenticationDirective; import org.eclipse.ditto.gateway.service.endpoints.routes.AbstractRoute; import org.eclipse.ditto.gateway.service.endpoints.routes.RouteBaseProperties; -import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonObjectBuilder; import akka.http.javadsl.model.MediaTypes; import akka.http.javadsl.server.PathMatchers; @@ -144,9 +142,9 @@ private Route connections(final RequestContext ctx, final DittoHeaders dittoHead concat( get(() -> // GET /connections?ids-only=false parameterOptional(ConnectionsParameter.IDS_ONLY.toString(), idsOnly -> handlePerRequest(ctx, - RetrieveConnections.newInstance(idsOnly.map(Boolean::valueOf).orElse(false), - dittoHeaders) - )) + RetrieveConnections.newInstance(idsOnly.map(Boolean::valueOf).orElse(false), + dittoHeaders) + )) ), post(() -> // POST /connections?dry-run= @@ -326,12 +324,12 @@ private static Connection buildConnectionForTest(final String connectionJson) { final var connectionJsonObject = getConnectionJsonObjectOrThrow(connectionJson); final var temporaryTestConnectionId = UUID.randomUUID() + "-dry-run"; return ConnectivityModelFactory.connectionFromJson( - setUriForHonoConnectionType( - getConnectionId(connectionJsonObject) - .map(connectionId -> connectionJson.replace(connectionId, temporaryTestConnectionId)) - .map(ConnectionsRoute::getConnectionJsonObjectOrThrow) - .orElseGet(() -> setConnectionId(connectionJsonObject, temporaryTestConnectionId)) - ) + setUriForHonoConnectionType( + getConnectionId(connectionJsonObject) + .map(connectionId -> connectionJson.replace(connectionId, temporaryTestConnectionId)) + .map(ConnectionsRoute::getConnectionJsonObjectOrThrow) + .orElseGet(() -> setConnectionId(connectionJsonObject, temporaryTestConnectionId)) + ) ); } From 5c5564eef7050f1c2e046641dedf2e3a39c1bf9f Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Thu, 13 Oct 2022 11:46:22 +0300 Subject: [PATCH 45/65] HubParamsModified renamed to HubInstanceInfoModified, few logs added Signed-off-by: Andrey Balarev --- .../messaging/persistence/ConnectionPersistenceActor.java | 8 ++++---- .../persistentactors/AbstractPersistenceSupervisor.java | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index 59759ef772..43bd4efa13 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -814,12 +814,12 @@ private void testConnection(final StagedCommand command) { origin.tell(TestConnectionResponse.alreadyCreated(entityId, command.getDittoHeaders()), self); } else { final TestConnection testConnection; - TestConnection connection = (TestConnection) command.getCommand().setDittoHeaders(headersWithDryRun); - if (connection.getConnection().getConnectionType() == ConnectionType.HONO) { + TestConnection testConnectionUnresolved = (TestConnection) command.getCommand().setDittoHeaders(headersWithDryRun); + if (testConnectionUnresolved.getConnection().getConnectionType() == ConnectionType.HONO) { testConnection = TestConnection.of( - honoConnectionFactory.getHonoConnection(connection.getConnection()), headersWithDryRun); + honoConnectionFactory.getHonoConnection(testConnectionUnresolved.getConnection()), headersWithDryRun); } else { - testConnection = connection; + testConnection = testConnectionUnresolved; } // no need to start more than 1 client for tests // set connection status to CLOSED so that client actors will not try to connect on startup diff --git a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceSupervisor.java b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceSupervisor.java index f69af02931..0583ee61fb 100644 --- a/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceSupervisor.java +++ b/internal/utils/persistent-actors/src/main/java/org/eclipse/ditto/internal/utils/persistentactors/AbstractPersistenceSupervisor.java @@ -463,6 +463,7 @@ private void ensureEnforcerActorBeingStarted() { protected void restartChild() { if (persistenceActorChild != null) { + log.debug("Restarting persistence child actor."); waitingForStopBeforeRestart = true; getContext().stop(persistenceActorChild); // start happens when "Terminated" message is received. } From 29fd5a3a44707c8e3369f9cdf2eefbcc8a3a202e Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Thu, 13 Oct 2022 22:42:02 +0300 Subject: [PATCH 46/65] CR-11683 Bug fixed in handling of 'Event' Signed-off-by: Andrey Balarev --- .../config/ConnectionConfigProvider.java | 3 ++- .../ConnectivityConfigModifiedBehavior.java | 23 +++++++++++-------- .../config/DittoConnectionConfigProvider.java | 2 +- .../ConnectionSupervisorActor.java | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java index bed7376d5b..ad43dbadf1 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java @@ -69,7 +69,8 @@ CompletionStage registerForConnectivityConfigChanges(ConnectionId connecti * * @param event the event used to create a config which should overwrite the default connectivity config. * In case of Hub params changed, a null value is passed, which will invoke unconditional connection restart + * @param subscriber the actor that potentially will receive a command from this handler. */ - void handleEvent(Event event); + void handleEvent(Event event, ActorRef subscriber); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java index 5aeea71401..8460eb43a8 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java @@ -18,6 +18,7 @@ import akka.actor.AbstractActor; import akka.actor.Actor; +import akka.actor.ActorRef; import akka.japi.pf.ReceiveBuilder; /** @@ -25,25 +26,27 @@ */ public interface ConnectivityConfigModifiedBehavior extends Actor { - /** - * Injectable behavior to handle an {@code Event} that transports config changes. - * This involves modified Hub parameters (credentials) for 'Hono'-connections as well. - * - * @return behavior to handle an {@code Event} that transports config changes. - */ - default AbstractActor.Receive connectivityConfigModifiedBehavior() { + /** + * Injectable behavior to handle an {@code Event} that transports config changes. + * This involves modified Hub parameters (credentials) for 'Hono'-connections as well. + * + * @param subscriber the actor that potentially will receive a command message after handling the event. + * @return behavior to handle an {@code Event} that transports config changes. + */ + default AbstractActor.Receive connectivityConfigModifiedBehavior(ActorRef subscriber) { return ReceiveBuilder.create() - .match(Event.class, event -> getConnectivityConfigProvider().canHandle(event), this::handleEvent) + .match(Event.class, event -> getConnectivityConfigProvider().canHandle(event), event -> handleEvent(event, subscriber)) .build(); } /** * Handles the received event by converting it to a {@link Config}. * + * @param subscriber the actor that potentially will receive a command message from this handler. * @param event the received event */ - default void handleEvent(final Event event) { - getConnectivityConfigProvider().handleEvent(event); + default void handleEvent(final Event event, ActorRef subscriber) { + getConnectivityConfigProvider().handleEvent(event, subscriber); } /** diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java index c1ca4b292a..49b1cbac20 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java @@ -57,7 +57,7 @@ public boolean canHandle(final Event event) { } @Override - public void handleEvent(final Event event) { + public void handleEvent(final Event event, ActorRef subscriber) { // By default not handled in Ditto } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java index d738491976..d7a4d40965 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java @@ -152,7 +152,7 @@ protected Receive activeBehaviour( isRegisteredForConnectivityConfigChanges = true; }) .build() - .orElse(connectivityConfigModifiedBehavior()) + .orElse(connectivityConfigModifiedBehavior(getSelf())) .orElse(super.activeBehaviour(matchProcessNextTwinMessageBehavior, matchAnyBehavior)); } From 1886e85ad98741ee8b7315dcb9a30ccc67f64590 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Mon, 17 Oct 2022 17:15:45 +0300 Subject: [PATCH 47/65] Review issues fixes. Signed-off-by: Andrey Balarev --- .../connectivity/model/ConnectionType.java | 1 + .../connectivity/model/HonoAddressAlias.java | 1 + .../model/UserPasswordCredentials.java | 1 + .../query/RetrieveHonoConnectionResponse.java | 203 ------------------ ...va => RetrieveResolvedHonoConnection.java} | 28 +-- .../RetrieveHonoConnectionResponseTest.java | 104 --------- ...> RetrieveResolvedHonoConnectionTest.java} | 36 ++-- .../config/ConnectionConfigProvider.java | 2 +- .../ConnectivityConfigModifiedBehavior.java | 2 +- .../hono/DefaultHonoConnectionFactory.java | 2 - .../messaging/hono/HonoConnectionFactory.java | 2 - .../service/messaging/hono/package-info.java | 2 +- .../ConnectionPersistenceActor.java | 6 +- .../ConnectionSupervisorActor.java | 10 +- .../commands/ConnectionCreatedStrategies.java | 13 +- ...trieveResolvedHonoConnectionStrategy.java} | 22 +- .../resources/connectivity-extension.conf | 10 - .../src/main/resources/connectivity.conf | 9 +- .../DefaultClientActorPropsFactoryTest.java | 2 +- .../hono-connection-custom-expected.json | 57 +---- .../hono-connection-custom-test.json | 57 +---- .../hono-connection-default-test.json | 57 +---- 22 files changed, 101 insertions(+), 526 deletions(-) delete mode 100644 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java rename connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/{RetrieveHonoConnection.java => RetrieveResolvedHonoConnection.java} (80%) delete mode 100644 connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponseTest.java rename connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/{RetrieveHonoConnectionTest.java => RetrieveResolvedHonoConnectionTest.java} (71%) rename connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/{RetrieveHonoConnectionStrategy.java => RetrieveResolvedHonoConnectionStrategy.java} (74%) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java index c338acfc5f..3ee56f53f7 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java @@ -54,6 +54,7 @@ public enum ConnectionType implements CharSequence { /** * Indicates a connection to Eclipse Hono. + * @since 3.1.0 */ HONO("hono"); diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 5dc0d74340..574e97ea9e 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -23,6 +23,7 @@ /** * Possible address aliases used by connections of type 'Hono' + * @since 3.1.0 */ public enum HonoAddressAlias { diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java index 220c28e026..fb49e72a33 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java @@ -115,6 +115,7 @@ public static UserPasswordCredentials newInstance(final String username, final S * * @param jsonObject the jsonObject * @return credentials. + * @since 3.1.0 */ public static UserPasswordCredentials newInstance(final JsonObject jsonObject) { return UserPasswordCredentials.fromJson(jsonObject); diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java deleted file mode 100644 index 9c8bb72974..0000000000 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponse.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2022 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.connectivity.model.signals.commands.query; - -import static org.eclipse.ditto.base.model.common.ConditionChecker.checkArgument; -import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; - -import java.util.Collections; -import java.util.Objects; -import java.util.function.Predicate; - -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; - -import org.eclipse.ditto.base.model.common.HttpStatus; -import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.base.model.json.FieldType; -import org.eclipse.ditto.base.model.json.JsonParsableCommandResponse; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; -import org.eclipse.ditto.base.model.signals.SignalWithEntityId; -import org.eclipse.ditto.base.model.signals.commands.AbstractCommandResponse; -import org.eclipse.ditto.base.model.signals.commands.CommandResponseHttpStatusValidator; -import org.eclipse.ditto.base.model.signals.commands.CommandResponseJsonDeserializer; -import org.eclipse.ditto.connectivity.model.Connection; -import org.eclipse.ditto.connectivity.model.ConnectionId; -import org.eclipse.ditto.connectivity.model.ConnectionType; -import org.eclipse.ditto.connectivity.model.WithConnectionId; -import org.eclipse.ditto.json.JsonField; -import org.eclipse.ditto.json.JsonFieldDefinition; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonObjectBuilder; -import org.eclipse.ditto.json.JsonValue; - -/** - * Response to a {@link RetrieveConnection} command. - */ -@Immutable -@JsonParsableCommandResponse(type = RetrieveHonoConnectionResponse.TYPE) -public final class RetrieveHonoConnectionResponse extends AbstractCommandResponse - implements ConnectivityQueryCommandResponse, WithConnectionId, - SignalWithEntityId { - - /** - * Type of this response. - */ - public static final String TYPE = TYPE_PREFIX + RetrieveHonoConnection.NAME; - - static final JsonFieldDefinition JSON_CONNECTION = - JsonFieldDefinition.ofJsonObject("connection", FieldType.REGULAR, JsonSchemaVersion.V_2); - - private static final HttpStatus HTTP_STATUS = HttpStatus.OK; - - private static final CommandResponseJsonDeserializer JSON_DESERIALIZER = - CommandResponseJsonDeserializer.newInstance(TYPE, - context -> { - final JsonObject jsonObject = context.getJsonObject(); - return new RetrieveHonoConnectionResponse(jsonObject.getValueOrThrow(JSON_CONNECTION), - context.getDeserializedHttpStatus(), - context.getDittoHeaders()); - }); - - private final JsonObject connection; - - private RetrieveHonoConnectionResponse(final JsonObject connection, - final HttpStatus httpStatus, - final DittoHeaders dittoHeaders) { - - super(TYPE, - CommandResponseHttpStatusValidator.validateHttpStatus(httpStatus, - Collections.singleton(HTTP_STATUS), - RetrieveHonoConnectionResponse.class), - dittoHeaders); - this.connection = checkNotNull(connection, "connection"); - checkArgument(connection.getValueOrThrow(Connection.JsonFields.CONNECTION_TYPE), - ConnectionType.HONO.getName()::equals, - () -> "The connection must be of type 'Hono'!"); - } - - /** - * Returns a new instance of {@code RetrieveHonoConnectionResponse}. - * - * @param connection the retrieved jsonObject. - * @param dittoHeaders the headers of the request. - * @return a new RetrieveHonoConnectionResponse response. - * @throws NullPointerException if any argument is {@code null}. - * @throws IllegalArgumentException if the connection type is not 'Hono'. - */ - public static RetrieveHonoConnectionResponse of(final JsonObject connection, final DittoHeaders dittoHeaders) { - return new RetrieveHonoConnectionResponse(connection, HTTP_STATUS, dittoHeaders); - } - - /** - * Creates a new {@code RetrieveHonoConnectionResponse} from a JSON string. - * - * @param jsonString the JSON string of which the response is to be retrieved. - * @param dittoHeaders the headers of the response. - * @return the response. - * @throws NullPointerException if {@code jsonString} is {@code null}. - * @throws IllegalArgumentException if {@code jsonString} is empty or if the connection type is not 'Hono'. - * @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonString} was not in the expected - * format. - * @throws org.eclipse.ditto.json.JsonMissingFieldException if connectionId is missing in the passed in {@code jsonString} - */ - public static RetrieveHonoConnectionResponse fromJson(final String jsonString, final DittoHeaders dittoHeaders) { - return fromJson(JsonObject.of(jsonString), dittoHeaders); - } - - /** - * Creates a new {@code RetrieveHonoConnectionResponse} from a JSON object. - * - * @param jsonObject the JSON object of which the response is to be created. - * @param dittoHeaders the headers of the response. - * @return the response. - * @throws NullPointerException if {@code jsonObject} is {@code null}. - * @throws IllegalArgumentException if the connection type is not 'Hono'. - * @throws org.eclipse.ditto.json.JsonParseException if the passed in {@code jsonObject} was not in the expected - * format. - * @throws org.eclipse.ditto.json.JsonMissingFieldException if connectionId is missing in the passed in {@code jsonString} - */ - public static RetrieveHonoConnectionResponse fromJson(final JsonObject jsonObject, - final DittoHeaders dittoHeaders) { - return JSON_DESERIALIZER.deserialize(jsonObject, dittoHeaders); - } - - @Override - protected void appendPayload(final JsonObjectBuilder jsonObjectBuilder, final JsonSchemaVersion schemaVersion, - final Predicate thePredicate) { - - final Predicate predicate = schemaVersion.and(thePredicate); - jsonObjectBuilder.set(JSON_CONNECTION, connection, predicate); - } - - /** - * @return the {@code JsonObject} of the connection. - */ - public JsonObject getJsonObject() { - return connection; - } - - @Override - public ConnectionId getEntityId() { - return ConnectionId.of(connection.getValueOrThrow(Connection.JsonFields.ID)); - } - - @Override - public RetrieveHonoConnectionResponse setEntity(final JsonValue entity) { - return of(entity.asObject(), getDittoHeaders()); - } - - @Override - public JsonValue getEntity(final JsonSchemaVersion schemaVersion) { - return connection; - } - - @Override - public RetrieveHonoConnectionResponse setDittoHeaders(final DittoHeaders dittoHeaders) { - return of(connection, dittoHeaders); - } - - @Override - protected boolean canEqual(@Nullable final Object other) { - return other instanceof RetrieveHonoConnectionResponse; - } - - @Override - public boolean equals(@Nullable final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } - final RetrieveHonoConnectionResponse that = (RetrieveHonoConnectionResponse) o; - return Objects.equals(connection, that.connection); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), connection); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " [" + - super.toString() + - ", jsonObject=" + connection + - "]"; - } - -} diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnection.java similarity index 80% rename from connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnection.java rename to connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnection.java index 9cfb29a798..8d4ba579c8 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnection.java @@ -37,16 +37,18 @@ /** * Command which retrieves a {@link org.eclipse.ditto.connectivity.model.Connection} of type 'hono' * after resolving its aliases and with its additional properties like header mappings and specific config. + * + * @since 3.1.0 */ @Immutable -@JsonParsableCommand(typePrefix = ConnectivityCommand.TYPE_PREFIX, name = RetrieveHonoConnection.NAME) -public final class RetrieveHonoConnection extends AbstractCommand - implements ConnectivityQueryCommand, WithConnectionId, SignalWithEntityId { +@JsonParsableCommand(typePrefix = ConnectivityCommand.TYPE_PREFIX, name = RetrieveResolvedHonoConnection.NAME) +public final class RetrieveResolvedHonoConnection extends AbstractCommand + implements ConnectivityQueryCommand, WithConnectionId, SignalWithEntityId { /** * Name of this command. */ - public static final String NAME = "retrieveHonoConnection"; + public static final String NAME = "retrieveResolvedHonoConnection"; /** * Type of this command. @@ -55,7 +57,7 @@ public final class RetrieveHonoConnection extends AbstractCommand(TYPE, jsonObject).deserialize(() -> { + public static RetrieveResolvedHonoConnection fromJson(final JsonObject jsonObject, final DittoHeaders dittoHeaders) { + return new CommandJsonDeserializer(TYPE, jsonObject).deserialize(() -> { final String readConnectionId = jsonObject.getValueOrThrow(ConnectivityCommand.JsonFields.JSON_CONNECTION_ID); final ConnectionId connectionId = ConnectionId.of(readConnectionId); @@ -127,13 +129,13 @@ public Category getCategory() { } @Override - public RetrieveHonoConnection setDittoHeaders(final DittoHeaders dittoHeaders) { + public RetrieveResolvedHonoConnection setDittoHeaders(final DittoHeaders dittoHeaders) { return of(connectionId, dittoHeaders); } @Override protected boolean canEqual(@Nullable final Object other) { - return other instanceof RetrieveHonoConnection; + return other instanceof RetrieveResolvedHonoConnection; } @Override @@ -147,7 +149,7 @@ public boolean equals(@Nullable final Object o) { if (!super.equals(o)) { return false; } - final RetrieveHonoConnection that = (RetrieveHonoConnection) o; + final RetrieveResolvedHonoConnection that = (RetrieveResolvedHonoConnection) o; return Objects.equals(connectionId, that.connectionId); } diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponseTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponseTest.java deleted file mode 100644 index 18e212fd5d..0000000000 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionResponseTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2022 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.connectivity.model.signals.commands.query; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mutabilitydetector.unittesting.AllowedReason.provided; -import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; -import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; - -import org.eclipse.ditto.base.model.common.HttpStatus; -import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.base.model.signals.commands.CommandResponse; -import org.eclipse.ditto.connectivity.model.MappingContext; -import org.eclipse.ditto.connectivity.model.signals.commands.TestConstants; -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonPointer; -import org.eclipse.ditto.json.assertions.DittoJsonAssertions; -import org.junit.Test; - -import nl.jqno.equalsverifier.EqualsVerifier; - -/** - * Unit test for {@link RetrieveHonoConnectionResponse}. - */ -public final class RetrieveHonoConnectionResponseTest { - - private static final JsonObject KNOWN_JSON = JsonObject.newBuilder() - .set(CommandResponse.JsonFields.TYPE, RetrieveHonoConnectionResponse.TYPE) - .set(CommandResponse.JsonFields.STATUS, HttpStatus.OK.getCode()) - .set(RetrieveHonoConnectionResponse.JSON_CONNECTION, TestConstants.HONO_CONNECTION.toJson()) - .build(); - - @Test - public void testHashCodeAndEquals() { - EqualsVerifier.forClass(RetrieveHonoConnectionResponse.class) - .usingGetClass() - .verify(); - } - - @Test - public void assertImmutability() { - assertInstancesOf(RetrieveHonoConnectionResponse.class, - areImmutable(), - provided(JsonObject.class, MappingContext.class).isAlsoImmutable()); - } - - @Test - public void retrieveInstanceWithNullConnection() { - assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> RetrieveHonoConnectionResponse.of(null, DittoHeaders.empty())) - .withMessage("The %s must not be null!", "connection") - .withNoCause(); - } - - @Test - public void retrieveInstanceWithNonHonoConnection() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> RetrieveHonoConnectionResponse.of(TestConstants.CONNECTION.toJson(), DittoHeaders.empty())) - .withMessage("The %s must be of type 'Hono'!", "connection") - .withNoCause(); - } - - @Test - public void fromJsonReturnsExpected() { - final RetrieveHonoConnectionResponse expected = - RetrieveHonoConnectionResponse.of(TestConstants.HONO_CONNECTION.toJson(), DittoHeaders.empty()); - - final RetrieveHonoConnectionResponse actual = - RetrieveHonoConnectionResponse.fromJson(KNOWN_JSON, DittoHeaders.empty()); - - assertThat(actual).isEqualTo(expected); - } - - @Test - public void toJsonReturnsExpected() { - final RetrieveHonoConnectionResponse underTest = - RetrieveHonoConnectionResponse.of(TestConstants.HONO_CONNECTION.toJson(), DittoHeaders.empty()); - - assertThat(underTest.toJson()).isEqualTo(KNOWN_JSON); - } - - @Test - public void getResourcePathReturnsExpected() { - final JsonPointer expectedResourcePath = JsonFactory.emptyPointer(); - - final RetrieveHonoConnectionResponse underTest = - RetrieveHonoConnectionResponse.of(TestConstants.HONO_CONNECTION.toJson(), DittoHeaders.empty()); - - DittoJsonAssertions.assertThat(underTest.getResourcePath()).isEqualTo(expectedResourcePath); - } - -} diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnectionTest.java similarity index 71% rename from connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionTest.java rename to connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnectionTest.java index 01b39aa3a6..94bd12fb22 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveHonoConnectionTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnectionTest.java @@ -35,25 +35,25 @@ import nl.jqno.equalsverifier.EqualsVerifier; /** - * Unit test for {@link RetrieveHonoConnection}. + * Unit test for {@link RetrieveResolvedHonoConnection}. */ -public final class RetrieveHonoConnectionTest { +public final class RetrieveResolvedHonoConnectionTest { private static final JsonObject KNOWN_JSON = JsonObject.newBuilder() - .set(Command.JsonFields.TYPE, RetrieveHonoConnection.TYPE) + .set(Command.JsonFields.TYPE, RetrieveResolvedHonoConnection.TYPE) .set(ConnectivityCommand.JsonFields.JSON_CONNECTION_ID, TestConstants.ID.toString()) .build(); @Test public void testHashCodeAndEquals() { - EqualsVerifier.forClass(RetrieveHonoConnection.class) + EqualsVerifier.forClass(RetrieveResolvedHonoConnection.class) .usingGetClass() .verify(); } @Test public void assertImmutability() { - assertInstancesOf(RetrieveHonoConnection.class, + assertInstancesOf(RetrieveResolvedHonoConnection.class, areImmutable(), provided(ConnectionId.class).isAlsoImmutable()); } @@ -61,18 +61,18 @@ public void assertImmutability() { @Test public void createInstanceWithNullConnectionId() { assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> RetrieveHonoConnection.of(null, DittoHeaders.empty())) + .isThrownBy(() -> RetrieveResolvedHonoConnection.of(null, DittoHeaders.empty())) .withMessage("The %s must not be null!", "connectionId") .withNoCause(); } @Test public void fromJsonReturnsExpected() { - final RetrieveHonoConnection expected = - RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); + final RetrieveResolvedHonoConnection expected = + RetrieveResolvedHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); - final RetrieveHonoConnection actual = - RetrieveHonoConnection.fromJson(KNOWN_JSON, DittoHeaders.empty()); + final RetrieveResolvedHonoConnection actual = + RetrieveResolvedHonoConnection.fromJson(KNOWN_JSON, DittoHeaders.empty()); assertThat(actual).isEqualTo(expected); } @@ -80,15 +80,15 @@ public void fromJsonReturnsExpected() { @Test public void toJsonReturnsExpected() { final JsonObject actual = - RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()).toJson(); + RetrieveResolvedHonoConnection.of(TestConstants.ID, DittoHeaders.empty()).toJson(); assertThat(actual).isEqualTo(KNOWN_JSON); } @Test public void getEntityIdReturnsExpected() { - final RetrieveHonoConnection actual = - RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); + final RetrieveResolvedHonoConnection actual = + RetrieveResolvedHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); assertThat((CharSequence) actual.getEntityId()).isEqualTo(ConnectionId.of(TestConstants.ID)); } @@ -100,11 +100,11 @@ public void setDittoHeadersReturnsExpected() { map.put("header2_key", "header2_value"); map.put("header3_key", "header3_value"); final DittoHeaders EXPECTED_DITTO_HEADERS = DittoHeaders.of(map); - final RetrieveHonoConnection actual = - RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); + final RetrieveResolvedHonoConnection actual = + RetrieveResolvedHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); assertThat(actual.getDittoHeaders()).isEmpty(); - RetrieveHonoConnection changed = actual.setDittoHeaders(EXPECTED_DITTO_HEADERS); + RetrieveResolvedHonoConnection changed = actual.setDittoHeaders(EXPECTED_DITTO_HEADERS); assertThat(changed.getDittoHeaders()).isEqualTo(EXPECTED_DITTO_HEADERS); } @@ -112,8 +112,8 @@ public void setDittoHeadersReturnsExpected() { public void getResourcePathReturnsExpected() { final JsonPointer expectedResourcePath = JsonFactory.emptyPointer(); - final RetrieveHonoConnection underTest = - RetrieveHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); + final RetrieveResolvedHonoConnection underTest = + RetrieveResolvedHonoConnection.of(TestConstants.ID, DittoHeaders.empty()); DittoJsonAssertions.assertThat(underTest.getResourcePath()).isEqualTo(expectedResourcePath); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java index ad43dbadf1..fc9e9c98f6 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java @@ -57,7 +57,7 @@ CompletionStage registerForConnectivityConfigChanges(ConnectionId connecti /** * Returns {@code true} if the implementation can handle the given {@code event} to generate a modified {@link - * ConnectivityConfig} when passed to {@link #handleEvent(Event)}. + * ConnectivityConfig} when passed to {@link #handleEvent(Event, akka.actor.ActorRef)}. * * @param event the event that may be used to generate modified config * @return {@code true} if the event is compatible diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java index 8460eb43a8..de9024e6a8 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java @@ -28,7 +28,7 @@ public interface ConnectivityConfigModifiedBehavior extends Actor { /** * Injectable behavior to handle an {@code Event} that transports config changes. - * This involves modified Hub parameters (credentials) for 'Hono'-connections as well. + * This involves modified credentials for Hono-connections as well. * * @param subscriber the actor that potentially will receive a command message after handling the event. * @return behavior to handle an {@code Event} that transports config changes. diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java index 62a2ec2780..18c806b30f 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/DefaultHonoConnectionFactory.java @@ -28,8 +28,6 @@ /** * Default implementation of {@link HonoConnectionFactory}. * This implementation uses {@link HonoConfig} to obtain the required properties for creating the Hono connection. - * - * @since 3.0.0 */ public final class DefaultHonoConnectionFactory extends HonoConnectionFactory { diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java index fc545a0948..3057a0ea4d 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoConnectionFactory.java @@ -58,8 +58,6 @@ *
  • the credentials and
  • *
  • the sources and targets.
  • * - * - * @since 3.0.0 */ public abstract class HonoConnectionFactory implements DittoExtensionPoint { diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/package-info.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/package-info.java index 8665cb6659..4cc3440549 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/package-info.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/hono/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index 43bd4efa13..5539e8eb23 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -173,6 +173,7 @@ public final class ConnectionPersistenceActor // never retry, just escalate. ConnectionSupervisorActor will handle restarting this actor private static final SupervisorStrategy ESCALATE_ALWAYS_STRATEGY = OneForOneEscalateStrategy.escalateStrategy(); + private final ActorSystem actorSystem; private final Cluster cluster; private final ActorRef commandForwarderActor; private final ClientActorPropsFactory propsFactory; @@ -204,7 +205,7 @@ public final class ConnectionPersistenceActor final Config connectivityConfigOverwrites) { super(connectionId); - final ActorSystem actorSystem = context().system(); + this.actorSystem = context().system(); cluster = Cluster.get(actorSystem); final Config dittoExtensionConfig = ScopedConfig.dittoExtension(actorSystem.settings().config()); this.commandForwarderActor = commandForwarderActor; @@ -314,7 +315,7 @@ protected CommandStrategy.Context getStrategyContext() { @Override protected ConnectionCreatedStrategies getCreatedStrategy() { - return ConnectionCreatedStrategies.getInstance(); + return ConnectionCreatedStrategies.getInstance(actorSystem); } @Override @@ -1186,7 +1187,6 @@ private void restoreOpenConnection() { private ConnectivityCommandInterceptor getCommandValidator() { - final var actorSystem = getContext().getSystem(); final MqttConfig mqttConfig = connectivityConfig.getConnectionConfig().getMqttConfig(); final ConnectionValidator connectionValidator = ConnectionValidator.of(actorSystem.log(), diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java index d7a4d40965..63b928d42e 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java @@ -172,8 +172,7 @@ protected void handleMessagesDuringStartup(final Object message) { // only after the config overwrites were retrieved, the ConnectionSupervisorActor // will move to the "initialized" state and open the connection initConfigOverwrites(entityId, null); - } else if (message != Control.REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL - && message != Control.REGISTRATION_FOR_HUB_PARAMS_CHANGES_SUCCESSFUL) { + } else if (message != Control.REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL) { initConfigOverwrites(entityId, null); } getContext().cancelReceiveTimeout(); @@ -296,8 +295,7 @@ private Config handleConfigRetrievalException(final Throwable throwable, } private enum Control { - REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL, - REGISTRATION_FOR_HUB_PARAMS_CHANGES_SUCCESSFUL + REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL } private static class CheckForOverwritesConfig { @@ -310,9 +308,9 @@ private CheckForOverwritesConfig(@Nullable final DittoHeaders dittoHeaders) { } /** - * Command to restart the connection. Used in case the config or hub params ('hono'-connections only) are modified. + * Command to restart the connection with a modified Config (provided in the command) or unconditional restart if modifiedConfig is null. */ - public static class RestartConnection { + public static final class RestartConnection { @Nullable private final Config modifiedConfig; diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java index e099189886..72d3583670 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java @@ -25,6 +25,8 @@ import org.eclipse.ditto.internal.utils.persistentactors.results.Result; import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory; +import akka.actor.ActorSystem; + /** * Strategies to handle signals as an existing connection. */ @@ -32,20 +34,19 @@ public final class ConnectionCreatedStrategies extends AbstractCommandStrategies, Connection, ConnectionState, ConnectivityEvent> implements ConnectivityCommandStrategies { - private static final ConnectionCreatedStrategies CREATED_STRATEGIES = newCreatedStrategies(); - private ConnectionCreatedStrategies() { super(Command.class); } /** + * @param actorSystem Actor system reference * @return the unique instance of this class. */ - public static ConnectionCreatedStrategies getInstance() { - return CREATED_STRATEGIES; + public static ConnectionCreatedStrategies getInstance(final ActorSystem actorSystem) { + return newCreatedStrategies(actorSystem); } - private static ConnectionCreatedStrategies newCreatedStrategies() { + private static ConnectionCreatedStrategies newCreatedStrategies(final ActorSystem actorSystem) { final ConnectionCreatedStrategies strategies = new ConnectionCreatedStrategies(); strategies.addStrategy(new StagedCommandStrategy()); strategies.addStrategy(new TestConnectionConflictStrategy()); @@ -59,7 +60,7 @@ private static ConnectionCreatedStrategies newCreatedStrategies() { strategies.addStrategy(new RetrieveConnectionLogsStrategy()); strategies.addStrategy(new ResetConnectionLogsStrategy()); strategies.addStrategy(new RetrieveConnectionStrategy()); - strategies.addStrategy(new RetrieveHonoConnectionStrategy()); + strategies.addStrategy(new RetrieveResolvedHonoConnectionStrategy(actorSystem)); strategies.addStrategy(new RetrieveConnectionStatusStrategy()); strategies.addStrategy(new RetrieveConnectionMetricsStrategy()); strategies.addStrategy(new LoggingExpiredStrategy()); diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveHonoConnectionStrategy.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveResolvedHonoConnectionStrategy.java similarity index 74% rename from connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveHonoConnectionStrategy.java rename to connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveResolvedHonoConnectionStrategy.java index b9a59c7686..579605b18b 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveHonoConnectionStrategy.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveResolvedHonoConnectionStrategy.java @@ -17,36 +17,38 @@ import org.eclipse.ditto.base.model.entity.metadata.Metadata; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionType; -import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnection; import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveConnectionResponse; -import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveHonoConnection; +import org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveResolvedHonoConnection; import org.eclipse.ditto.connectivity.model.signals.events.ConnectivityEvent; import org.eclipse.ditto.connectivity.service.messaging.hono.HonoConnectionFactory; import org.eclipse.ditto.connectivity.service.messaging.persistence.stages.ConnectionState; import org.eclipse.ditto.internal.utils.persistentactors.results.Result; import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory; +import akka.actor.ActorSystem; + /** - * This strategy handles the {@link RetrieveConnection} command. + * This strategy handles the {@link org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveResolvedHonoConnection} command. */ -final class RetrieveHonoConnectionStrategy extends AbstractConnectivityCommandStrategy { +final class RetrieveResolvedHonoConnectionStrategy + extends AbstractConnectivityCommandStrategy { + + private final HonoConnectionFactory honoConnectionFactory; - RetrieveHonoConnectionStrategy() { - super(RetrieveHonoConnection.class); + RetrieveResolvedHonoConnectionStrategy(final ActorSystem actorSystem) { + super(RetrieveResolvedHonoConnection.class); + this.honoConnectionFactory = HonoConnectionFactory.get(actorSystem, actorSystem.settings().config()); } @Override protected Result> doApply(final Context context, @Nullable final Connection entity, final long nextRevision, - final RetrieveHonoConnection command, + final RetrieveResolvedHonoConnection command, @Nullable final Metadata metadata) { final Result> result; if (entity != null && entity.getConnectionType() == ConnectionType.HONO) { - final var actorSystem = context.getActorSystem(); - final var honoConnectionFactory = HonoConnectionFactory.get( - actorSystem, actorSystem.settings().config()); final var json = honoConnectionFactory.getHonoConnection(entity).toJson(); result = ResultFactory.newQueryResult(command, diff --git a/connectivity/service/src/main/resources/connectivity-extension.conf b/connectivity/service/src/main/resources/connectivity-extension.conf index de025ddb35..e69de29bb2 100644 --- a/connectivity/service/src/main/resources/connectivity-extension.conf +++ b/connectivity/service/src/main/resources/connectivity-extension.conf @@ -1,10 +0,0 @@ -ditto { - mapping-strategy.implementation = "org.eclipse.ditto.connectivity.api.ConnectivityMappingStrategies" - connectivity.hono.credentials { - username = "honoUsername" - username = ${?HONO_CONNECTION_HONO_USERNAME} - - password = "honoPassword" - password = ${?HONO_CONNECTION_HONO_PASSWORD} - } -} \ No newline at end of file diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index 486e9f36ff..38d6a76083 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -1,5 +1,4 @@ ditto { - service-name = "connectivity" mongodb { @@ -66,6 +65,8 @@ ditto { snapshot-adapter = "org.eclipse.ditto.connectivity.service.messaging.persistence.ConnectionMongoSnapshotAdapter" } + mapping-strategy.implementation = "org.eclipse.ditto.connectivity.api.ConnectivityMappingStrategies" + persistence.operations.delay-after-persistence-actor-shutdown = 5s persistence.operations.delay-after-persistence-actor-shutdown = ${?DELAY_AFTER_PERSISTENCE_ACTOR_SHUTDOWN} @@ -83,6 +84,12 @@ ditto { bootstrap-servers = "bootstrap.server:9999" bootstrap-servers = ${?HONO_CONNECTION_BOOTSTRAP_SERVERS} + + username = "honoUsername" + username = ${?HONO_CONNECTION_HONO_USERNAME} + + password = "honoPassword" + password = ${?HONO_CONNECTION_HONO_PASSWORD} } user-indicated-errors-base = [ diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactoryTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactoryTest.java index 2483494112..8d0d756226 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactoryTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/DefaultClientActorPropsFactoryTest.java @@ -114,7 +114,7 @@ public void kafkaActorPropsIsSerializable() { } /** - * Tests serialization of props of Kafka client actor. The props needs to be serializable because client actors + * Tests serialization of props of Hono client actor. The props needs to be serializable because client actors * may be created on a different connectivity service instance using a local connection object. */ @Test diff --git a/connectivity/service/src/test/resources/hono-connection-custom-expected.json b/connectivity/service/src/test/resources/hono-connection-custom-expected.json index 2679545f97..c55d5c5216 100644 --- a/connectivity/service/src/test/resources/hono-connection-custom-expected.json +++ b/connectivity/service/src/test/resources/hono-connection-custom-expected.json @@ -12,7 +12,7 @@ "consumerCount": 1, "qos": 0, "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "enforcement": { "input": "{{ header:device_id }}", @@ -53,7 +53,7 @@ "consumerCount": 1, "qos": 1, "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "enforcement": { "input": "{{ header:device_id }}", @@ -93,7 +93,7 @@ "consumerCount": 1, "qos": 0, "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "enforcement": { "input": "{{ header:device_id }}", @@ -126,7 +126,7 @@ "_/_/things/live/commands" ], "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "headerMapping": { "user_key4": "user_value4", @@ -143,7 +143,7 @@ "_/_/things/live/events" ], "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "headerMapping": { "user_key5": "user_value5", @@ -191,8 +191,8 @@ "entries": { "DEVICE": { "subjects": { - "integration:{{solution:id}}:hub": { - "type": "iot-things-integration" + "integration:hono": { + "type": "hono-integration" } }, "resources": { @@ -220,47 +220,8 @@ }, "DEFAULT": { "subjects": { - "iot-suite-dev:/service-instance.{{solution:package-service-instance-id}}.iot-things": { - "type": "suite-auth" - } - }, - "resources": { - "policy:/": { - "revoke": [], - "grant": [ - "READ", - "WRITE" - ] - }, - "thing:/": { - "revoke": [], - "grant": [ - "READ", - "WRITE" - ] - }, - "message:/": { - "revoke": [], - "grant": [ - "READ", - "WRITE" - ] - } - } - }, - "DEVICE-MANAGEMENT": { - "subjects": { - "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-manager": { - "type": "suite-auth" - }, - "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@developer-console": { - "type": "suite-auth" - }, - "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-rollouts": { - "type": "suite-auth" - }, - "integration:{{ solution:id }}:iot-manager": { - "type": "iot-things-integration" + "integration:hono": { + "type": "generated" } }, "resources": { diff --git a/connectivity/service/src/test/resources/hono-connection-custom-test.json b/connectivity/service/src/test/resources/hono-connection-custom-test.json index 4f482e4e39..1c3b288695 100644 --- a/connectivity/service/src/test/resources/hono-connection-custom-test.json +++ b/connectivity/service/src/test/resources/hono-connection-custom-test.json @@ -12,7 +12,7 @@ "consumerCount": 1, "qos": 0, "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "enforcement": { "input": "{{ header:device_id }}", @@ -51,7 +51,7 @@ "consumerCount": 1, "qos": 1, "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "enforcement": { "input": "{{ header:device_id }}", @@ -89,7 +89,7 @@ "consumerCount": 1, "qos": 0, "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "enforcement": { "input": "{{ header:device_id }}", @@ -121,7 +121,7 @@ "_/_/things/live/commands" ], "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "headerMapping": { "user_key4": "user_value4", @@ -135,7 +135,7 @@ "_/_/things/live/events" ], "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "headerMapping": { "user_key5": "user_value5", @@ -179,8 +179,8 @@ "entries": { "DEVICE": { "subjects": { - "integration:{{solution:id}}:hub": { - "type": "iot-things-integration" + "integration:hono": { + "type": "hono-integration" } }, "resources": { @@ -208,47 +208,8 @@ }, "DEFAULT": { "subjects": { - "iot-suite-dev:/service-instance.{{solution:package-service-instance-id}}.iot-things": { - "type": "suite-auth" - } - }, - "resources": { - "policy:/": { - "revoke": [], - "grant": [ - "READ", - "WRITE" - ] - }, - "thing:/": { - "revoke": [], - "grant": [ - "READ", - "WRITE" - ] - }, - "message:/": { - "revoke": [], - "grant": [ - "READ", - "WRITE" - ] - } - } - }, - "DEVICE-MANAGEMENT": { - "subjects": { - "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-manager": { - "type": "suite-auth" - }, - "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@developer-console": { - "type": "suite-auth" - }, - "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-rollouts": { - "type": "suite-auth" - }, - "integration:{{ solution:id }}:iot-manager": { - "type": "iot-things-integration" + "integration:hono": { + "type": "generated" } }, "resources": { diff --git a/connectivity/service/src/test/resources/hono-connection-default-test.json b/connectivity/service/src/test/resources/hono-connection-default-test.json index 418c605863..7fdb31de0f 100644 --- a/connectivity/service/src/test/resources/hono-connection-default-test.json +++ b/connectivity/service/src/test/resources/hono-connection-default-test.json @@ -12,7 +12,7 @@ "consumerCount": 1, "qos": 0, "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "enforcement": { "input": "{{ header:device_id }}", @@ -49,7 +49,7 @@ "consumerCount": 1, "qos": 1, "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "enforcement": { "input": "{{ header:device_id }}", @@ -85,7 +85,7 @@ "consumerCount": 1, "qos": 0, "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "enforcement": { "input": "{{ header:device_id }}", @@ -115,7 +115,7 @@ "_/_/things/live/commands" ], "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ] }, { @@ -125,7 +125,7 @@ "_/_/things/live/events" ], "authorizationContext": [ - "integration:{{solution.id}}:hub" + "nginx:ditto" ], "headerMapping": { } @@ -164,8 +164,8 @@ "entries": { "DEVICE": { "subjects": { - "integration:{{solution:id}}:hub": { - "type": "iot-things-integration" + "integration:hono": { + "type": "hono-integration" } }, "resources": { @@ -193,47 +193,8 @@ }, "DEFAULT": { "subjects": { - "iot-suite-dev:/service-instance.{{solution:package-service-instance-id}}.iot-things": { - "type": "suite-auth" - } - }, - "resources": { - "policy:/": { - "revoke": [], - "grant": [ - "READ", - "WRITE" - ] - }, - "thing:/": { - "revoke": [], - "grant": [ - "READ", - "WRITE" - ] - }, - "message:/": { - "revoke": [], - "grant": [ - "READ", - "WRITE" - ] - } - } - }, - "DEVICE-MANAGEMENT": { - "subjects": { - "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-manager": { - "type": "suite-auth" - }, - "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@developer-console": { - "type": "suite-auth" - }, - "iot-suite-dev:/service-instance.{{ solution:package-service-instance-id }}.iot-things@iot-rollouts": { - "type": "suite-auth" - }, - "integration:{{ solution:id }}:iot-manager": { - "type": "iot-things-integration" + "integration:hono": { + "type": "generated" } }, "resources": { From 5f2ec1c87344524baf88fcbc23897df46e97aafe Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Wed, 19 Oct 2022 18:29:20 +0300 Subject: [PATCH 48/65] Automatic reconnect fixed to filter hono-connections only Signed-off-by: Andrey Balarev --- .../config/ConnectionConfigProvider.java | 13 +++-- .../ConnectivityConfigModifiedBehavior.java | 18 ++++--- .../config/DittoConnectionConfigProvider.java | 2 +- .../ConnectionPersistenceActor.java | 20 ++++++-- .../ConnectionSupervisorActor.java | 42 ++++++++++++++- .../ConnectionPersistenceActorTest.java | 51 +++++++++++++++++-- 6 files changed, 126 insertions(+), 20 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java index fc9e9c98f6..02df1ce04e 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java @@ -49,7 +49,7 @@ CompletionStage getConnectivityConfigOverwrites(ConnectionId connectionI * * @param connectionId the connection id * @param dittoHeaders the DittoHeaders of the original command which woke up the connection supervisor actor. - * @param subscriber the subscriber that will receive {@link org.eclipse.ditto.base.model.signals.events.Event}s + * @param subscriber the supervisor actor of the connection interested in these {@link org.eclipse.ditto.base.model.signals.events.Event}s * @return a future that succeeds or fails depending on whether registration was successful. */ CompletionStage registerForConnectivityConfigChanges(ConnectionId connectionId, @@ -57,7 +57,9 @@ CompletionStage registerForConnectivityConfigChanges(ConnectionId connecti /** * Returns {@code true} if the implementation can handle the given {@code event} to generate a modified {@link - * ConnectivityConfig} when passed to {@link #handleEvent(Event, akka.actor.ActorRef)}. + * ConnectivityConfig} when passed to {@link #handleEvent(Event, akka.actor.ActorRef, akka.actor.ActorRef)}. + * Returns {@code true} if the implementation can handle the given {@code event} to generate a modified {@link ConnectivityConfig} + * when passed to {@link #handleEvent(org.eclipse.ditto.base.model.signals.events.Event, akka.actor.ActorRef, akka.actor.ActorRef)}. * * @param event the event that may be used to generate modified config * @return {@code true} if the event is compatible @@ -68,9 +70,10 @@ CompletionStage registerForConnectivityConfigChanges(ConnectionId connecti * Uses the given {@code event} to create a config which should overwrite the default connectivity config. * * @param event the event used to create a config which should overwrite the default connectivity config. - * In case of Hub params changed, a null value is passed, which will invoke unconditional connection restart - * @param subscriber the actor that potentially will receive a command from this handler. + * In case of changed connection credentialsHub, a null value should be passed. + * @param supervisorActor the supervisor actor of the connection interested in these {@link org.eclipse.ditto.base.model.signals.events.Event}s + * @param persistenceActor the persistence actor of the connection */ - void handleEvent(Event event, ActorRef subscriber); + void handleEvent(Event event, ActorRef supervisorActor, @Nullable ActorRef persistenceActor); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java index de9024e6a8..93421442d3 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java @@ -12,6 +12,10 @@ */ package org.eclipse.ditto.connectivity.service.config; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + import org.eclipse.ditto.base.model.signals.events.Event; import com.typesafe.config.Config; @@ -30,23 +34,25 @@ public interface ConnectivityConfigModifiedBehavior extends Actor { * Injectable behavior to handle an {@code Event} that transports config changes. * This involves modified credentials for Hono-connections as well. * - * @param subscriber the actor that potentially will receive a command message after handling the event. + * @param supervisorActor the actor that potentially will receive a command message after handling the event. * @return behavior to handle an {@code Event} that transports config changes. */ - default AbstractActor.Receive connectivityConfigModifiedBehavior(ActorRef subscriber) { + default AbstractActor.Receive connectivityConfigModifiedBehavior(ActorRef supervisorActor, Supplier persistenceActorSupplier) { return ReceiveBuilder.create() - .match(Event.class, event -> getConnectivityConfigProvider().canHandle(event), event -> handleEvent(event, subscriber)) + .match(Event.class, getConnectivityConfigProvider()::canHandle, + event -> handleEvent(event, supervisorActor, persistenceActorSupplier.get())) .build(); } /** * Handles the received event by converting it to a {@link Config}. * - * @param subscriber the actor that potentially will receive a command message from this handler. + * @param supervisorActor the connection supervisor actor reference + * @param persistenceActor the connection persistence actor reference * @param event the received event */ - default void handleEvent(final Event event, ActorRef subscriber) { - getConnectivityConfigProvider().handleEvent(event, subscriber); + default void handleEvent(final Event event, ActorRef supervisorActor, @Nullable ActorRef persistenceActor) { + getConnectivityConfigProvider().handleEvent(event, supervisorActor, persistenceActor); } /** diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java index 49b1cbac20..f4618f0be4 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java @@ -57,7 +57,7 @@ public boolean canHandle(final Event event) { } @Override - public void handleEvent(final Event event, ActorRef subscriber) { + public void handleEvent(final Event event, ActorRef supervisorActor, @Nullable ActorRef persistenceActor) { // By default not handled in Ditto } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index 5539e8eb23..845a278c31 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -683,6 +683,7 @@ protected Receive matchAnyAfterInitialization() { .matchEquals(Control.CHECK_LOGGING_ACTIVE, this::checkLoggingEnabled) .matchEquals(Control.TRIGGER_UPDATE_PRIORITY, this::triggerUpdatePriority) .match(UpdatePriority.class, this::updatePriority) + .match(ConnectionSupervisorActor.RestartByConnectionType.class, this::initiateRestartByConnectionType) // maintain client actor refs .match(ClientActorRefs.class, this::syncClientActorRefs) @@ -692,6 +693,17 @@ protected Receive matchAnyAfterInitialization() { .orElse(super.matchAnyAfterInitialization()); } + private void initiateRestartByConnectionType( + final ConnectionSupervisorActor.RestartByConnectionType restartByConnectionType) { + if (entity.getConnectionType().equals(restartByConnectionType.getConnectionType())) { + sender().tell(ConnectionSupervisorActor.RestartConnection.of(null), self()); + log.info("Restart command sent to ConnectionSupervisorActor {}.", sender()); + } else { + log.info("Skipping restart of non-{} connection {}.", + restartByConnectionType.getConnectionType(), entityId); + } + } + @Override protected void becomeDeletedHandler() { cancelPeriodicPriorityUpdate(); @@ -815,10 +827,12 @@ private void testConnection(final StagedCommand command) { origin.tell(TestConnectionResponse.alreadyCreated(entityId, command.getDittoHeaders()), self); } else { final TestConnection testConnection; - TestConnection testConnectionUnresolved = (TestConnection) command.getCommand().setDittoHeaders(headersWithDryRun); + TestConnection testConnectionUnresolved = + (TestConnection) command.getCommand().setDittoHeaders(headersWithDryRun); if (testConnectionUnresolved.getConnection().getConnectionType() == ConnectionType.HONO) { testConnection = TestConnection.of( - honoConnectionFactory.getHonoConnection(testConnectionUnresolved.getConnection()), headersWithDryRun); + honoConnectionFactory.getHonoConnection(testConnectionUnresolved.getConnection()), + headersWithDryRun); } else { testConnection = testConnectionUnresolved; } @@ -1251,7 +1265,7 @@ enum Control { } /** - * Local message this actor may sent to itself in order to update the priority of the connection. + * Local message this actor may send to itself in order to update the priority of the connection. */ @Immutable static final class UpdatePriority implements WithDittoHeaders { diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java index 63b928d42e..dbce47b240 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java @@ -30,6 +30,7 @@ import org.eclipse.ditto.base.service.actors.ShutdownBehaviour; import org.eclipse.ditto.base.service.config.supervision.ExponentialBackOffConfig; import org.eclipse.ditto.connectivity.model.ConnectionId; +import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.signals.commands.ConnectivityCommand; import org.eclipse.ditto.connectivity.model.signals.commands.exceptions.ConnectionUnavailableException; import org.eclipse.ditto.connectivity.model.signals.commands.modify.LoggingExpired; @@ -152,7 +153,7 @@ protected Receive activeBehaviour( isRegisteredForConnectivityConfigChanges = true; }) .build() - .orElse(connectivityConfigModifiedBehavior(getSelf())) + .orElse(connectivityConfigModifiedBehavior(getSelf(), () -> persistenceActorChild)) .orElse(super.activeBehaviour(matchProcessNextTwinMessageBehavior, matchAnyBehavior)); } @@ -327,6 +328,45 @@ public static RestartConnection of(@Nullable Config modifiedConfig) { public Config getModifiedConfig() { return modifiedConfig; } + @Override + public boolean equals(@Nullable final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final RestartConnection that = (RestartConnection) o; + return Objects.equals(modifiedConfig, that.modifiedConfig); + } + + @Override + public int hashCode() { + return Objects.hash(modifiedConfig); + } + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + ", modifiedConfig=" + modifiedConfig + + "]"; + } + } + + /** + * Signals the persistence actor to initiate restart of itself if its type is equal to the specified connectionType. + */ + public static class RestartByConnectionType { + + private final ConnectionType connectionType; + + public RestartByConnectionType(ConnectionType connectionType) { + this.connectionType = connectionType; + } + + public ConnectionType getConnectionType() { + return connectionType; + } } } diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java index 2ca5242b78..74d80c4534 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.time.Instant; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -37,6 +38,7 @@ import org.eclipse.ditto.connectivity.model.ConnectionConfigurationInvalidException; import org.eclipse.ditto.connectivity.model.ConnectionId; import org.eclipse.ditto.connectivity.model.ConnectionIdInvalidException; +import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.ConnectivityStatus; import org.eclipse.ditto.connectivity.model.RecoveryStatus; @@ -117,11 +119,13 @@ public final class ConnectionPersistenceActorTest extends WithMockServers { "ditto.extensions.custom-connectivity-command-interceptor-provider", MockCommandValidator.class.getName(), "ditto.extensions.hono-connection-factory", - ConfigValueFactory.fromAnyRef("org.eclipse.ditto.connectivity.service.messaging.hono.DefaultHonoConnectionFactory"), + ConfigValueFactory.fromAnyRef( + "org.eclipse.ditto.connectivity.service.messaging.hono.DefaultHonoConnectionFactory"), "ditto.connectivity.hono.base-uri", ConfigValueFactory.fromAnyRef("tcp://localhost:9922"), "ditto.connectivity.hono.validate-certificates", ConfigValueFactory.fromAnyRef("false"), "ditto.connectivity.hono.sasl-mechanism", ConfigValueFactory.fromAnyRef("PLAIN"), - "ditto.connectivity.hono.bootstrap-servers", ConfigValueFactory.fromAnyRef("tcp://server1:port1,tcp://server2:port2,tcp://server3:port3") + "ditto.connectivity.hono.bootstrap-servers", + ConfigValueFactory.fromAnyRef("tcp://server1:port1,tcp://server2:port2,tcp://server3:port3") )).withFallback(TestConstants.CONFIG)); @Rule @@ -214,10 +218,49 @@ public void testConnectionTypeHono() throws IOException { .build()); expectMockClientActorMessage(testConnectionWithDryRunHeader); mockClientActorProbe.reply(new Status.Success("mock")); - testProbe.expectMsg(TestConnectionResponse.success(honoConnection.getId(), "mock", testConnection.getDittoHeaders())); + testProbe.expectMsg( + TestConnectionResponse.success(honoConnection.getId(), "mock", testConnection.getDittoHeaders())); } - private static Connection generateConnectionObjectFromJsonFile( String fileName) throws IOException { + @Test + public void testRestartByConnectionType() throws IOException { + // GIVEN + final var honoConnection = generateConnectionObjectFromJsonFile("hono-connection-custom-test.json"); + mockClientActorProbe.setAutoPilot(new TestActor.AutoPilot() { + @Override + public TestActor.AutoPilot run(final ActorRef sender, final Object msg) { + if (msg instanceof WithSender withSender && withSender.getMessage() instanceof OpenConnection) { + sender.tell(new Status.Success("connected"), mockClientActorProbe.ref()); + } + return keepRunning(); + } + }); + final var testProbe = actorSystemResource1.newTestProbe(); + final var connectionActorProps = Props.create(ConnectionPersistenceActor.class, + () -> new ConnectionPersistenceActor(connectionId, + commandForwarderActor, + pubSubMediatorProbe.ref(), + Trilean.TRUE, + ConfigFactory.empty())); + + final var underTest = actorSystemResource1.newActor(connectionActorProps, connectionId.toString()); + underTest.tell(createConnection(honoConnection), testProbe.ref()); + testProbe.expectMsgClass(CreateConnectionResponse.class); + + Arrays.stream(ConnectionType.values()).forEach(connectionType -> { + // WHEN + underTest.tell(new ConnectionSupervisorActor.RestartByConnectionType(connectionType), testProbe.ref()); + + // THEN + if (connectionType == honoConnection.getConnectionType()) { + testProbe.expectMsg(ConnectionSupervisorActor.RestartConnection.of(null)); + } else { + testProbe.expectNoMsg(); + } + }); + } + + private static Connection generateConnectionObjectFromJsonFile(String fileName) throws IOException { final var testClassLoader = DefaultHonoConnectionFactoryTest.class.getClassLoader(); try (final var connectionJsonFileStreamReader = new InputStreamReader( testClassLoader.getResourceAsStream(fileName) From 3ff0fe93d8b6b5930b1dea23cdf7e9aa4b96d901 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Fri, 21 Oct 2022 15:20:37 +0300 Subject: [PATCH 49/65] Auto-reconnect review issues fixed. Signed-off-by: Andrey Balarev --- .../config/ConnectionConfigProvider.java | 7 ++--- .../ConnectivityConfigModifiedBehavior.java | 23 ++++++++++------ .../config/DittoConnectionConfigProvider.java | 3 ++- .../ConnectionPersistenceActor.java | 6 +++-- .../ConnectionSupervisorActor.java | 27 ++++++++++++++++--- .../ConnectionPersistenceActorTest.java | 23 ++++++++-------- 6 files changed, 59 insertions(+), 30 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java index 02df1ce04e..c78873b711 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectionConfigProvider.java @@ -56,8 +56,6 @@ CompletionStage registerForConnectivityConfigChanges(ConnectionId connecti @Nullable DittoHeaders dittoHeaders, ActorRef subscriber); /** - * Returns {@code true} if the implementation can handle the given {@code event} to generate a modified {@link - * ConnectivityConfig} when passed to {@link #handleEvent(Event, akka.actor.ActorRef, akka.actor.ActorRef)}. * Returns {@code true} if the implementation can handle the given {@code event} to generate a modified {@link ConnectivityConfig} * when passed to {@link #handleEvent(org.eclipse.ditto.base.model.signals.events.Event, akka.actor.ActorRef, akka.actor.ActorRef)}. * @@ -69,9 +67,8 @@ CompletionStage registerForConnectivityConfigChanges(ConnectionId connecti /** * Uses the given {@code event} to create a config which should overwrite the default connectivity config. * - * @param event the event used to create a config which should overwrite the default connectivity config. - * In case of changed connection credentialsHub, a null value should be passed. - * @param supervisorActor the supervisor actor of the connection interested in these {@link org.eclipse.ditto.base.model.signals.events.Event}s + * @param event the event used to invoke restart of the connection due to some changes in its configuration + * @param supervisorActor the supervisor actor of the connection interested in these {@link Event}s * @param persistenceActor the persistence actor of the connection */ void handleEvent(Event event, ActorRef supervisorActor, @Nullable ActorRef persistenceActor); diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java index 93421442d3..966281e9e2 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/ConnectivityConfigModifiedBehavior.java @@ -12,6 +12,8 @@ */ package org.eclipse.ditto.connectivity.service.config; +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + import java.util.function.Supplier; import javax.annotation.Nullable; @@ -31,13 +33,17 @@ public interface ConnectivityConfigModifiedBehavior extends Actor { /** - * Injectable behavior to handle an {@code Event} that transports config changes. - * This involves modified credentials for Hono-connections as well. - * - * @param supervisorActor the actor that potentially will receive a command message after handling the event. - * @return behavior to handle an {@code Event} that transports config changes. - */ - default AbstractActor.Receive connectivityConfigModifiedBehavior(ActorRef supervisorActor, Supplier persistenceActorSupplier) { + * Injectable behavior to handle an {@code Event} that transports config changes. + * This involves modified credentials for Hono-connections as well. + * + * @param supervisorActor the actor that potentially will receive a command message after handling the event. + * @param persistenceActorSupplier a supplier of the actor that potentially will receive a command message after + * handling the event. + * @return behavior to handle an {@code Event} that transports config changes. + */ + default AbstractActor.Receive connectivityConfigModifiedBehavior(final ActorRef supervisorActor, + final Supplier persistenceActorSupplier) { + checkNotNull(persistenceActorSupplier); return ReceiveBuilder.create() .match(Event.class, getConnectivityConfigProvider()::canHandle, event -> handleEvent(event, supervisorActor, persistenceActorSupplier.get())) @@ -51,7 +57,8 @@ default AbstractActor.Receive connectivityConfigModifiedBehavior(ActorRef superv * @param persistenceActor the connection persistence actor reference * @param event the received event */ - default void handleEvent(final Event event, ActorRef supervisorActor, @Nullable ActorRef persistenceActor) { + default void handleEvent(final Event event, final ActorRef supervisorActor, + @Nullable final ActorRef persistenceActor) { getConnectivityConfigProvider().handleEvent(event, supervisorActor, persistenceActor); } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java index f4618f0be4..e2d9f3d1ab 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/config/DittoConnectionConfigProvider.java @@ -57,7 +57,8 @@ public boolean canHandle(final Event event) { } @Override - public void handleEvent(final Event event, ActorRef supervisorActor, @Nullable ActorRef persistenceActor) { + public void handleEvent(final Event event, final ActorRef supervisorActor, + @Nullable final ActorRef persistenceActor) { // By default not handled in Ditto } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index 845a278c31..7989391bd2 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -191,7 +191,7 @@ public final class ConnectionPersistenceActor private final ConnectivityCommandInterceptor commandValidator; private int subscriptionCounter = 0; private Instant connectionClosedAt = Instant.now(); - private HonoConnectionFactory honoConnectionFactory; + private final HonoConnectionFactory honoConnectionFactory; @Nullable private Instant loggingEnabledUntil; @Nullable private ActorRef clientActorRouter; @Nullable private ActorRef clientActorRefsAggregationActor; @@ -264,6 +264,7 @@ protected DittoDiagnosticLoggingAdapter createLogger() { * * @param connectionId the connection ID. * @param commandForwarderActor the actor used to send signals into the ditto cluster.. + * @param pubSubMediator the pubSubMediator * @param connectivityConfigOverwrites the overwrites for the connectivity config for the given connection. * @return the Akka configuration Props object. */ @@ -695,6 +696,7 @@ protected Receive matchAnyAfterInitialization() { private void initiateRestartByConnectionType( final ConnectionSupervisorActor.RestartByConnectionType restartByConnectionType) { + checkNotNull(entity, "entity"); if (entity.getConnectionType().equals(restartByConnectionType.getConnectionType())) { sender().tell(ConnectionSupervisorActor.RestartConnection.of(null), self()); log.info("Restart command sent to ConnectionSupervisorActor {}.", sender()); @@ -827,7 +829,7 @@ private void testConnection(final StagedCommand command) { origin.tell(TestConnectionResponse.alreadyCreated(entityId, command.getDittoHeaders()), self); } else { final TestConnection testConnection; - TestConnection testConnectionUnresolved = + final TestConnection testConnectionUnresolved = (TestConnection) command.getCommand().setDittoHeaders(headersWithDryRun); if (testConnectionUnresolved.getConnection().getConnectionType() == ConnectionType.HONO) { testConnection = TestConnection.of( diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java index dbce47b240..5c1d6d322f 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java @@ -12,6 +12,8 @@ */ package org.eclipse.ditto.connectivity.service.messaging.persistence; +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -320,14 +322,24 @@ private RestartConnection(@Nullable final Config modifiedConfig) { this.modifiedConfig = modifiedConfig; } - public static RestartConnection of(@Nullable Config modifiedConfig) { + /** + * + * @param modifiedConfig a new config to restart connection if changed or {@link null} to restart it unconditionally + * @return {@link RestartConnection} command class + */ + public static RestartConnection of(@Nullable final Config modifiedConfig) { return new RestartConnection(modifiedConfig); } + /** + * Getter + * @return the modified config + */ @Nullable public Config getModifiedConfig() { return modifiedConfig; } + @Override public boolean equals(@Nullable final Object o) { if (this == o) { @@ -357,14 +369,23 @@ public String toString() { /** * Signals the persistence actor to initiate restart of itself if its type is equal to the specified connectionType. */ - public static class RestartByConnectionType { + public static final class RestartByConnectionType { private final ConnectionType connectionType; - public RestartByConnectionType(ConnectionType connectionType) { + /** + * Constructor + * @param connectionType the desired connection type to filter by + */ + public RestartByConnectionType(final ConnectionType connectionType) { + checkNotNull(connectionType, "connectionType"); this.connectionType = connectionType; } + /** + * Getter + * @return the connection type + */ public ConnectionType getConnectionType() { return connectionType; } diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java index 74d80c4534..5473786556 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java @@ -182,7 +182,7 @@ public TestActor.AutoPilot run(final ActorRef sender, final Object msg) { public void testConnection() { //GIVEN final var connection = TestConstants.createConnection(connectionId); - var testConnection = TestConnection.of(connection, dittoHeadersWithCorrelationId); + final var testConnection = TestConnection.of(connection, dittoHeadersWithCorrelationId); final var testProbe = actorSystemResource1.newTestProbe(); final var connectionSupervisorActor = createSupervisor(); @@ -190,7 +190,7 @@ public void testConnection() { connectionSupervisorActor.tell(testConnection, testProbe.ref()); //THEN - var testConnectionWithDryRunHeader = TestConnection.of(connection, dittoHeadersWithCorrelationId + final var testConnectionWithDryRunHeader = TestConnection.of(connection, dittoHeadersWithCorrelationId .toBuilder() .dryRun(true) .build()); @@ -212,10 +212,11 @@ public void testConnectionTypeHono() throws IOException { connectionSupervisorActor.tell(testConnection, testProbe.ref()); //THEN - var testConnectionWithDryRunHeader = TestConnection.of(expectedHonoConnection, dittoHeadersWithCorrelationId - .toBuilder() - .dryRun(true) - .build()); + final var testConnectionWithDryRunHeader = TestConnection.of(expectedHonoConnection, + dittoHeadersWithCorrelationId + .toBuilder() + .dryRun(true) + .build()); expectMockClientActorMessage(testConnectionWithDryRunHeader); mockClientActorProbe.reply(new Status.Success("mock")); testProbe.expectMsg( @@ -260,7 +261,7 @@ public TestActor.AutoPilot run(final ActorRef sender, final Object msg) { }); } - private static Connection generateConnectionObjectFromJsonFile(String fileName) throws IOException { + private static Connection generateConnectionObjectFromJsonFile(final String fileName) throws IOException { final var testClassLoader = DefaultHonoConnectionFactoryTest.class.getClassLoader(); try (final var connectionJsonFileStreamReader = new InputStreamReader( testClassLoader.getResourceAsStream(fileName) @@ -274,7 +275,7 @@ private static Connection generateConnectionObjectFromJsonFile(String fileName) public void testConnectionCausingFailure() { //GIVEN final var connection = TestConstants.createConnection(connectionId); - var testConnection = TestConnection.of(connection, dittoHeadersWithCorrelationId); + final var testConnection = TestConnection.of(connection, dittoHeadersWithCorrelationId); final var testProbe = actorSystemResource1.newTestProbe(); final var connectionSupervisorActor = createSupervisor(); @@ -282,7 +283,7 @@ public void testConnectionCausingFailure() { connectionSupervisorActor.tell(testConnection, testProbe.ref()); //THEN - var testConnectionWithDryRunHeader = TestConnection.of(connection, dittoHeadersWithCorrelationId + final var testConnectionWithDryRunHeader = TestConnection.of(connection, dittoHeadersWithCorrelationId .toBuilder() .dryRun(true) .build()); @@ -298,7 +299,7 @@ public void testConnectionCausingFailure() { public void testConnectionCausingException() { //GIVEN final var connection = TestConstants.createConnection(connectionId); - var testConnection = TestConnection.of(connection, dittoHeadersWithCorrelationId); + final var testConnection = TestConnection.of(connection, dittoHeadersWithCorrelationId); final var testProbe = actorSystemResource1.newTestProbe(); final var connectionSupervisorActor = createSupervisor(); @@ -306,7 +307,7 @@ public void testConnectionCausingException() { connectionSupervisorActor.tell(testConnection, testProbe.ref()); //THEN - var testConnectionWithDryRunHeader = TestConnection.of(connection, dittoHeadersWithCorrelationId + final var testConnectionWithDryRunHeader = TestConnection.of(connection, dittoHeadersWithCorrelationId .toBuilder() .dryRun(true) .build()); From b4a0e76d9a8df426ec6ee83dd9c05cbb8c479c80 Mon Sep 17 00:00:00 2001 From: "Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM)" Date: Fri, 21 Oct 2022 15:44:58 +0300 Subject: [PATCH 50/65] Revert ConnectionRoute to origin/master and change REST API documentation --- .../paths/connections/connectionId.yml | 14 +++----- .../sources/paths/connections/connections.yml | 34 ++++++++----------- .../schemas/connections/connectionType.yml | 3 +- .../sources/schemas/connections/source.yml | 4 ++- .../sources/schemas/connections/target.yml | 1 + 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/documentation/src/main/resources/openapi/sources/paths/connections/connectionId.yml b/documentation/src/main/resources/openapi/sources/paths/connections/connectionId.yml index 28b30655de..41c1656623 100644 --- a/documentation/src/main/resources/openapi/sources/paths/connections/connectionId.yml +++ b/documentation/src/main/resources/openapi/sources/paths/connections/connectionId.yml @@ -100,13 +100,13 @@ put: $ref: '../../schemas/connections/newConnection.yml' example: { "name": "hono-example-connection-123", - "connectionType": "amqp-10", + "connectionType": "hono", "connectionStatus": "open", - "uri": "amqps://user:password@hono.eclipseprojects.io:5671", "sources": [ { "addresses": [ - "telemetry/FOO", + "telemetry", + "event", "..." ], "authorizationContext": [ @@ -128,7 +128,7 @@ put: ], "targets": [ { - "address": "events/twin", + "address": "command", "topics": [ "_/_/things/twin/events" ], @@ -136,12 +136,8 @@ put: "ditto:outbound-auth-subject", "..." ], - "headerMapping": { - "message-id": "{{ header:correlation-id }}", - "content-type": "application/vnd.eclipse.ditto+json" - } + "headerMapping": { } } - ] } required: true diff --git a/documentation/src/main/resources/openapi/sources/paths/connections/connections.yml b/documentation/src/main/resources/openapi/sources/paths/connections/connections.yml index dbe1332752..e1ec5e62e0 100644 --- a/documentation/src/main/resources/openapi/sources/paths/connections/connections.yml +++ b/documentation/src/main/resources/openapi/sources/paths/connections/connections.yml @@ -129,50 +129,46 @@ post: application/json: schema: $ref: '../../schemas/connections/newConnection.yml' - example: { + example: { "name": "hono-example-connection-123", - "connectionType": "amqp-10", + "connectionType": "hono", "connectionStatus": "open", - "uri": "amqps://user:password@hono.eclipseprojects.io:5671", "sources": [ { "addresses": [ - "telemetry/FOO", - "..." + "telemetry", + "event", + "..." ], "authorizationContext": [ - "ditto:inbound-auth-subject", - "..." + "ditto:inbound-auth-subject", + "..." ], "consumerCount": 1, "enforcement": { "input": "{{ header:device_id }}", "filters": [ - "{{ thing:id }}" + "{{ thing:id }}" ] }, "payloadMapping": [ - "Ditto", - "..." + "Ditto", + "status" ] } ], "targets": [ { - "address": "events/twin", + "address": "command", "topics": [ - "_/_/things/twin/events" + "_/_/things/twin/events" ], "authorizationContext": [ - "ditto:outbound-auth-subject", - "..." + "ditto:outbound-auth-subject", + "..." ], - "headerMapping": { - "message-id": "{{ header:correlation-id }}", - "content-type": "application/vnd.eclipse.ditto+json" - } + "headerMapping": { } } - ] } description: |- diff --git a/documentation/src/main/resources/openapi/sources/schemas/connections/connectionType.yml b/documentation/src/main/resources/openapi/sources/schemas/connections/connectionType.yml index 43b8a43de2..5e6dc53be5 100644 --- a/documentation/src/main/resources/openapi/sources/schemas/connections/connectionType.yml +++ b/documentation/src/main/resources/openapi/sources/schemas/connections/connectionType.yml @@ -16,4 +16,5 @@ enum: - http-push - mqtt - mqtt-5 - - kafka \ No newline at end of file + - kafka, + - hono \ No newline at end of file diff --git a/documentation/src/main/resources/openapi/sources/schemas/connections/source.yml b/documentation/src/main/resources/openapi/sources/schemas/connections/source.yml index 48daaf5b07..feb78becf8 100644 --- a/documentation/src/main/resources/openapi/sources/schemas/connections/source.yml +++ b/documentation/src/main/resources/openapi/sources/schemas/connections/source.yml @@ -16,7 +16,8 @@ properties: type: array uniqueItems: true title: Array of source addresses - description: The source addresses this connection consumes messages from + description: The source addresses this connection consumes messages from. The "telemetry", "events", + "command_response" aliases should be used for connections of type "hono". items: type: string title: Source address @@ -146,6 +147,7 @@ properties: * Ditto protocol header value: `{{ header:[any-header-name] }}` If placeholder resolution fails for a response, then the response is dropped. + NOTE Use "command" alias for connections of type "hono". example: - "{{ header:device_id }}" - "{{ source:address }}" diff --git a/documentation/src/main/resources/openapi/sources/schemas/connections/target.yml b/documentation/src/main/resources/openapi/sources/schemas/connections/target.yml index 61144bcd0a..152b759351 100644 --- a/documentation/src/main/resources/openapi/sources/schemas/connections/target.yml +++ b/documentation/src/main/resources/openapi/sources/schemas/connections/target.yml @@ -24,6 +24,7 @@ properties: * Thing Namespace: `{{ thing:namespace }}` * Thing Name: `{{ thing:name }}` (the part of the ID without the namespace) + NOTE Use "command" alias for connections of type "hono". topics: type: array title: Topics From 6f7adeb6f0556b085bbad2c3d185fef3b2884f34 Mon Sep 17 00:00:00 2001 From: "Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM)" Date: Fri, 21 Oct 2022 15:49:52 +0300 Subject: [PATCH 51/65] Reverted ConnectionRoute to origin/master --- .../routes/connections/ConnectionsRoute.java | 113 ++++++------------ 1 file changed, 38 insertions(+), 75 deletions(-) diff --git a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java index b4190e8d56..e6878d25b1 100644 --- a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java +++ b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java @@ -27,7 +27,6 @@ import org.eclipse.ditto.base.model.signals.commands.Command; import org.eclipse.ditto.connectivity.model.Connection; import org.eclipse.ditto.connectivity.model.ConnectionId; -import org.eclipse.ditto.connectivity.model.ConnectionType; import org.eclipse.ditto.connectivity.model.ConnectivityModelFactory; import org.eclipse.ditto.connectivity.model.ConnectivityStatus; import org.eclipse.ditto.connectivity.model.signals.commands.ConnectivityCommand; @@ -51,7 +50,9 @@ import org.eclipse.ditto.gateway.service.endpoints.directives.auth.DevopsAuthenticationDirective; import org.eclipse.ditto.gateway.service.endpoints.routes.AbstractRoute; import org.eclipse.ditto.gateway.service.endpoints.routes.RouteBaseProperties; +import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; import akka.http.javadsl.model.MediaTypes; import akka.http.javadsl.server.PathMatchers; @@ -142,9 +143,9 @@ private Route connections(final RequestContext ctx, final DittoHeaders dittoHead concat( get(() -> // GET /connections?ids-only=false parameterOptional(ConnectionsParameter.IDS_ONLY.toString(), idsOnly -> handlePerRequest(ctx, - RetrieveConnections.newInstance(idsOnly.map(Boolean::valueOf).orElse(false), - dittoHeaders) - )) + RetrieveConnections.newInstance(idsOnly.map(Boolean::valueOf).orElse(false), + dittoHeaders) + )) ), post(() -> // POST /connections?dry-run= @@ -305,89 +306,51 @@ private static ConnectivityCommand buildConnectivityCommand(final String comm } private static Connection buildConnectionForPost(final String connectionJson) { - final var connectionJsonObject = getConnectionJsonObjectOrThrow(connectionJson); - if (isContainingId(connectionJsonObject)) { - throw ConnectionIdNotExplicitlySettableException.newBuilder().build(); - } else { - return ConnectivityModelFactory.connectionFromJson( - setUriForHonoConnectionType( - setConnectionId( - setConnectionStatus(connectionJsonObject), - ConnectionId.generateRandom() - ) - ) - ); - } - } - - private static Connection buildConnectionForTest(final String connectionJson) { - final var connectionJsonObject = getConnectionJsonObjectOrThrow(connectionJson); - final var temporaryTestConnectionId = UUID.randomUUID() + "-dry-run"; - return ConnectivityModelFactory.connectionFromJson( - setUriForHonoConnectionType( - getConnectionId(connectionJsonObject) - .map(connectionId -> connectionJson.replace(connectionId, temporaryTestConnectionId)) - .map(ConnectionsRoute::getConnectionJsonObjectOrThrow) - .orElseGet(() -> setConnectionId(connectionJsonObject, temporaryTestConnectionId)) - ) - ); - } + final JsonObject connectionJsonObject = wrapJsonRuntimeException(() -> JsonFactory.newObject(connectionJson)); - private static Connection buildConnectionForPut(final ConnectionId connectionId, final String connectionJson) { - var connectionJsonObject = getConnectionJsonObjectOrThrow(connectionJson); - final var actualConnectionIdOptional = getConnectionId(connectionJsonObject); - if (actualConnectionIdOptional.isEmpty()) { - connectionJsonObject = setConnectionId(connectionJsonObject, connectionId); - } else if (!connectionId.equals(actualConnectionIdOptional.get())) { + if (connectionJsonObject.contains(Connection.JsonFields.ID.getPointer())) { throw ConnectionIdNotExplicitlySettableException.newBuilder().build(); } - return ConnectivityModelFactory.connectionFromJson(setUriForHonoConnectionType(connectionJsonObject)); - } + final JsonObjectBuilder jsonObjectBuilder = connectionJsonObject.toBuilder(); + jsonObjectBuilder.set(Connection.JsonFields.ID, UUID.randomUUID().toString()); + final String connectionStatus = connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_STATUS) + .orElse(ConnectivityStatus.UNKNOWN.getName()); + jsonObjectBuilder.set(Connection.JsonFields.CONNECTION_STATUS, connectionStatus); - private static boolean isHonoConnectionType(final JsonObject connectionJsonObject) { - return connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_TYPE) - .flatMap(ConnectionType::forName) - .filter(ConnectionType.HONO::equals) - .isPresent(); + return ConnectivityModelFactory.connectionFromJson(jsonObjectBuilder.build()); } - private static JsonObject setUriForHonoConnectionType(final JsonObject connectionJsonObject) { - final JsonObject result; - if (isHonoConnectionType(connectionJsonObject)) { - result = connectionJsonObject.set(Connection.JsonFields.URI, "ssl://hono-endpoint:9094"); + private static Connection buildConnectionForTest(final String connectionJson) { + final JsonObject connectionJsonObject; + final JsonObject connectionJsonObjectBeforeReplacement = + wrapJsonRuntimeException(() -> JsonFactory.newObject(connectionJson)); + final Optional optionalConnectionId = + connectionJsonObjectBeforeReplacement.getValue(Connection.JsonFields.ID); + final String temporaryTestConnectionId = UUID.randomUUID() + "-dry-run"; + if (optionalConnectionId.isPresent()) { + final String temporaryConnectionJson = + connectionJson.replace(optionalConnectionId.get(), temporaryTestConnectionId); + connectionJsonObject = wrapJsonRuntimeException(() -> JsonFactory.newObject(temporaryConnectionJson)); } else { - result = connectionJsonObject; + final JsonObjectBuilder jsonObjectBuilder = connectionJsonObjectBeforeReplacement.toBuilder(); + connectionJsonObject = jsonObjectBuilder.set(Connection.JsonFields.ID, temporaryTestConnectionId).build(); } - return result; + return ConnectivityModelFactory.connectionFromJson(connectionJsonObject); } - private static JsonObject getConnectionJsonObjectOrThrow(final String connectionJsonString) { - return wrapJsonRuntimeException(() -> JsonObject.of(connectionJsonString)); - } - - private static boolean isContainingId(final JsonObject connectionJsonObject) { - return connectionJsonObject.contains(Connection.JsonFields.ID.getPointer()); - } - - private static JsonObject setConnectionStatus(final JsonObject connectionJsonObject) { - return connectionJsonObject.set( - Connection.JsonFields.CONNECTION_STATUS, - getConnectionStatusOrElseUnknown(connectionJsonObject) - ); - } - - private static String getConnectionStatusOrElseUnknown(final JsonObject connectionJsonObject) { - return connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_STATUS) - .orElseGet(ConnectivityStatus.UNKNOWN::getName); - } + private static Connection buildConnectionForPut(final ConnectionId connectionId, final String connectionJson) { + final JsonObject connectionJsonObject = wrapJsonRuntimeException(() -> JsonFactory.newObject(connectionJson)); + if (connectionJsonObject.contains(Connection.JsonFields.ID.getPointer()) + && + !connectionId.toString().equals(connectionJsonObject.getValue(Connection.JsonFields.ID).orElse(null))) { + throw ConnectionIdNotExplicitlySettableException.newBuilder().build(); + } - private static JsonObject setConnectionId(final JsonObject connectionJsonObject, final CharSequence connectionId) { - return connectionJsonObject.set(Connection.JsonFields.ID, connectionId.toString()); - } + final JsonObjectBuilder jsonObjectBuilder = connectionJsonObject.toBuilder(); + jsonObjectBuilder.set(Connection.JsonFields.ID, connectionId.toString()); - private static Optional getConnectionId(final JsonObject connectionJsonObject) { - return connectionJsonObject.getValue(Connection.JsonFields.ID).map(ConnectionId::of); + return ConnectivityModelFactory.connectionFromJson(jsonObjectBuilder.build()); } -} \ No newline at end of file +} From 57703cc84e20c7cfbc336232666b4b80edc14202 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Fri, 21 Oct 2022 15:54:28 +0300 Subject: [PATCH 52/65] Javadoc fixed Signed-off-by: Andrey Balarev --- .../messaging/persistence/ConnectionSupervisorActor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java index 5c1d6d322f..50c276db0d 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java @@ -324,7 +324,7 @@ private RestartConnection(@Nullable final Config modifiedConfig) { /** * - * @param modifiedConfig a new config to restart connection if changed or {@link null} to restart it unconditionally + * @param modifiedConfig a new config to restart connection if changed or {@code null} to restart it unconditionally * @return {@link RestartConnection} command class */ public static RestartConnection of(@Nullable final Config modifiedConfig) { From f49fb66e1010e492fe413d937f8e744d4d924a2f Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Mon, 24 Oct 2022 11:08:43 +0300 Subject: [PATCH 53/65] More review issues fixed. Signed-off-by: Andrey Balarev --- .../persistence/ConnectionPersistenceActor.java | 15 +++++++++------ .../persistence/ConnectionSupervisorActor.java | 12 +++++------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index 7989391bd2..afe3f951a3 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -696,13 +696,16 @@ protected Receive matchAnyAfterInitialization() { private void initiateRestartByConnectionType( final ConnectionSupervisorActor.RestartByConnectionType restartByConnectionType) { - checkNotNull(entity, "entity"); - if (entity.getConnectionType().equals(restartByConnectionType.getConnectionType())) { - sender().tell(ConnectionSupervisorActor.RestartConnection.of(null), self()); - log.info("Restart command sent to ConnectionSupervisorActor {}.", sender()); + if (entity != null) { + if (entity.getConnectionType().equals(restartByConnectionType.getConnectionType())) { + sender().tell(ConnectionSupervisorActor.RestartConnection.of(null), self()); + log.info("Restart command sent to ConnectionSupervisorActor {}.", sender()); + } else { + log.info("Skipping restart of non-{} connection {}.", + restartByConnectionType.getConnectionType(), entityId); + } } else { - log.info("Skipping restart of non-{} connection {}.", - restartByConnectionType.getConnectionType(), entityId); + log.info("Skipping restart of connection {} due to unexpected null-entity.", entityId); } } diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java index 50c276db0d..f764f39f04 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionSupervisorActor.java @@ -148,8 +148,8 @@ protected Receive activeBehaviour( .match(Config.class, this::onConnectivityConfigModified) .match(CheckForOverwritesConfig.class, checkForOverwrites -> initConfigOverwrites(getEntityId(), checkForOverwrites.dittoHeaders)) - .match(RestartConnection.class, restartConnection -> Optional.ofNullable(restartConnection - .getModifiedConfig()).ifPresentOrElse(this::onConnectivityConfigModified, this::restartChild)) + .match(RestartConnection.class, restartConnection -> restartConnection.getModifiedConfig() + .ifPresentOrElse(this::onConnectivityConfigModified, this::restartChild)) .matchEquals(Control.REGISTRATION_FOR_CONFIG_CHANGES_SUCCESSFUL, c -> { log.debug("Successfully registered for connectivity config changes."); isRegisteredForConnectivityConfigChanges = true; @@ -335,9 +335,8 @@ public static RestartConnection of(@Nullable final Config modifiedConfig) { * Getter * @return the modified config */ - @Nullable - public Config getModifiedConfig() { - return modifiedConfig; + public Optional getModifiedConfig() { + return Optional.ofNullable(modifiedConfig); } @Override @@ -378,8 +377,7 @@ public static final class RestartByConnectionType { * @param connectionType the desired connection type to filter by */ public RestartByConnectionType(final ConnectionType connectionType) { - checkNotNull(connectionType, "connectionType"); - this.connectionType = connectionType; + this.connectionType = checkNotNull(connectionType, "connectionType"); } /** From 9b38b8df4c70d9585f42ee4da94c298353272aa5 Mon Sep 17 00:00:00 2001 From: "Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM)" Date: Mon, 24 Oct 2022 12:30:15 +0300 Subject: [PATCH 54/65] Changed ConnectionId.generateRandom() --- .../service/endpoints/routes/connections/ConnectionsRoute.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java index e6878d25b1..631d66c73a 100644 --- a/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java +++ b/gateway/service/src/main/java/org/eclipse/ditto/gateway/service/endpoints/routes/connections/ConnectionsRoute.java @@ -313,7 +313,7 @@ private static Connection buildConnectionForPost(final String connectionJson) { } final JsonObjectBuilder jsonObjectBuilder = connectionJsonObject.toBuilder(); - jsonObjectBuilder.set(Connection.JsonFields.ID, UUID.randomUUID().toString()); + jsonObjectBuilder.set(Connection.JsonFields.ID, ConnectionId.generateRandom().toString()); final String connectionStatus = connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_STATUS) .orElse(ConnectivityStatus.UNKNOWN.getName()); jsonObjectBuilder.set(Connection.JsonFields.CONNECTION_STATUS, connectionStatus); From d5c9addabc242fca2a07b46083dcc66882f4edba Mon Sep 17 00:00:00 2001 From: "Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM)" Date: Tue, 25 Oct 2022 08:46:01 +0300 Subject: [PATCH 55/65] Introduce dedicated implementation of HonoConnection --- .../model/ConnectivityModelFactory.java | 10 +- .../connectivity/model/HonoConnection.java | 959 ++++++++++++++++++ .../validation/ConnectionValidator.java | 9 +- 3 files changed, 974 insertions(+), 4 deletions(-) mode change 100644 => 100755 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java create mode 100755 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java old mode 100644 new mode 100755 index 0ffcce3281..dc680c1ad3 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java @@ -70,7 +70,9 @@ public static ConnectionBuilder newConnectionBuilder(final ConnectionId id, final ConnectionType connectionType, final ConnectivityStatus connectionStatus, final String uri) { - + if (connectionType == ConnectionType.HONO) { + return HonoConnection.getBuilder(id, connectionType, connectionStatus, uri); + } return ImmutableConnection.getBuilder(id, connectionType, connectionStatus, uri); } @@ -83,6 +85,9 @@ public static ConnectionBuilder newConnectionBuilder(final ConnectionId id, * @throws NullPointerException if {@code connection} is {@code null}. */ public static ConnectionBuilder newConnectionBuilder(final Connection connection) { + if (HonoConnection.isHonoConnectionType(connection)) { + return HonoConnection.getBuilder(connection); + } return ImmutableConnection.getBuilder(connection); } @@ -95,6 +100,9 @@ public static ConnectionBuilder newConnectionBuilder(final Connection connection * @throws org.eclipse.ditto.json.JsonParseException if {@code jsonObject} is not an appropriate JSON object. */ public static Connection connectionFromJson(final JsonObject jsonObject) { + if (HonoConnection.isHonoConnectionType(jsonObject)) { + return HonoConnection.fromJson(jsonObject); + } return ImmutableConnection.fromJson(jsonObject); } diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java new file mode 100755 index 0000000000..5a935e0f95 --- /dev/null +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java @@ -0,0 +1,959 @@ +/* + * Copyright (c) 2017 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model; + +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkArgument; +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + +import java.net.URI; +import java.net.URISyntaxException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.NotThreadSafe; + +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonCollectors; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonField; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; +import org.eclipse.ditto.json.JsonParseException; +import org.eclipse.ditto.json.JsonValue; +@Immutable +public class HonoConnection implements Connection { + + private final ConnectionId id; + @Nullable private final String name; + private final ConnectionType connectionType; + private final ConnectivityStatus connectionStatus; + @Nullable private final HonoConnection.ConnectionUri uri; + @Nullable private final Credentials credentials; + @Nullable private final String trustedCertificates; + @Nullable private final ConnectionLifecycle lifecycle; + + + private final List sources; + private final List targets; + private final int clientCount; + private final boolean failOverEnabled; + private final boolean validateCertificate; + private final int processorPoolSize; + private final Map specificConfig; + private final PayloadMappingDefinition payloadMappingDefinition; + private final Set tags; + @Nullable private final SshTunnel sshTunnel; + + private HonoConnection(final HonoConnection.Builder builder) { + id = checkNotNull(builder.id, "id"); + name = builder.name; + connectionType = builder.connectionType; + connectionStatus = checkNotNull(builder.connectionStatus, "connectionStatus"); + credentials = builder.credentials; + trustedCertificates = builder.trustedCertificates; + uri = HonoConnection.ConnectionUri.of(builder.uri); + sources = Collections.unmodifiableList(new ArrayList<>(builder.sources)); + targets = Collections.unmodifiableList(new ArrayList<>(builder.targets)); + clientCount = builder.clientCount; + failOverEnabled = builder.failOverEnabled; + validateCertificate = builder.validateCertificate; + processorPoolSize = builder.processorPoolSize; + specificConfig = Collections.unmodifiableMap(new HashMap<>(builder.specificConfig)); + payloadMappingDefinition = builder.payloadMappingDefinition; + tags = Collections.unmodifiableSet(new LinkedHashSet<>(builder.tags)); + lifecycle = builder.lifecycle; + sshTunnel = builder.sshTunnel; + } + + /** + * Returns a new {@code ConnectionBuilder} object. + * + * @param id the connection ID. + * @param connectionType the connection type. + * @param connectionStatus the connection status. + * @param uri the URI. + * @return new instance of {@code ConnectionBuilder}. + * @throws NullPointerException if any argument is {@code null}. + */ + public static ConnectionBuilder getBuilder(final ConnectionId id, + final ConnectionType connectionType, + final ConnectivityStatus connectionStatus, + final String uri) { + + return new HonoConnection.Builder(connectionType) + .id(id) + .connectionStatus(connectionStatus) + .uri(uri); + } + + /** + * Returns a new {@code ConnectionBuilder} object. + * + * @param connection the connection to use for initializing the builder. + * @return new instance of {@code ImmutableConnectionBuilder}. + * @throws NullPointerException if {@code connection} is {@code null}. + */ + public static ConnectionBuilder getBuilder(final Connection connection) { + checkNotNull(connection, "Connection"); + + return new HonoConnection.Builder(connection.getConnectionType()) + .id(connection.getId()) + .connectionStatus(connection.getConnectionStatus()) + .credentials(connection.getCredentials().orElse(null)) + .uri(connection.getUri()) + .trustedCertificates(connection.getTrustedCertificates().orElse(null)) + .failoverEnabled(connection.isFailoverEnabled()) + .validateCertificate(connection.isValidateCertificates()) + .processorPoolSize(connection.getProcessorPoolSize()) + .sources(connection.getSources()) + .targets(connection.getTargets()) + .clientCount(connection.getClientCount()) + .specificConfig(connection.getSpecificConfig()) + .payloadMappingDefinition(connection.getPayloadMappingDefinition()) + .name(connection.getName().orElse(null)) + .sshTunnel(connection.getSshTunnel().orElse(null)) + .tags(connection.getTags()) + .lifecycle(connection.getLifecycle().orElse(null)); + } + + /** + * Creates a new {@code Connection} object from the specified JSON object. + * + * @param jsonObject a JSON object which provides the data for the Connection to be created. + * @return a new Connection which is initialised with the extracted data from {@code jsonObject}. + * @throws NullPointerException if {@code jsonObject} is {@code null}. + * @throws org.eclipse.ditto.json.JsonParseException if {@code jsonObject} is not an appropriate JSON object. + */ + public static Connection fromJson(final JsonObject jsonObject) { + final ConnectionType type = getConnectionTypeOrThrow(jsonObject); + final MappingContext mappingContext = jsonObject.getValue(JsonFields.MAPPING_CONTEXT) + .map(ConnectivityModelFactory::mappingContextFromJson) + .orElse(null); + + final PayloadMappingDefinition payloadMappingDefinition = + jsonObject.getValue(JsonFields.MAPPING_DEFINITIONS) + .map(ImmutablePayloadMappingDefinition::fromJson) + .orElse(ConnectivityModelFactory.emptyPayloadMappingDefinition()); + + final ConnectionBuilder builder = new HonoConnection.Builder(type) + .id(ConnectionId.of(jsonObject.getValueOrThrow(JsonFields.ID))) + .connectionStatus(getConnectionStatusOrThrow(jsonObject)) + .uri(jsonObject.getValueOrThrow(JsonFields.URI)) + .sources(getSources(jsonObject)) + .targets(getTargets(jsonObject)) + .name(jsonObject.getValue(JsonFields.NAME).orElse(null)) + .mappingContext(mappingContext) + .payloadMappingDefinition(payloadMappingDefinition) + .specificConfig(getSpecificConfiguration(jsonObject)) + .tags(getTags(jsonObject)); + + jsonObject.getValue(JsonFields.LIFECYCLE) + .flatMap(ConnectionLifecycle::forName).ifPresent(builder::lifecycle); + jsonObject.getValue(JsonFields.CREDENTIALS).ifPresent(builder::credentialsFromJson); + jsonObject.getValue(JsonFields.CLIENT_COUNT).ifPresent(builder::clientCount); + jsonObject.getValue(JsonFields.FAILOVER_ENABLED).ifPresent(builder::failoverEnabled); + jsonObject.getValue(JsonFields.VALIDATE_CERTIFICATES).ifPresent(builder::validateCertificate); + jsonObject.getValue(JsonFields.PROCESSOR_POOL_SIZE).ifPresent(builder::processorPoolSize); + jsonObject.getValue(JsonFields.TRUSTED_CERTIFICATES).ifPresent(builder::trustedCertificates); + jsonObject.getValue(JsonFields.SSH_TUNNEL) + .ifPresent(jsonFields -> builder.sshTunnel(ImmutableSshTunnel.fromJson(jsonFields))); + + return builder.build(); + } + + private static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { + final String readConnectionType = jsonObject.getValueOrThrow(JsonFields.CONNECTION_TYPE); + return ConnectionType.forName(readConnectionType) + .orElseThrow(() -> JsonParseException.newBuilder() + .message(MessageFormat.format("Connection type <{0}> is invalid!", readConnectionType)) + .build()); + } + + private static ConnectivityStatus getConnectionStatusOrThrow(final JsonObject jsonObject) { + final String readConnectionStatus = jsonObject.getValueOrThrow(JsonFields.CONNECTION_STATUS); + return ConnectivityStatus.forName(readConnectionStatus) + .orElseThrow(() -> JsonParseException.newBuilder() + .message(MessageFormat.format("Connection status <{0}> is invalid!", readConnectionStatus)) + .build()); + } + + private static List getSources(final JsonObject jsonObject) { + final Optional sourcesArray = jsonObject.getValue(JsonFields.SOURCES); + if (sourcesArray.isPresent()) { + final JsonArray values = sourcesArray.get(); + return IntStream.range(0, values.getSize()) + .mapToObj(index -> values.get(index) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .map(valueAsObject -> ConnectivityModelFactory.sourceFromJson(valueAsObject, index))) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } + + private static List getTargets(final JsonObject jsonObject) { + return jsonObject.getValue(JsonFields.TARGETS) + .map(array -> array.stream() + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .map(ConnectivityModelFactory::targetFromJson) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()); + } + + private static Map getSpecificConfiguration(final JsonObject jsonObject) { + return jsonObject.getValue(JsonFields.SPECIFIC_CONFIG) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .map(JsonObject::stream) + .map(jsonFields -> jsonFields.collect(Collectors.toMap(JsonField::getKeyName, + f -> f.getValue().isString() ? f.getValue().asString() : f.getValue().toString()))) + .orElse(Collections.emptyMap()); + } + + private static Set getTags(final JsonObject jsonObject) { + return jsonObject.getValue(JsonFields.TAGS) + .map(array -> array.stream() + .filter(JsonValue::isString) + .map(JsonValue::asString) + .collect(Collectors.toCollection(LinkedHashSet::new))) + .orElseGet(LinkedHashSet::new); + } + + @Override + public ConnectionId getId() { + return id; + } + + @Override + public Optional getName() { + return Optional.ofNullable(name); + } + + @Override + public ConnectionType getConnectionType() { + return connectionType; + } + + @Override + public ConnectivityStatus getConnectionStatus() { + return connectionStatus; + } + + @Override + public List getSources() { + return sources; + } + + @Override + public List getTargets() { + return targets; + } + + @Override + public Optional getSshTunnel() { + return Optional.ofNullable(sshTunnel); + } + + @Override + public int getClientCount() { + return clientCount; + } + + @Override + public boolean isFailoverEnabled() { + return failOverEnabled; + } + + @Override + public Optional getCredentials() { + return Optional.ofNullable(credentials); + } + + @Override + public Optional getTrustedCertificates() { + return Optional.ofNullable(trustedCertificates); + } + + @Override + public String getUri() { + return uri.toString(); + } + + @Override + public String getProtocol() { + return uri.getProtocol(); + } + + @Override + public Optional getUsername() { + return uri.getUserName(); + } + + @Override + public Optional getPassword() { + return uri.getPassword(); + } + + @Override + public String getHostname() { + return uri.getHostname(); + } + + @Override + public int getPort() { + return uri.getPort(); + } + + @Override + public Optional getPath() { + return uri.getPath(); + } + + @Override + public boolean isValidateCertificates() { + return validateCertificate; + } + + @Override + public int getProcessorPoolSize() { + return processorPoolSize; + } + + @Override + public Map getSpecificConfig() { + return specificConfig; + } + + @Override + public PayloadMappingDefinition getPayloadMappingDefinition() { + return payloadMappingDefinition; + } + + @Override + public Set getTags() { + return tags; + } + + @Override + public Optional getLifecycle() { + return Optional.ofNullable(lifecycle); + } + + @Override + public JsonObject toJson(final JsonSchemaVersion schemaVersion, final Predicate thePredicate) { + final Predicate predicate = schemaVersion.and(thePredicate); + final JsonObjectBuilder jsonObjectBuilder = JsonFactory.newObjectBuilder(); + + if (null != lifecycle) { + jsonObjectBuilder.set(JsonFields.LIFECYCLE, lifecycle.name(), predicate); + } + jsonObjectBuilder.set(JsonFields.ID, String.valueOf(id), predicate); + jsonObjectBuilder.set(JsonFields.NAME, name, predicate); + jsonObjectBuilder.set(JsonFields.CONNECTION_TYPE, connectionType.getName(), predicate); + jsonObjectBuilder.set(JsonFields.CONNECTION_STATUS, connectionStatus.getName(), predicate); + jsonObjectBuilder.set(JsonFields.URI, uri.toString(), predicate); + jsonObjectBuilder.set(JsonFields.SOURCES, sources.stream() + .sorted(Comparator.comparingInt(Source::getIndex)) + .map(source -> source.toJson(schemaVersion, thePredicate)) + .collect(JsonCollectors.valuesToArray()), predicate.and(Objects::nonNull)); + jsonObjectBuilder.set(JsonFields.TARGETS, targets.stream() + .map(target -> target.toJson(schemaVersion, thePredicate)) + .collect(JsonCollectors.valuesToArray()), predicate.and(Objects::nonNull)); + jsonObjectBuilder.set(JsonFields.CLIENT_COUNT, clientCount, predicate); + jsonObjectBuilder.set(JsonFields.FAILOVER_ENABLED, failOverEnabled, predicate); + jsonObjectBuilder.set(JsonFields.VALIDATE_CERTIFICATES, validateCertificate, predicate); + jsonObjectBuilder.set(JsonFields.PROCESSOR_POOL_SIZE, processorPoolSize, predicate); + if (!specificConfig.isEmpty()) { + jsonObjectBuilder.set(JsonFields.SPECIFIC_CONFIG, specificConfig.entrySet() + .stream() + .map(entry -> JsonField.newInstance(entry.getKey(), JsonValue.of(entry.getValue()))) + .collect(JsonCollectors.fieldsToObject()), predicate); + } + if (!payloadMappingDefinition.isEmpty()) { + jsonObjectBuilder.set(JsonFields.MAPPING_DEFINITIONS, + payloadMappingDefinition.toJson(schemaVersion, thePredicate)); + } + if (credentials != null) { + jsonObjectBuilder.set(JsonFields.CREDENTIALS, credentials.toJson()); + } + if (trustedCertificates != null) { + jsonObjectBuilder.set(JsonFields.TRUSTED_CERTIFICATES, trustedCertificates, predicate); + } + if (sshTunnel != null) { + jsonObjectBuilder.set(JsonFields.SSH_TUNNEL, sshTunnel.toJson(predicate), predicate); + } + jsonObjectBuilder.set(JsonFields.TAGS, tags.stream() + .map(JsonFactory::newValue) + .collect(JsonCollectors.valuesToArray()), predicate); + return jsonObjectBuilder.build(); + } + + @SuppressWarnings("OverlyComplexMethod") + @Override + public boolean equals(@Nullable final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final HonoConnection that = (HonoConnection) o; + return failOverEnabled == that.failOverEnabled && + Objects.equals(id, that.id) && + Objects.equals(name, that.name) && + Objects.equals(connectionType, that.connectionType) && + Objects.equals(connectionStatus, that.connectionStatus) && + Objects.equals(sources, that.sources) && + Objects.equals(targets, that.targets) && + Objects.equals(clientCount, that.clientCount) && + Objects.equals(credentials, that.credentials) && + Objects.equals(trustedCertificates, that.trustedCertificates) && + Objects.equals(uri, that.uri) && + Objects.equals(processorPoolSize, that.processorPoolSize) && + Objects.equals(validateCertificate, that.validateCertificate) && + Objects.equals(specificConfig, that.specificConfig) && + Objects.equals(payloadMappingDefinition, that.payloadMappingDefinition) && + Objects.equals(lifecycle, that.lifecycle) && + Objects.equals(sshTunnel, that.sshTunnel) && + Objects.equals(tags, that.tags); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, connectionType, connectionStatus, sources, targets, clientCount, failOverEnabled, + credentials, trustedCertificates, uri, validateCertificate, processorPoolSize, specificConfig, + payloadMappingDefinition, sshTunnel, tags, lifecycle); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "id=" + id + + ", name=" + name + + ", connectionType=" + connectionType + + ", connectionStatus=" + connectionStatus + + ", failoverEnabled=" + failOverEnabled + + ", credentials=" + credentials + + ", trustedCertificates=hash:" + Objects.hash(trustedCertificates) + + ", uri=" + uri.uriString + + ", sources=" + sources + + ", targets=" + targets + + ", sshTunnel=" + sshTunnel + + ", clientCount=" + clientCount + + ", validateCertificate=" + validateCertificate + + ", processorPoolSize=" + processorPoolSize + + ", specificConfig=" + specificConfig + + ", payloadMappingDefinition=" + payloadMappingDefinition + + ", tags=" + tags + + ", lifecycle=" + lifecycle + + "]"; + } + + /** + * Builder for {@code ImmutableConnection}. + */ + @NotThreadSafe + private static final class Builder implements ConnectionBuilder { + + private static final String MIGRATED_MAPPER_ID = "javascript"; + private final ConnectionType connectionType; + + // required but changeable: + @Nullable private ConnectionId id; + @Nullable private ConnectivityStatus connectionStatus; + @Nullable private String uri; + + // optional: + @Nullable private String name = null; + @Nullable private Credentials credentials; + @Nullable private MappingContext mappingContext = null; + @Nullable private String trustedCertificates; + @Nullable private ConnectionLifecycle lifecycle = null; + @Nullable private SshTunnel sshTunnel = null; + + // optional with default: + private Set tags = new LinkedHashSet<>(); + private boolean failOverEnabled = true; + private boolean validateCertificate = true; + private final List sources = new ArrayList<>(); + private final List targets = new ArrayList<>(); + private int clientCount = 1; + private int processorPoolSize = 1; + private PayloadMappingDefinition payloadMappingDefinition = + ConnectivityModelFactory.emptyPayloadMappingDefinition(); + private final Map specificConfig = new HashMap<>(); + + private Builder(final ConnectionType connectionType) { + this.connectionType = checkNotNull(connectionType, "Connection Type"); + } + + private static boolean isBlankOrNull(@Nullable final String toTest) { + return null == toTest || toTest.trim().isEmpty(); + } + + @Override + public ConnectionBuilder id(final ConnectionId id) { + this.id = checkNotNull(id, "ID"); + return this; + } + + @Override + public ConnectionBuilder name(@Nullable final String name) { + this.name = name; + return this; + } + + @Override + public ConnectionBuilder credentials(@Nullable Credentials credentials) { + this.credentials = credentials; + return this; + } + + @Override + public HonoConnection.Builder trustedCertificates(@Nullable final String trustedCertificates) { + if (isBlankOrNull(trustedCertificates)) { + this.trustedCertificates = null; + } else { + this.trustedCertificates = trustedCertificates; + } + return this; + } + + @Override + public ConnectionBuilder uri(final String uri) { + this.uri = uri; + return this; + } + + @Override + public ConnectionBuilder connectionStatus(final ConnectivityStatus connectionStatus) { + this.connectionStatus = checkNotNull(connectionStatus, "ConnectionStatus"); + return this; + } + + @Override + public ConnectionBuilder failoverEnabled(final boolean failOverEnabled) { + this.failOverEnabled = failOverEnabled; + return this; + } + + @Override + public ConnectionBuilder validateCertificate(final boolean validateCertificate) { + this.validateCertificate = validateCertificate; + return this; + } + + @Override + public ConnectionBuilder processorPoolSize(final int processorPoolSize) { + checkArgument(processorPoolSize, ps -> ps > 0, () -> "The processor pool size must be positive!"); + this.processorPoolSize = processorPoolSize; + return this; + } + + @Override + public ConnectionBuilder sources(final List sources) { + this.sources.addAll(checkNotNull(sources, "sources")); + return this; + } + + @Override + public ConnectionBuilder targets(final List targets) { + this.targets.addAll(checkNotNull(targets, "targets")); + return this; + } + + @Override + public ConnectionBuilder setSources(final List sources) { + this.sources.clear(); + return sources(sources); + } + + @Override + public ConnectionBuilder setTargets(final List targets) { + this.targets.clear(); + return targets(targets); + } + + @Override + public ConnectionBuilder clientCount(final int clientCount) { + checkArgument(clientCount, ps -> ps > 0, () -> "The client count must be > 0!"); + this.clientCount = clientCount; + return this; + } + + @Override + public ConnectionBuilder specificConfig(final Map specificConfig) { + this.specificConfig.putAll(checkNotNull(specificConfig, "Specific Config")); + return this; + } + + @Override + public ConnectionBuilder mappingContext(@Nullable final MappingContext mappingContext) { + this.mappingContext = mappingContext; + return this; + } + + @Override + public ConnectionBuilder tags(final Collection tags) { + this.tags = new LinkedHashSet<>(checkNotNull(tags, "tags to set")); + return this; + } + + @Override + public ConnectionBuilder tag(final String tag) { + tags.add(checkNotNull(tag, "tag to set")); + return this; + } + + @Override + public ConnectionBuilder lifecycle(@Nullable final ConnectionLifecycle lifecycle) { + this.lifecycle = lifecycle; + return this; + } + + @Override + public ConnectionBuilder sshTunnel(@Nullable final SshTunnel sshTunnel) { + this.sshTunnel = sshTunnel; + return this; + } + + @Override + public ConnectionBuilder payloadMappingDefinition(final PayloadMappingDefinition payloadMappingDefinition) { + this.payloadMappingDefinition = payloadMappingDefinition; + return this; + } + + @Override + public Connection build() { + checkSourceAndTargetAreValid(); + checkAuthorizationContextsAreValid(); + checkConnectionAnnouncementsOnlySetIfClientCount1(); + migrateLegacyConfigurationOnTheFly(); + return new HonoConnection(this); + } + + private boolean shouldMigrateMappingContext() { + return mappingContext != null; + } + + private void migrateLegacyConfigurationOnTheFly() { + if (shouldMigrateMappingContext()) { + this.payloadMappingDefinition = + payloadMappingDefinition.withDefinition(MIGRATED_MAPPER_ID, mappingContext); + } + setSources(sources.stream().map(this::migrateSource).collect(Collectors.toList())); + setTargets(targets.stream().map(this::migrateTarget).collect(Collectors.toList())); + } + + private Source migrateSource(final Source source) { + final Source sourceAfterReplyTargetMigration = ImmutableSource.migrateReplyTarget(source, connectionType); + if (shouldMigrateMappingContext()) { + return new ImmutableSource.Builder(sourceAfterReplyTargetMigration) + .payloadMapping(addMigratedPayloadMappings(source.getPayloadMapping())) + .build(); + } else { + return sourceAfterReplyTargetMigration; + } + } + + private Target migrateTarget(final Target target) { + final boolean shouldAddHeaderMapping = shouldAddDefaultHeaderMappingToTarget(connectionType); + final boolean shouldMigrateMappingContext = shouldMigrateMappingContext(); + if (shouldMigrateMappingContext || shouldAddHeaderMapping) { + final TargetBuilder builder = new ImmutableTarget.Builder(target); + if (shouldMigrateMappingContext) { + builder.payloadMapping(addMigratedPayloadMappings(target.getPayloadMapping())); + } + if (shouldAddHeaderMapping) { + builder.headerMapping(target.getHeaderMapping()); + } + return builder.build(); + } else { + return target; + } + } + + private boolean shouldAddDefaultHeaderMappingToTarget(final ConnectionType connectionType) { + switch (connectionType) { + case AMQP_091: + case AMQP_10: + case KAFKA: + case MQTT_5: + return true; + case MQTT: + case HTTP_PUSH: + default: + return false; + } + } + + + private PayloadMapping addMigratedPayloadMappings(final PayloadMapping payloadMapping) { + final ArrayList merged = new ArrayList<>(payloadMapping.getMappings()); + merged.add(MIGRATED_MAPPER_ID); + return ConnectivityModelFactory.newPayloadMapping(merged); + } + + private void checkSourceAndTargetAreValid() { + if (sources.isEmpty() && targets.isEmpty()) { + throw ConnectionConfigurationInvalidException.newBuilder("Either a source or a target must be " + + "specified in the configuration of a connection!").build(); + } + } + + /** + * If no context is set on connection level each target and source must have its own context. + */ + private void checkAuthorizationContextsAreValid() { + // if the auth context on connection level is empty, + // an auth context is required to be set on each source/target + final Set sourcesWithoutAuthContext = sources.stream() + .filter(source -> source.getAuthorizationContext().isEmpty()) + .flatMap(source -> source.getAddresses().stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + final Set targetsWithoutAuthContext = targets.stream() + .filter(target -> target.getAuthorizationContext().isEmpty()) + .map(Target::getAddress) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + if (!sourcesWithoutAuthContext.isEmpty() || !targetsWithoutAuthContext.isEmpty()) { + final StringBuilder message = new StringBuilder("The "); + if (!sourcesWithoutAuthContext.isEmpty()) { + message.append("Sources ").append(sourcesWithoutAuthContext); + } + if (!sourcesWithoutAuthContext.isEmpty() && !targetsWithoutAuthContext.isEmpty()) { + message.append(" and "); + } + if (!targetsWithoutAuthContext.isEmpty()) { + message.append("Targets ").append(targetsWithoutAuthContext); + } + message.append(" are missing an authorization context."); + throw ConnectionConfigurationInvalidException.newBuilder(message.toString()).build(); + } + } + + private void checkConnectionAnnouncementsOnlySetIfClientCount1() { + if (clientCount > 1 && containsTargetWithConnectionAnnouncementsTopic()) { + final String message = MessageFormat.format("Connection announcements (topic {0}) can" + + " only be used with client count 1.", Topic.CONNECTION_ANNOUNCEMENTS.getName()); + throw ConnectionConfigurationInvalidException.newBuilder(message) + .build(); + } + } + + private boolean containsTargetWithConnectionAnnouncementsTopic() { + return targets.stream() + .map(Target::getTopics) + .flatMap(Set::stream) + .map(FilteredTopic::getTopic) + .anyMatch(Topic.CONNECTION_ANNOUNCEMENTS::equals); + } + + } + + @Immutable + static final class ConnectionUri { + + private static final String MASKED_URI_PATTERN = "{0}://{1}{2}:{3,number,#}{4}"; + + @SuppressWarnings("squid:S2068") // S2068 tripped due to 'PASSWORD' in variable name + private static final String USERNAME_PASSWORD_SEPARATOR = ":"; + + @Nullable private final String uriString; + @Nullable private final String protocol; + @Nullable private final String hostname; + @Nullable private final int port; + @Nullable private final String path; + @Nullable private final String userName; + @Nullable private final String password; + private final String uriStringWithMaskedPassword; + + private ConnectionUri(final String theUriString) { + final URI uri; + if (theUriString != null) { + try { + uri = new URI(theUriString).parseServerAuthority(); + } catch (final URISyntaxException e) { + throw ConnectionUriInvalidException.newBuilder(theUriString).build(); + } + // validate self + if (!isValid(uri)) { + throw ConnectionUriInvalidException.newBuilder(theUriString).build(); + } + + uriString = uri.toASCIIString(); + protocol = uri.getScheme(); + hostname = uri.getHost(); + port = uri.getPort(); + path = uri.getPath(); + + // initialize nullable fields + final String userInfo = uri.getUserInfo(); + if (userInfo != null && userInfo.contains(USERNAME_PASSWORD_SEPARATOR)) { + final int separatorIndex = userInfo.indexOf(USERNAME_PASSWORD_SEPARATOR); + userName = userInfo.substring(0, separatorIndex); + password = userInfo.substring(separatorIndex + 1); + } else { + userName = null; + password = null; + } + + // must be initialized after all else + uriStringWithMaskedPassword = createUriStringWithMaskedPassword(); + } else { + uriString = ""; + protocol = ""; + hostname = ""; + port = 9999; + path = ""; + userName = null; + password = null; + uriStringWithMaskedPassword = null; + } + } + + private String createUriStringWithMaskedPassword() { + return MessageFormat.format(MASKED_URI_PATTERN, protocol, getUserCredentialsOrEmptyString(), hostname, port, + getPathOrEmptyString()); + } + + private String getUserCredentialsOrEmptyString() { + if (null != userName && null != password) { + return userName + ":*****@"; + } + return ""; + } + + private String getPathOrEmptyString() { + return getPath().orElse(""); + } + + /** + * Test validity of a connection URI. A connection URI is valid if it has an explicit port number ,has no query + * parameters, and has a nonempty password whenever it has a nonempty username. + * + * @param uri the URI object with which the connection URI is created. + * @return whether the connection URI is valid. + */ + private static boolean isValid(final URI uri) { + return uri.getPort() > 0 && uri.getQuery() == null; + } + + /** + * Returns a new instance of {@code ConnectionUri}. The is the reverse function of {@link #toString()}. + * + * @param uriString the string representation of the Connection URI. + * @return the instance. + * @throws NullPointerException if {@code uriString} is {@code null}. + * @throws ConnectionUriInvalidException if {@code uriString} is not a + * valid URI. + * @see #toString() + */ + static ConnectionUri of(final String uriString) { + return new ConnectionUri(uriString); + } + + String getProtocol() { + return protocol; + } + + Optional getUserName() { + return Optional.ofNullable(userName); + } + + Optional getPassword() { + return Optional.ofNullable(password); + } + + String getHostname() { + return hostname; + } + + int getPort() { + return port; + } + + /** + * Returns the path or an empty string. + * + * @return the path or an empty string. + */ + Optional getPath() { + return path.isEmpty() ? Optional.empty() : Optional.of(path); + } + + String getUriStringWithMaskedPassword() { + return uriStringWithMaskedPassword; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ConnectionUri that = (ConnectionUri) o; + return Objects.equals(uriString, that.uriString); + } + + @Override + public int hashCode() { + return Objects.hash(uriString); + } + + /** + * @return the string representation of this ConnectionUri. This is the reverse function of {@link #of(String)}. + * @see #of(String) + */ + @Override + public String toString() { + return uriString; + } + + } + + protected static boolean isHonoConnectionType(final JsonObject connectionJsonObject) { + return connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_TYPE) + .flatMap(ConnectionType::forName) + .filter(ConnectionType.HONO::equals) + .isPresent(); + } + + protected static boolean isHonoConnectionType(final Connection connection) { + return connection.getConnectionType() == ConnectionType.HONO; + } + +} + diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/validation/ConnectionValidator.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/validation/ConnectionValidator.java index afeb2a062c..6c9bf95f3e 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/validation/ConnectionValidator.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/validation/ConnectionValidator.java @@ -199,13 +199,16 @@ void validate(final Connection connection, final DittoHeaders dittoHeaders, fina final ConnectionLogger connectionLogger = ConnectionLogger.getInstance(connection.getId(), connectivityConfig.getMonitoringConfig().logger()); validateFormatOfCertificates(connection, dittoHeaders, connectionLogger); - + final ConnectionType connectionType = connection.getConnectionType(); // validate configured host final HostValidator hostValidator = new DefaultHostValidator(connectivityConfig, loggingAdapter); - hostValidator.validateHostname(connection.getHostname(), dittoHeaders); + + if(connectionType != ConnectionType.HONO) { + hostValidator.validateHostname(connection.getHostname(), dittoHeaders); + } // tunneling not supported for kafka - if (ConnectionType.KAFKA == connection.getConnectionType() && connection.getSshTunnel().isPresent()) { + if (ConnectionType.KAFKA == connectionType && connection.getSshTunnel().isPresent()) { throw ConnectionConfigurationInvalidException .newBuilder("SSH tunneling not supported.") .description( From e649ad076ba80fe6568bf225e296a506785981f1 Mon Sep 17 00:00:00 2001 From: "Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM)" Date: Wed, 2 Nov 2022 17:19:42 +0200 Subject: [PATCH 56/65] Fixed HonoConnection implementation and added HonoConnection test ConnectionUri class made public --- .../api/ConnectivityMappingStrategies.java | 1 + .../connectivity/model/ConnectionUri.java | 191 +++++++ .../model/ConnectivityModelFactory.java | 21 +- .../connectivity/model/HonoConnection.java | 323 ++---------- .../model/ImmutableConnection.java | 170 +----- .../connectivity/model/ConnectionUriTest.java | 206 ++++++++ .../model/HonoConnectionTest.java | 498 ++++++++++++++++++ .../model/ImmutableConnectionTest.java | 101 ---- 8 files changed, 962 insertions(+), 549 deletions(-) create mode 100644 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java create mode 100644 connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java create mode 100644 connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java diff --git a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/ConnectivityMappingStrategies.java b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/ConnectivityMappingStrategies.java index b4ba1d97d2..cbdec89cfc 100644 --- a/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/ConnectivityMappingStrategies.java +++ b/connectivity/api/src/main/java/org/eclipse/ditto/connectivity/api/ConnectivityMappingStrategies.java @@ -78,6 +78,7 @@ private static MappingStrategies getConnectivityMappingStrategies() { .add(GlobalErrorRegistry.getInstance()) .add(Connection.class, ConnectivityModelFactory::connectionFromJson) .add("ImmutableConnection", ConnectivityModelFactory::connectionFromJson) + .add("HonoConnection", ConnectivityModelFactory::connectionFromJson) .add(ResourceStatus.class, ConnectivityModelFactory::resourceStatusFromJson) .add("ImmutableResourceStatus", ConnectivityModelFactory::resourceStatusFromJson) .add(ConnectivityStatus.class, ConnectivityStatus::fromJson) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java new file mode 100644 index 0000000000..c4e1329879 --- /dev/null +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2017 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model; + +import java.net.URI; +import java.net.URISyntaxException; +import java.text.MessageFormat; +import java.util.Objects; +import java.util.Optional; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Represents a uri within the Connectivity service. + */ +@Immutable +final class ConnectionUri { + + private static final String MASKED_URI_PATTERN = "{0}://{1}{2}:{3,number,#}{4}"; + + @SuppressWarnings("squid:S2068") // S2068 tripped due to 'PASSWORD' in variable name + private static final String USERNAME_PASSWORD_SEPARATOR = ":"; + + private final String uriString; + private final String protocol; + private final String hostname; + private final int port; + private final String path; + @Nullable private final String userName; + @Nullable private final String password; + private final String uriStringWithMaskedPassword; + + private ConnectionUri(@Nullable final String theUriString) { + if (!isBlankOrNull(theUriString)) { + final URI uri; + try { + uri = new URI(theUriString).parseServerAuthority(); + } catch (final URISyntaxException e) { + throw ConnectionUriInvalidException.newBuilder(theUriString).build(); + } + // validate self + if (!isValid(uri)) { + throw ConnectionUriInvalidException.newBuilder(theUriString).build(); + } + uriString = uri.toASCIIString(); + protocol = uri.getScheme(); + hostname = uri.getHost(); + port = uri.getPort(); + path = uri.getPath(); + + // initialize nullable fields + final String userInfo = uri.getUserInfo(); + if (userInfo != null && userInfo.contains(USERNAME_PASSWORD_SEPARATOR)) { + final int separatorIndex = userInfo.indexOf(USERNAME_PASSWORD_SEPARATOR); + userName = userInfo.substring(0, separatorIndex); + password = userInfo.substring(separatorIndex + 1); + } else { + userName = null; + password = null; + } + + // must be initialized after all else + uriStringWithMaskedPassword = createUriStringWithMaskedPassword(); + } else { + uriString = ""; + protocol = ""; + hostname = ""; + port = 9999; + path = ""; + userName = null; + password = null; + uriStringWithMaskedPassword = ""; + } + } + + private String createUriStringWithMaskedPassword() { + return MessageFormat.format(MASKED_URI_PATTERN, protocol, getUserCredentialsOrEmptyString(), hostname, port, + getPathOrEmptyString()); + } + + private String getUserCredentialsOrEmptyString() { + if (null != userName && null != password) { + return userName + ":*****@"; + } + return ""; + } + + private String getPathOrEmptyString() { + return getPath().orElse(""); + } + + /** + * Test validity of a connection URI. A connection URI is valid if it has an explicit port number ,has no query + * parameters, and has a nonempty password whenever it has a nonempty username. + * + * @param uri the URI object with which the connection URI is created. + * @return whether the connection URI is valid. + */ + static boolean isValid(final URI uri) { + return uri.getPort() > 0 && uri.getQuery() == null; + } + + /** + * Returns a new instance of {@code ConnectionUri}. The is the reverse function of {@link #toString()}. + * + * @param uriString the string representation of the Connection URI. + * @return the instance. + * @throws NullPointerException if {@code uriString} is {@code null}. + * @throws ConnectionUriInvalidException if {@code uriString} is not a + * valid URI. + * @see #toString() + */ + static ConnectionUri of( @Nullable final String uriString) { + return new ConnectionUri(uriString); + } + + String getProtocol() { + return protocol; + } + + Optional getUserName() { + return Optional.ofNullable(userName); + } + + Optional getPassword() { + return Optional.ofNullable(password); + } + + String getHostname() { + return hostname; + } + + int getPort() { + return port; + } + + static boolean isBlankOrNull(@Nullable final String toTest) { + return null == toTest || toTest.trim().isEmpty(); + } + + /** + * Returns the path or an empty string. + * + * @return the path or an empty string. + */ + Optional getPath() { + return path.isEmpty() ? Optional.empty() : Optional.of(path); + } + + String getUriStringWithMaskedPassword() { + return uriStringWithMaskedPassword; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ConnectionUri that = (ConnectionUri) o; + return Objects.equals(uriString, that.uriString); + } + + @Override + public int hashCode() { + return Objects.hash(uriString); + } + + /** + * @return the string representation of this ConnectionUri. This is the reverse function of {@link #of(String)}. + * @see #of(String) + */ + @Override + public String toString() { + return uriString; + } + +} \ No newline at end of file diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java index dc680c1ad3..69a25a1819 100755 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java @@ -70,10 +70,13 @@ public static ConnectionBuilder newConnectionBuilder(final ConnectionId id, final ConnectionType connectionType, final ConnectivityStatus connectionStatus, final String uri) { + final ConnectionBuilder builder; if (connectionType == ConnectionType.HONO) { - return HonoConnection.getBuilder(id, connectionType, connectionStatus, uri); + builder = HonoConnection.getBuilder(id, connectionType, connectionStatus, uri); + } else { + builder = ImmutableConnection.getBuilder(id, connectionType, connectionStatus, uri); } - return ImmutableConnection.getBuilder(id, connectionType, connectionStatus, uri); + return builder; } /** @@ -85,10 +88,13 @@ public static ConnectionBuilder newConnectionBuilder(final ConnectionId id, * @throws NullPointerException if {@code connection} is {@code null}. */ public static ConnectionBuilder newConnectionBuilder(final Connection connection) { + final ConnectionBuilder builder; if (HonoConnection.isHonoConnectionType(connection)) { - return HonoConnection.getBuilder(connection); + builder = HonoConnection.getBuilder(connection); + } else { + builder = ImmutableConnection.getBuilder(connection); } - return ImmutableConnection.getBuilder(connection); + return builder; } /** @@ -100,10 +106,13 @@ public static ConnectionBuilder newConnectionBuilder(final Connection connection * @throws org.eclipse.ditto.json.JsonParseException if {@code jsonObject} is not an appropriate JSON object. */ public static Connection connectionFromJson(final JsonObject jsonObject) { + final Connection connection; if (HonoConnection.isHonoConnectionType(jsonObject)) { - return HonoConnection.fromJson(jsonObject); + connection = HonoConnection.fromJson(jsonObject); + } else { + connection = ImmutableConnection.fromJson(jsonObject); } - return ImmutableConnection.fromJson(jsonObject); + return connection; } /** diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java index 5a935e0f95..413a6988af 100755 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java @@ -15,8 +15,6 @@ import static org.eclipse.ditto.base.model.common.ConditionChecker.checkArgument; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; -import java.net.URI; -import java.net.URISyntaxException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; @@ -31,34 +29,35 @@ import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.IntStream; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.NotThreadSafe; import org.eclipse.ditto.base.model.json.JsonSchemaVersion; -import org.eclipse.ditto.json.JsonArray; + import org.eclipse.ditto.json.JsonCollectors; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonField; import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonObjectBuilder; -import org.eclipse.ditto.json.JsonParseException; import org.eclipse.ditto.json.JsonValue; + +/** + * Immutable implementation of {@link Connection} of type + * {@link org.eclipse.ditto.connectivity.model.ConnectionType} HONO. + */ @Immutable -public class HonoConnection implements Connection { +final class HonoConnection implements Connection { private final ConnectionId id; @Nullable private final String name; private final ConnectionType connectionType; private final ConnectivityStatus connectionStatus; - @Nullable private final HonoConnection.ConnectionUri uri; + private final ConnectionUri uri; @Nullable private final Credentials credentials; @Nullable private final String trustedCertificates; @Nullable private final ConnectionLifecycle lifecycle; - - private final List sources; private final List targets; private final int clientCount; @@ -77,7 +76,7 @@ private HonoConnection(final HonoConnection.Builder builder) { connectionStatus = checkNotNull(builder.connectionStatus, "connectionStatus"); credentials = builder.credentials; trustedCertificates = builder.trustedCertificates; - uri = HonoConnection.ConnectionUri.of(builder.uri); + uri = ConnectionUri.of(builder.uri); sources = Collections.unmodifiableList(new ArrayList<>(builder.sources)); targets = Collections.unmodifiableList(new ArrayList<>(builder.targets)); clientCount = builder.clientCount; @@ -104,7 +103,7 @@ private HonoConnection(final HonoConnection.Builder builder) { public static ConnectionBuilder getBuilder(final ConnectionId id, final ConnectionType connectionType, final ConnectivityStatus connectionStatus, - final String uri) { + @Nullable final String uri) { return new HonoConnection.Builder(connectionType) .id(id) @@ -116,7 +115,7 @@ public static ConnectionBuilder getBuilder(final ConnectionId id, * Returns a new {@code ConnectionBuilder} object. * * @param connection the connection to use for initializing the builder. - * @return new instance of {@code ImmutableConnectionBuilder}. + * @return new instance of {@code ConnectionBuilder}. * @throws NullPointerException if {@code connection} is {@code null}. */ public static ConnectionBuilder getBuilder(final Connection connection) { @@ -149,9 +148,10 @@ public static ConnectionBuilder getBuilder(final Connection connection) { * @return a new Connection which is initialised with the extracted data from {@code jsonObject}. * @throws NullPointerException if {@code jsonObject} is {@code null}. * @throws org.eclipse.ditto.json.JsonParseException if {@code jsonObject} is not an appropriate JSON object. + * @throws org.eclipse.ditto.json.JsonMissingFieldException if {@code jsonObject} does not contain a value at the defined location. */ public static Connection fromJson(final JsonObject jsonObject) { - final ConnectionType type = getConnectionTypeOrThrow(jsonObject); + final ConnectionType type = ImmutableConnection.getConnectionTypeOrThrow(jsonObject); final MappingContext mappingContext = jsonObject.getValue(JsonFields.MAPPING_CONTEXT) .map(ConnectivityModelFactory::mappingContextFromJson) .orElse(null); @@ -163,15 +163,15 @@ public static Connection fromJson(final JsonObject jsonObject) { final ConnectionBuilder builder = new HonoConnection.Builder(type) .id(ConnectionId.of(jsonObject.getValueOrThrow(JsonFields.ID))) - .connectionStatus(getConnectionStatusOrThrow(jsonObject)) - .uri(jsonObject.getValueOrThrow(JsonFields.URI)) - .sources(getSources(jsonObject)) - .targets(getTargets(jsonObject)) + .connectionStatus(ImmutableConnection.getConnectionStatusOrThrow(jsonObject)) + .uri(jsonObject.getValue(JsonFields.URI).orElse("")) + .sources(ImmutableConnection.getSources(jsonObject)) + .targets(ImmutableConnection.getTargets(jsonObject)) .name(jsonObject.getValue(JsonFields.NAME).orElse(null)) .mappingContext(mappingContext) .payloadMappingDefinition(payloadMappingDefinition) - .specificConfig(getSpecificConfiguration(jsonObject)) - .tags(getTags(jsonObject)); + .specificConfig(ImmutableConnection.getSpecificConfiguration(jsonObject)) + .tags(ImmutableConnection.getTags(jsonObject)); jsonObject.getValue(JsonFields.LIFECYCLE) .flatMap(ConnectionLifecycle::forName).ifPresent(builder::lifecycle); @@ -187,66 +187,15 @@ public static Connection fromJson(final JsonObject jsonObject) { return builder.build(); } - private static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { - final String readConnectionType = jsonObject.getValueOrThrow(JsonFields.CONNECTION_TYPE); - return ConnectionType.forName(readConnectionType) - .orElseThrow(() -> JsonParseException.newBuilder() - .message(MessageFormat.format("Connection type <{0}> is invalid!", readConnectionType)) - .build()); - } - - private static ConnectivityStatus getConnectionStatusOrThrow(final JsonObject jsonObject) { - final String readConnectionStatus = jsonObject.getValueOrThrow(JsonFields.CONNECTION_STATUS); - return ConnectivityStatus.forName(readConnectionStatus) - .orElseThrow(() -> JsonParseException.newBuilder() - .message(MessageFormat.format("Connection status <{0}> is invalid!", readConnectionStatus)) - .build()); - } - - private static List getSources(final JsonObject jsonObject) { - final Optional sourcesArray = jsonObject.getValue(JsonFields.SOURCES); - if (sourcesArray.isPresent()) { - final JsonArray values = sourcesArray.get(); - return IntStream.range(0, values.getSize()) - .mapToObj(index -> values.get(index) - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .map(valueAsObject -> ConnectivityModelFactory.sourceFromJson(valueAsObject, index))) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - } else { - return Collections.emptyList(); - } - } - - private static List getTargets(final JsonObject jsonObject) { - return jsonObject.getValue(JsonFields.TARGETS) - .map(array -> array.stream() - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .map(ConnectivityModelFactory::targetFromJson) - .collect(Collectors.toList())) - .orElse(Collections.emptyList()); - } - - private static Map getSpecificConfiguration(final JsonObject jsonObject) { - return jsonObject.getValue(JsonFields.SPECIFIC_CONFIG) - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .map(JsonObject::stream) - .map(jsonFields -> jsonFields.collect(Collectors.toMap(JsonField::getKeyName, - f -> f.getValue().isString() ? f.getValue().asString() : f.getValue().toString()))) - .orElse(Collections.emptyMap()); - } - - private static Set getTags(final JsonObject jsonObject) { - return jsonObject.getValue(JsonFields.TAGS) - .map(array -> array.stream() - .filter(JsonValue::isString) - .map(JsonValue::asString) - .collect(Collectors.toCollection(LinkedHashSet::new))) - .orElseGet(LinkedHashSet::new); + static boolean isHonoConnectionType(final JsonObject connectionJsonObject) { + return connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_TYPE) + .flatMap(ConnectionType::forName) + .filter(ConnectionType.HONO::equals) + .isPresent(); + } + + static boolean isHonoConnectionType(final Connection connection) { + return connection.getConnectionType() == ConnectionType.HONO; } @Override @@ -465,7 +414,7 @@ public String toString() { ", failoverEnabled=" + failOverEnabled + ", credentials=" + credentials + ", trustedCertificates=hash:" + Objects.hash(trustedCertificates) + - ", uri=" + uri.uriString + + ", uri=\"\"" + ", sources=" + sources + ", targets=" + targets + ", sshTunnel=" + sshTunnel + @@ -480,7 +429,7 @@ public String toString() { } /** - * Builder for {@code ImmutableConnection}. + * Builder for {@code HonoConnection}. */ @NotThreadSafe private static final class Builder implements ConnectionBuilder { @@ -514,16 +463,12 @@ private static final class Builder implements ConnectionBuilder { private final Map specificConfig = new HashMap<>(); private Builder(final ConnectionType connectionType) { - this.connectionType = checkNotNull(connectionType, "Connection Type"); - } - - private static boolean isBlankOrNull(@Nullable final String toTest) { - return null == toTest || toTest.trim().isEmpty(); + this.connectionType = checkNotNull(connectionType, "connectionType"); } @Override public ConnectionBuilder id(final ConnectionId id) { - this.id = checkNotNull(id, "ID"); + this.id = checkNotNull(id, "id"); return this; } @@ -534,11 +479,15 @@ public ConnectionBuilder name(@Nullable final String name) { } @Override - public ConnectionBuilder credentials(@Nullable Credentials credentials) { + public ConnectionBuilder credentials(@Nullable final Credentials credentials) { this.credentials = credentials; return this; } + static boolean isBlankOrNull(@Nullable final String toTest) { + return null == toTest || toTest.trim().isEmpty(); + } + @Override public HonoConnection.Builder trustedCertificates(@Nullable final String trustedCertificates) { if (isBlankOrNull(trustedCertificates)) { @@ -550,14 +499,14 @@ public HonoConnection.Builder trustedCertificates(@Nullable final String trusted } @Override - public ConnectionBuilder uri(final String uri) { + public ConnectionBuilder uri(@Nullable final String uri) { this.uri = uri; return this; } @Override public ConnectionBuilder connectionStatus(final ConnectivityStatus connectionStatus) { - this.connectionStatus = checkNotNull(connectionStatus, "ConnectionStatus"); + this.connectionStatus = checkNotNull(connectionStatus, "connectionStatus"); return this; } @@ -606,14 +555,14 @@ public ConnectionBuilder setTargets(final List targets) { @Override public ConnectionBuilder clientCount(final int clientCount) { - checkArgument(clientCount, ps -> ps > 0, () -> "The client count must be > 0!"); + checkArgument(clientCount, ps -> ps > 0, () -> "The client count must be positive!"); this.clientCount = clientCount; return this; } @Override public ConnectionBuilder specificConfig(final Map specificConfig) { - this.specificConfig.putAll(checkNotNull(specificConfig, "Specific Config")); + this.specificConfig.putAll(checkNotNull(specificConfig, "specificConfig")); return this; } @@ -625,13 +574,13 @@ public ConnectionBuilder mappingContext(@Nullable final MappingContext mappingCo @Override public ConnectionBuilder tags(final Collection tags) { - this.tags = new LinkedHashSet<>(checkNotNull(tags, "tags to set")); + this.tags = new LinkedHashSet<>(checkNotNull(tags, "tags")); return this; } @Override public ConnectionBuilder tag(final String tag) { - tags.add(checkNotNull(tag, "tag to set")); + tags.add(checkNotNull(tag, "tag")); return this; } @@ -704,17 +653,7 @@ private Target migrateTarget(final Target target) { } private boolean shouldAddDefaultHeaderMappingToTarget(final ConnectionType connectionType) { - switch (connectionType) { - case AMQP_091: - case AMQP_10: - case KAFKA: - case MQTT_5: - return true; - case MQTT: - case HTTP_PUSH: - default: - return false; - } + return connectionType == ConnectionType.HONO; } @@ -781,179 +720,5 @@ private boolean containsTargetWithConnectionAnnouncementsTopic() { } - @Immutable - static final class ConnectionUri { - - private static final String MASKED_URI_PATTERN = "{0}://{1}{2}:{3,number,#}{4}"; - - @SuppressWarnings("squid:S2068") // S2068 tripped due to 'PASSWORD' in variable name - private static final String USERNAME_PASSWORD_SEPARATOR = ":"; - - @Nullable private final String uriString; - @Nullable private final String protocol; - @Nullable private final String hostname; - @Nullable private final int port; - @Nullable private final String path; - @Nullable private final String userName; - @Nullable private final String password; - private final String uriStringWithMaskedPassword; - - private ConnectionUri(final String theUriString) { - final URI uri; - if (theUriString != null) { - try { - uri = new URI(theUriString).parseServerAuthority(); - } catch (final URISyntaxException e) { - throw ConnectionUriInvalidException.newBuilder(theUriString).build(); - } - // validate self - if (!isValid(uri)) { - throw ConnectionUriInvalidException.newBuilder(theUriString).build(); - } - - uriString = uri.toASCIIString(); - protocol = uri.getScheme(); - hostname = uri.getHost(); - port = uri.getPort(); - path = uri.getPath(); - - // initialize nullable fields - final String userInfo = uri.getUserInfo(); - if (userInfo != null && userInfo.contains(USERNAME_PASSWORD_SEPARATOR)) { - final int separatorIndex = userInfo.indexOf(USERNAME_PASSWORD_SEPARATOR); - userName = userInfo.substring(0, separatorIndex); - password = userInfo.substring(separatorIndex + 1); - } else { - userName = null; - password = null; - } - - // must be initialized after all else - uriStringWithMaskedPassword = createUriStringWithMaskedPassword(); - } else { - uriString = ""; - protocol = ""; - hostname = ""; - port = 9999; - path = ""; - userName = null; - password = null; - uriStringWithMaskedPassword = null; - } - } - - private String createUriStringWithMaskedPassword() { - return MessageFormat.format(MASKED_URI_PATTERN, protocol, getUserCredentialsOrEmptyString(), hostname, port, - getPathOrEmptyString()); - } - - private String getUserCredentialsOrEmptyString() { - if (null != userName && null != password) { - return userName + ":*****@"; - } - return ""; - } - - private String getPathOrEmptyString() { - return getPath().orElse(""); - } - - /** - * Test validity of a connection URI. A connection URI is valid if it has an explicit port number ,has no query - * parameters, and has a nonempty password whenever it has a nonempty username. - * - * @param uri the URI object with which the connection URI is created. - * @return whether the connection URI is valid. - */ - private static boolean isValid(final URI uri) { - return uri.getPort() > 0 && uri.getQuery() == null; - } - - /** - * Returns a new instance of {@code ConnectionUri}. The is the reverse function of {@link #toString()}. - * - * @param uriString the string representation of the Connection URI. - * @return the instance. - * @throws NullPointerException if {@code uriString} is {@code null}. - * @throws ConnectionUriInvalidException if {@code uriString} is not a - * valid URI. - * @see #toString() - */ - static ConnectionUri of(final String uriString) { - return new ConnectionUri(uriString); - } - - String getProtocol() { - return protocol; - } - - Optional getUserName() { - return Optional.ofNullable(userName); - } - - Optional getPassword() { - return Optional.ofNullable(password); - } - - String getHostname() { - return hostname; - } - - int getPort() { - return port; - } - - /** - * Returns the path or an empty string. - * - * @return the path or an empty string. - */ - Optional getPath() { - return path.isEmpty() ? Optional.empty() : Optional.of(path); - } - - String getUriStringWithMaskedPassword() { - return uriStringWithMaskedPassword; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final ConnectionUri that = (ConnectionUri) o; - return Objects.equals(uriString, that.uriString); - } - - @Override - public int hashCode() { - return Objects.hash(uriString); - } - - /** - * @return the string representation of this ConnectionUri. This is the reverse function of {@link #of(String)}. - * @see #of(String) - */ - @Override - public String toString() { - return uriString; - } - - } - - protected static boolean isHonoConnectionType(final JsonObject connectionJsonObject) { - return connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_TYPE) - .flatMap(ConnectionType::forName) - .filter(ConnectionType.HONO::equals) - .isPresent(); - } - - protected static boolean isHonoConnectionType(final Connection connection) { - return connection.getConnectionType() == ConnectionType.HONO; - } - } diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java index 8cdfdef3ee..35ca605214 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java @@ -15,10 +15,6 @@ import static org.eclipse.ditto.base.model.common.ConditionChecker.checkArgument; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLDecoder; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; @@ -193,7 +189,7 @@ public static Connection fromJson(final JsonObject jsonObject) { return builder.build(); } - private static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { + static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { final String readConnectionType = jsonObject.getValueOrThrow(JsonFields.CONNECTION_TYPE); return ConnectionType.forName(readConnectionType) .orElseThrow(() -> JsonParseException.newBuilder() @@ -201,7 +197,7 @@ private static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObje .build()); } - private static ConnectivityStatus getConnectionStatusOrThrow(final JsonObject jsonObject) { + static ConnectivityStatus getConnectionStatusOrThrow(final JsonObject jsonObject) { final String readConnectionStatus = jsonObject.getValueOrThrow(JsonFields.CONNECTION_STATUS); return ConnectivityStatus.forName(readConnectionStatus) .orElseThrow(() -> JsonParseException.newBuilder() @@ -209,7 +205,7 @@ private static ConnectivityStatus getConnectionStatusOrThrow(final JsonObject js .build()); } - private static List getSources(final JsonObject jsonObject) { + static List getSources(final JsonObject jsonObject) { final Optional sourcesArray = jsonObject.getValue(JsonFields.SOURCES); if (sourcesArray.isPresent()) { final JsonArray values = sourcesArray.get(); @@ -226,7 +222,7 @@ private static List getSources(final JsonObject jsonObject) { } } - private static List getTargets(final JsonObject jsonObject) { + static List getTargets(final JsonObject jsonObject) { return jsonObject.getValue(JsonFields.TARGETS) .map(array -> array.stream() .filter(JsonValue::isObject) @@ -236,7 +232,7 @@ private static List getTargets(final JsonObject jsonObject) { .orElse(Collections.emptyList()); } - private static Map getSpecificConfiguration(final JsonObject jsonObject) { + static Map getSpecificConfiguration(final JsonObject jsonObject) { return jsonObject.getValue(JsonFields.SPECIFIC_CONFIG) .filter(JsonValue::isObject) .map(JsonValue::asObject) @@ -246,7 +242,7 @@ private static Map getSpecificConfiguration(final JsonObject jso .orElse(Collections.emptyMap()); } - private static Set getTags(final JsonObject jsonObject) { + static Set getTags(final JsonObject jsonObject) { return jsonObject.getValue(JsonFields.TAGS) .map(array -> array.stream() .filter(JsonValue::isString) @@ -540,7 +536,7 @@ public ConnectionBuilder name(@Nullable final String name) { } @Override - public ConnectionBuilder credentials(@Nullable Credentials credentials) { + public ConnectionBuilder credentials(@Nullable final Credentials credentials) { this.credentials = credentials; return this; } @@ -787,156 +783,4 @@ private boolean containsTargetWithConnectionAnnouncementsTopic() { } - @Immutable - static final class ConnectionUri { - - private static final String MASKED_URI_PATTERN = "{0}://{1}{2}:{3,number,#}{4}"; - - @SuppressWarnings("squid:S2068") // S2068 tripped due to 'PASSWORD' in variable name - private static final String USERNAME_PASSWORD_SEPARATOR = ":"; - - private final String uriString; - private final String protocol; - private final String hostname; - private final int port; - private final String path; - @Nullable private final String userName; - @Nullable private final String password; - private final String uriStringWithMaskedPassword; - - private ConnectionUri(final String theUriString) { - final URI uri; - try { - uri = new URI(theUriString).parseServerAuthority(); - } catch (final URISyntaxException e) { - throw ConnectionUriInvalidException.newBuilder(theUriString).build(); - } - // validate self - if (!isValid(uri)) { - throw ConnectionUriInvalidException.newBuilder(theUriString).build(); - } - - uriString = uri.toASCIIString(); - protocol = uri.getScheme(); - hostname = uri.getHost(); - port = uri.getPort(); - path = uri.getPath(); - - // initialize nullable fields - final String userInfo = uri.getUserInfo(); - if (userInfo != null && userInfo.contains(USERNAME_PASSWORD_SEPARATOR)) { - final int separatorIndex = userInfo.indexOf(USERNAME_PASSWORD_SEPARATOR); - userName = userInfo.substring(0, separatorIndex); - password = userInfo.substring(separatorIndex + 1); - } else { - userName = null; - password = null; - } - - // must be initialized after all else - uriStringWithMaskedPassword = createUriStringWithMaskedPassword(); - } - - private String createUriStringWithMaskedPassword() { - return MessageFormat.format(MASKED_URI_PATTERN, protocol, getUserCredentialsOrEmptyString(), hostname, port, - getPathOrEmptyString()); - } - - private String getUserCredentialsOrEmptyString() { - if (null != userName && null != password) { - return userName + ":*****@"; - } - return ""; - } - - private String getPathOrEmptyString() { - return getPath().orElse(""); - } - - /** - * Test validity of a connection URI. A connection URI is valid if it has an explicit port number ,has no query - * parameters, and has a nonempty password whenever it has a nonempty username. - * - * @param uri the URI object with which the connection URI is created. - * @return whether the connection URI is valid. - */ - private static boolean isValid(final URI uri) { - return uri.getPort() > 0 && uri.getQuery() == null; - } - - /** - * Returns a new instance of {@code ConnectionUri}. The is the reverse function of {@link #toString()}. - * - * @param uriString the string representation of the Connection URI. - * @return the instance. - * @throws NullPointerException if {@code uriString} is {@code null}. - * @throws ConnectionUriInvalidException if {@code uriString} is not a - * valid URI. - * @see #toString() - */ - static ConnectionUri of(final String uriString) { - return new ConnectionUri(uriString); - } - - String getProtocol() { - return protocol; - } - - Optional getUserName() { - return Optional.ofNullable(userName); - } - - Optional getPassword() { - return Optional.ofNullable(password); - } - - String getHostname() { - return hostname; - } - - int getPort() { - return port; - } - - /** - * Returns the path or an empty string. - * - * @return the path or an empty string. - */ - Optional getPath() { - return path.isEmpty() ? Optional.empty() : Optional.of(path); - } - - String getUriStringWithMaskedPassword() { - return uriStringWithMaskedPassword; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final ConnectionUri that = (ConnectionUri) o; - return Objects.equals(uriString, that.uriString); - } - - @Override - public int hashCode() { - return Objects.hash(uriString); - } - - /** - * @return the string representation of this ConnectionUri. This is the reverse function of {@link #of(String)}. - * @see #of(String) - */ - @Override - public String toString() { - return uriString; - } - - } - } diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java new file mode 100644 index 0000000000..bde214c9c9 --- /dev/null +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2017 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class ConnectionUriTest { + + @Test + public void parseUriAsExpected() { + final ConnectionUri underTest = + ConnectionUri.of("amqps://foo:bar@hono.eclipse.org:5671/vhost"); + + assertThat(underTest.getProtocol()).isEqualTo("amqps"); + assertThat(underTest.getUserName()).contains("foo"); + assertThat(underTest.getPassword()).contains("bar"); + assertThat(underTest.getHostname()).isEqualTo("hono.eclipse.org"); + assertThat(underTest.getPort()).isEqualTo(5671); + assertThat(underTest.getPath()).contains("/vhost"); + } + + @Test + public void parsePasswordWithPlusSign() { + final ConnectionUri underTest = + ConnectionUri.of("amqps://foo:bar+baz@hono.eclipse.org:5671/vhost"); + assertThat(underTest.getPassword()).contains("bar+baz"); + } + + @Test + public void parsePasswordWithPlusSignEncoded() { + final ConnectionUri underTest = + ConnectionUri.of("amqps://foo:bar%2Bbaz@hono.eclipse.org:5671/vhost"); + assertThat(underTest.getPassword()).contains("bar+baz"); + } + + @Test + public void parsePasswordWithPlusSignDoubleEncoded() { + final ConnectionUri underTest = + ConnectionUri.of("amqps://foo:bar%252Bbaz@hono.eclipse.org:5671/vhost"); + assertThat(underTest.getPassword()).contains("bar%2Bbaz"); + } + + @Test + public void parseUriWithoutCredentials() { + final ConnectionUri underTest = + ConnectionUri.of("amqps://hono.eclipse.org:5671"); + + assertThat(underTest.getUserName()).isEmpty(); + assertThat(underTest.getPassword()).isEmpty(); + } + + @Test + public void parseUriWithoutPath() { + final ConnectionUri underTest = ConnectionUri.of("amqps://foo:bar@hono.eclipse.org:5671"); + + assertThat(underTest.getPath()).isEmpty(); + } + + @Test(expected = ConnectionUriInvalidException.class) + public void cannotParseUriWithoutPort() { + ConnectionUri.of("amqps://foo:bar@hono.eclipse.org"); + } + + @Test(expected = ConnectionUriInvalidException.class) + public void cannotParseUriWithoutHost() { + ConnectionUri.of("amqps://foo:bar@:5671"); + } + + + /** + * Permit construction of connection URIs with username and without password because RFC-3986 permits it. + */ + @Test + public void canParseUriWithUsernameWithoutPassword() { + final ConnectionUri underTest = + ConnectionUri.of("amqps://foo:@hono.eclipse.org:5671"); + + assertThat(underTest.getUserName()).contains("foo"); + assertThat(underTest.getPassword()).contains(""); + } + + @Test + public void canParseUriWithoutUsernameWithPassword() { + final ConnectionUri underTest = + ConnectionUri.of("amqps://:bar@hono.eclipse.org:5671"); + + assertThat(underTest.getUserName()).contains(""); + assertThat(underTest.getPassword()).contains("bar"); + } + + @Test(expected = ConnectionUriInvalidException.class) + public void uriRegexFailsWithoutProtocol() { + ConnectionUri.of("://foo:bar@hono.eclipse.org:5671"); + } + + @Test + public void testPasswordFromUriWithNullValue() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getPassword()).isEmpty(); + } + + @Test + public void testUserFromUriWithNullValue() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getUserName()).isEmpty(); + } + + @Test + public void testPortFromUriWithNullValue() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getPort()).isEqualTo(9999); + } + + @Test + public void testHostFromUriWithNullValue() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getHostname()).isEmpty(); + } + + @Test + public void testProtocolFromUriWithNullValue() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getPassword()).isEmpty(); + } + + @Test + public void testPathFromUriWithNullValue() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getPath()).isEmpty(); + } + + @Test + public void testUriStringWithMaskedPasswordFromUriWithNullValue() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getUriStringWithMaskedPassword()).isEmpty(); + } + + @Test + public void testPasswordFromUriWithEmptyString() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getPassword()).isEmpty(); + } + + @Test + public void testUserFromUriWithEmptyString() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getUserName()).isEmpty(); + } + + @Test + public void testPortFromUriWithEmptyString() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getPort()).isEqualTo(9999); + } + + @Test + public void testHostFromUriWithEmptyString() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getHostname()).isEmpty(); + } + + @Test + public void testProtocolFromUriWithEmptyString() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getPassword()).isEmpty(); + } + + @Test + public void testPathFromUriWithEmptyString() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getPath()).isEmpty(); + } + + @Test + public void testUriStringWithMaskedPasswordFromUriWithEmptyString() { + final ConnectionUri underTest = + ConnectionUri.of(null); + assertThat(underTest.getUriStringWithMaskedPassword()).isEmpty(); + } + +} diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java new file mode 100644 index 0000000000..305291f49f --- /dev/null +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2017 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mutabilitydetector.unittesting.AllowedReason.assumingFields; +import static org.mutabilitydetector.unittesting.AllowedReason.provided; +import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; +import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.auth.AuthorizationContext; +import org.eclipse.ditto.base.model.auth.AuthorizationSubject; +import org.eclipse.ditto.base.model.auth.DittoAuthorizationContextType; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonCollectors; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonValue; +import org.junit.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +/** + * Unit test for {@link ImmutableConnection}. + */ +public final class HonoConnectionTest { + private static final ConnectionType TYPE = ConnectionType.HONO; + private static final ConnectivityStatus STATUS = ConnectivityStatus.OPEN; + + private static final ConnectionId ID = ConnectionId.of("myHonoConnectionId"); + private static final String NAME = "myHonoConnection"; + + @Nullable private static final String URI = null; + private static final String URI_EMPTY = ""; + private static final Credentials CREDENTIALS = ClientCertificateCredentials.newBuilder().build(); + + private static final AuthorizationContext AUTHORIZATION_CONTEXT = + AuthorizationContext.newInstance(DittoAuthorizationContextType.PRE_AUTHENTICATED_CONNECTION, + AuthorizationSubject.newInstance("myIssuer:mySubject")); + + private static final String STATUS_MAPPING = "ConnectionStatus"; + private static final String JAVA_SCRIPT_MAPPING = "JavaScript"; + private static final String MIGRATED_MAPPER_ID = "javascript"; + + private static final Source SOURCE1 = ConnectivityModelFactory.newSource(AUTHORIZATION_CONTEXT, "amqp/source1"); + private static final Source SOURCE2 = ConnectivityModelFactory.newSource(AUTHORIZATION_CONTEXT, "amqp/source2", 1); + private static final List SOURCES = Arrays.asList(SOURCE1, SOURCE2); + private static final List SOURCES_WITH_REPLY_TARGET_DISABLED = SOURCES.stream() + .map(s -> ConnectivityModelFactory.newSourceBuilder(s).replyTargetEnabled(false).build()) + .collect(Collectors.toList()); + private static final HeaderMapping HEADER_MAPPING = ConnectivityModelFactory.emptyHeaderMapping(); + private static final Target TARGET1 = ConnectivityModelFactory.newTargetBuilder() + .address("amqp/target1") + .authorizationContext(AUTHORIZATION_CONTEXT) + .headerMapping(HEADER_MAPPING) + .topics(Topic.TWIN_EVENTS, Topic.LIVE_EVENTS) + .build(); + private static final Target TARGET2 = ConnectivityModelFactory.newTargetBuilder() + .address("amqp/target2") + .authorizationContext(AUTHORIZATION_CONTEXT) + .headerMapping(HEADER_MAPPING) + .topics(Topic.LIVE_MESSAGES, Topic.LIVE_MESSAGES, Topic.LIVE_EVENTS) + .build(); + private static final Target TARGET3 = ConnectivityModelFactory.newTargetBuilder() + .address("amqp/target3") + .authorizationContext(AUTHORIZATION_CONTEXT) + .headerMapping(HEADER_MAPPING) + .topics(Topic.LIVE_MESSAGES, Topic.LIVE_MESSAGES, Topic.LIVE_COMMANDS) + .build(); + private static final List TARGETS = Arrays.asList(TARGET1, TARGET2, TARGET3); + + private static final JsonArray KNOWN_SOURCES_JSON = + SOURCES.stream().map(Source::toJson).collect(JsonCollectors.valuesToArray()); + private static final JsonArray KNOWN_TARGETS_JSON = + TARGETS.stream().map(Target::toJson).collect(JsonCollectors.valuesToArray()); + + private static final JsonArray KNOWN_SOURCES_WITH_MAPPING_JSON = + KNOWN_SOURCES_JSON.stream() + .map(JsonValue::asObject) + .map(o -> o.set(Source.JsonFields.PAYLOAD_MAPPING, JsonArray.of(JsonValue.of(JAVA_SCRIPT_MAPPING)))) + .collect(JsonCollectors.valuesToArray()); + private static final JsonArray KNOWN_TARGETS_WITH_MAPPING_JSON = + KNOWN_TARGETS_JSON.stream() + .map(JsonValue::asObject) + .map(o -> o.set(Source.JsonFields.PAYLOAD_MAPPING, JsonArray.of(JsonValue.of(STATUS_MAPPING)))) + .collect(JsonCollectors.valuesToArray()); + private static final JsonArray KNOWN_SOURCES_WITH_REPLY_TARGET = + KNOWN_SOURCES_WITH_MAPPING_JSON.stream() + .map(o -> o.asObject().toBuilder() + .set(Source.JsonFields.HEADER_MAPPING.getPointer(), + ImmutableSource.DEFAULT_SOURCE_HEADER_MAPPING.toJson()) + .set(Source.JsonFields.REPLY_TARGET.getPointer(), + ReplyTarget.newBuilder().address(ImmutableSource.DEFAULT_REPLY_TARGET_ADDRESS) + .headerMapping(ImmutableSource.DEFAULT_REPLY_TARGET_HEADER_MAPPING) + .build() + .toJson()) + .set(Source.JsonFields.REPLY_TARGET_ENABLED, true) + .build()) + .collect(JsonCollectors.valuesToArray()); + private static final JsonArray KNOWN_TARGETS_WITH_HEADER_MAPPING = + KNOWN_TARGETS_WITH_MAPPING_JSON.stream() + .map(o -> o.asObject().toBuilder() + .set(Target.JsonFields.HEADER_MAPPING, o.asObject() + .getValue(Target.JsonFields.HEADER_MAPPING) + .orElseGet(ConnectivityModelFactory.emptyHeaderMapping()::toJson)) + .build()) + .collect(JsonCollectors.valuesToArray()); + + private static final MappingContext KNOWN_MAPPING_CONTEXT = ConnectivityModelFactory.newMappingContext( + JAVA_SCRIPT_MAPPING, + Collections.singletonMap("incomingScript", + "function mapToDittoProtocolMsg(\n" + + " headers,\n" + + " textPayload,\n" + + " bytePayload,\n" + + " contentType\n" + + ") {\n" + + "\n" + + " // ###\n" + + " // Insert your mapping logic here\n" + + " let namespace = \"org.eclipse.ditto\";\n" + + " let name = \"foo-bar\";\n" + + " let group = \"things\";\n" + + " let channel = \"twin\";\n" + + " let criterion = \"commands\";\n" + + " let action = \"modify\";\n" + + " let path = \"/attributes/foo\";\n" + + " let dittoHeaders = headers;\n" + + " let value = textPayload;\n" + + " // ###\n" + + "\n" + + " return Ditto.buildDittoProtocolMsg(\n" + + " namespace,\n" + + " name,\n" + + " group,\n" + + " channel,\n" + + " criterion,\n" + + " action,\n" + + " path,\n" + + " dittoHeaders,\n" + + " value\n" + + " );\n" + + "}")); + + private static final MappingContext KNOWN_JAVA_MAPPING_CONTEXT = ConnectivityModelFactory.newMappingContext( + STATUS_MAPPING, new HashMap<>()); + + private static final PayloadMappingDefinition KNOWN_MAPPING_DEFINITIONS = + ConnectivityModelFactory.newPayloadMappingDefinition( + Stream.of(KNOWN_MAPPING_CONTEXT, KNOWN_JAVA_MAPPING_CONTEXT) + .collect(Collectors.toMap(MappingContext::getMappingEngine, ctx -> ctx))); + + private static final PayloadMappingDefinition LEGACY_MAPPINGS = + ConnectivityModelFactory.newPayloadMappingDefinition( + Stream.of(KNOWN_MAPPING_CONTEXT).collect(Collectors.toMap(ctx -> MIGRATED_MAPPER_ID, ctx -> ctx))); + + private static final Set KNOWN_TAGS = Collections.singleton("HONO"); + + private static final JsonObject KNOWN_JSON= JsonObject.newBuilder() + .set(Connection.JsonFields.ID, ID.toString()) + .set(Connection.JsonFields.NAME, NAME) + .set(Connection.JsonFields.CONNECTION_TYPE, TYPE.getName()) + .set(Connection.JsonFields.CONNECTION_STATUS, STATUS.getName()) + .set(Connection.JsonFields.CREDENTIALS, CREDENTIALS.toJson()) + .set(Connection.JsonFields.URI, URI_EMPTY) + .set(Connection.JsonFields.SOURCES, KNOWN_SOURCES_WITH_MAPPING_JSON) + .set(Connection.JsonFields.TARGETS, KNOWN_TARGETS_WITH_MAPPING_JSON) + .set(Connection.JsonFields.CLIENT_COUNT, 2) + .set(Connection.JsonFields.FAILOVER_ENABLED, true) + .set(Connection.JsonFields.VALIDATE_CERTIFICATES, true) + .set(Connection.JsonFields.PROCESSOR_POOL_SIZE, 1) + .set(Connection.JsonFields.MAPPING_DEFINITIONS, + JsonObject.newBuilder() + .set(JAVA_SCRIPT_MAPPING, KNOWN_MAPPING_CONTEXT.toJson()) + .set(STATUS_MAPPING, KNOWN_JAVA_MAPPING_CONTEXT.toJson()) + .build()) + .set(Connection.JsonFields.TAGS, KNOWN_TAGS.stream() + .map(JsonFactory::newValue) + .collect(JsonCollectors.valuesToArray())) + .build(); + + private static final JsonObject KNOWN_JSON_WITH_REPLY_TARGET = KNOWN_JSON + .set(Connection.JsonFields.SOURCES, KNOWN_SOURCES_WITH_REPLY_TARGET) + .set(Connection.JsonFields.TARGETS, KNOWN_TARGETS_WITH_HEADER_MAPPING); + + private static final JsonObject KNOWN_LEGACY_JSON = KNOWN_JSON + .set(Connection.JsonFields.MAPPING_CONTEXT, KNOWN_MAPPING_CONTEXT.toJson()); + + @Test + public void testHashCodeAndEquals() { + EqualsVerifier.forClass(HonoConnection.class) + .usingGetClass() + .verify(); + } + + @Test + public void assertImmutability() { + assertInstancesOf(ImmutableConnection.class, areImmutable(), + provided(AuthorizationContext.class, Source.class, Target.class, + MappingContext.class, Credentials.class, ConnectionId.class, + PayloadMappingDefinition.class, SshTunnel.class).isAlsoImmutable(), + assumingFields("mappings").areSafelyCopiedUnmodifiableCollectionsWithImmutableElements()); + } + + @Test + public void createMinimalConnectionConfigurationInstance() { + final Connection connection = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + .sources(SOURCES_WITH_REPLY_TARGET_DISABLED) + .targets(TARGETS) + .build(); + assertThat((CharSequence) connection.getId()).isEqualTo(ID); + assertThat((Object) connection.getConnectionType()).isEqualTo(TYPE); + assertThat(connection.getUri()).isEqualTo(URI_EMPTY); + assertThat(connection.getSources()).isEqualTo(SOURCES_WITH_REPLY_TARGET_DISABLED); + } + + @Test + public void createMinimalConnectionConfigurationInstanceWithEmptyUri() { + final Connection connection = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + .sources(SOURCES_WITH_REPLY_TARGET_DISABLED) + .targets(TARGETS) + .build(); + assertThat((CharSequence) connection.getId()).isEqualTo(ID); + assertThat((Object) connection.getConnectionType()).isEqualTo(TYPE); + assertThat(connection.getUri()).isEqualTo(URI_EMPTY); + assertThat(connection.getSources()).isEqualTo(SOURCES_WITH_REPLY_TARGET_DISABLED); + } + + @Test + public void createInstanceWithNullId() { + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> ConnectivityModelFactory.newConnectionBuilder(null, TYPE, STATUS, URI)) + .withMessage("The %s must not be null!", "id") + .withNoCause(); + } + + @Test + public void createInstanceWithNullIdAndEmptyUri() { + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> ConnectivityModelFactory.newConnectionBuilder(null, TYPE, STATUS, URI_EMPTY)) + .withMessage("The %s must not be null!", "id") + .withNoCause(); + } + + @Test + public void createInstanceWithNullUri() { + final Connection connection = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + .sources(SOURCES_WITH_REPLY_TARGET_DISABLED) + .targets(TARGETS) + .build(); + assertThat(connection.getUri()).isEmpty(); + } + + @Test + public void createInstanceWithEmptyUri() { + final Connection connection = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI_EMPTY) + .sources(SOURCES_WITH_REPLY_TARGET_DISABLED) + .targets(TARGETS) + .build(); + assertThat(connection.getUri()).isEmpty(); + } + + @Test + public void getBuilderFromConnectionCoversAllFields() { + + final Connection connection = HonoConnection.getBuilder(ID, TYPE, STATUS, URI) + .sources(SOURCES) + .targets(TARGETS) + .connectionStatus(ConnectivityStatus.OPEN) + .name("connection") + .clientCount(5) + .tag("AAA") + .trustedCertificates("certs") + .processorPoolSize(8) + .credentials(ClientCertificateCredentials.newBuilder() + .clientKey("clientkey") + .clientCertificate("certificate") + .build()) + .validateCertificate(true) + .uri(null) + .id(ID) + .payloadMappingDefinition( + ConnectivityModelFactory.newPayloadMappingDefinition("test", KNOWN_JAVA_MAPPING_CONTEXT)) + .build(); + assertThat(HonoConnection.getBuilder(connection).build()).isEqualTo(connection); + assertThat(HonoConnection.getBuilder(connection).build().getUri()).isEqualTo(URI_EMPTY); + } + + @Test + public void createInstanceWithNullSources() { + final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> builder.sources(null)) + .withMessage("The %s must not be null!", "sources") + .withNoCause(); + } + + @Test + public void createInstanceWithNullEventTarget() { + final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> builder.targets(null)) + .withMessage("The %s must not be null!", "targets") + .withNoCause(); + } + + @Test + public void createInstanceWithNullSourcesAndEmptyUri() { + final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI_EMPTY); + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> builder.sources(null)) + .withMessage("The %s must not be null!", "sources") + .withNoCause(); + } + + @Test + public void createInstanceWithNullEventTargetAndEmptyUri() { + final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI_EMPTY); + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> builder.targets(null)) + .withMessage("The %s must not be null!", "targets") + .withNoCause(); + } + + @Test + public void createInstanceWithConnectionAnnouncementsAndClientCountGreater1() { + final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI) + .targets(Collections.singletonList( + ConnectivityModelFactory.newTargetBuilder(TARGET1) + .topics(Topic.CONNECTION_ANNOUNCEMENTS).build()) + ) + .clientCount(2); + + assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) + .isThrownBy(builder::build) + .withMessageContaining(Topic.CONNECTION_ANNOUNCEMENTS.getName()) + .withNoCause(); + } + + @Test + public void fromJsonWithLegacyMappingContextReturnsExpected() { + + final Map definitions = new HashMap<>(KNOWN_MAPPING_DEFINITIONS.getDefinitions()); + definitions.putAll(LEGACY_MAPPINGS.getDefinitions()); + final Connection expected = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + .credentials(CREDENTIALS) + .name(NAME) + .setSources(addSourceMapping(SOURCES, JAVA_SCRIPT_MAPPING, "javascript")) + .setTargets(addTargetMapping(TARGETS, STATUS_MAPPING, "javascript")) + .clientCount(2) + .payloadMappingDefinition(ConnectivityModelFactory.newPayloadMappingDefinition(definitions)) + .tags(KNOWN_TAGS) + .build(); + + final Connection actual = HonoConnection.fromJson(KNOWN_LEGACY_JSON); + assertThat(actual).isEqualTo(expected); + } + + @Test + public void fromInvalidJsonFails() { + final JsonObject INVALID_JSON = KNOWN_JSON.remove(Connection.JsonFields.SOURCES.getPointer()) + .remove(Connection.JsonFields.TARGETS.getPointer()); + + assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) + .isThrownBy(() -> HonoConnection.fromJson(INVALID_JSON)) + .withMessageContaining("source") + .withMessageContaining("target") + .withNoCause(); + } + + @Test + public void fromJsonReturnsExpected() { + final Connection expected = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + .credentials(CREDENTIALS) + .name(NAME) + .setSources(addSourceMapping(SOURCES, JAVA_SCRIPT_MAPPING)) + .setTargets(addTargetMapping(TARGETS, STATUS_MAPPING)) + .clientCount(2) + .payloadMappingDefinition(KNOWN_MAPPING_DEFINITIONS) + .tags(KNOWN_TAGS) + .build(); + + final Connection actual = HonoConnection.fromJson(KNOWN_JSON); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void toJsonReturnsExpected() { + final Connection underTest = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + .credentials(CREDENTIALS) + .name(NAME) + .sources(addSourceMapping(Arrays.asList(SOURCE2, SOURCE1), + JAVA_SCRIPT_MAPPING)) // use different order to test sorting + .targets(addTargetMapping(TARGETS, STATUS_MAPPING)) + .clientCount(2) + .payloadMappingDefinition(KNOWN_MAPPING_DEFINITIONS) + .tags(KNOWN_TAGS) + .build(); + + final JsonObject actual = underTest.toJson(); + System.out.println(underTest.getUri()); + assertThat(actual).isEqualTo(KNOWN_JSON_WITH_REPLY_TARGET); + } + + @Test + public void emptyCertificatesLeadToEmptyOptional() { + final Connection underTest = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + .targets(TARGETS) + .validateCertificate(true) + .trustedCertificates("") + .build(); + + assertThat(underTest.getTrustedCertificates()).isEmpty(); + } + + @Test + public void emptyCertificatesFromJsonLeadToEmptyOptional() { + final JsonObject connectionJsonWithEmptyCa = KNOWN_JSON + .set(Connection.JsonFields.VALIDATE_CERTIFICATES, true) + .set(Connection.JsonFields.TRUSTED_CERTIFICATES, ""); + final Connection underTest = ConnectivityModelFactory.connectionFromJson(connectionJsonWithEmptyCa); + + assertThat(underTest.getTrustedCertificates()).isEmpty(); + } + + private List addSourceMapping(final List sources, final String... mapping) { + return sources.stream() + .map(s -> new ImmutableSource.Builder(s).payloadMapping( + ConnectivityModelFactory.newPayloadMapping(mapping)).build()) + .collect(Collectors.toList()); + } + + private List addTargetMapping(final List targets, final String... mapping) { + return targets.stream() + .map(t -> new ImmutableTarget.Builder(t).payloadMapping( + ConnectivityModelFactory.newPayloadMapping(mapping)).build()) + .collect(Collectors.toList()); + } + + @Test + public void providesDefaultHeaderMappings() { + + final Target targetWithoutHeaderMapping = ConnectivityModelFactory.newTargetBuilder() + .address("amqp/target1") + .authorizationContext(AUTHORIZATION_CONTEXT) + .topics(Topic.TWIN_EVENTS) + .build(); + final Connection connectionWithoutHeaderMappingForTarget = + ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + .targets(Collections.singletonList(targetWithoutHeaderMapping)) + .build(); + + connectionWithoutHeaderMappingForTarget.getTargets() + .forEach(target -> assertThat(target.getHeaderMapping()) + .isEqualTo(ConnectivityModelFactory.emptyHeaderMapping())); + } + + @Test + public void providesDefaultHeaderMappingsFromJson() { + final JsonObject connectionJsonWithoutHeaderMappingForTarget = KNOWN_JSON + .set(Connection.JsonFields.TARGETS, JsonArray.of( + TARGET1.toJson() + .remove(Target.JsonFields.HEADER_MAPPING.getPointer()))); + final Connection connectionWithoutHeaderMappingForTarget = + HonoConnection.fromJson(connectionJsonWithoutHeaderMappingForTarget); + + connectionWithoutHeaderMappingForTarget.getTargets() + .forEach(target -> assertThat(target.getHeaderMapping()) + .isEqualTo(ConnectivityModelFactory.emptyHeaderMapping())); + } + +} diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ImmutableConnectionTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ImmutableConnectionTest.java index 8114345e57..332b21c195 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ImmutableConnectionTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ImmutableConnectionTest.java @@ -439,107 +439,6 @@ public void nullSshTunnelLeadToEmptyOptional() { assertThat(underTest.getSshTunnel()).isEmpty(); } - @Test - public void parseUriAsExpected() { - final ImmutableConnection.ConnectionUri underTest = - ImmutableConnection.ConnectionUri.of("amqps://foo:bar@hono.eclipse.org:5671/vhost"); - - assertThat(underTest.getProtocol()).isEqualTo("amqps"); - assertThat(underTest.getUserName()).contains("foo"); - assertThat(underTest.getPassword()).contains("bar"); - assertThat(underTest.getHostname()).isEqualTo("hono.eclipse.org"); - assertThat(underTest.getPort()).isEqualTo(5671); - assertThat(underTest.getPath()).contains("/vhost"); - } - - @Test - public void parsePasswordWithPlusSign() { - final ImmutableConnection.ConnectionUri underTest = - ImmutableConnection.ConnectionUri.of("amqps://foo:bar+baz@hono.eclipse.org:5671/vhost"); - assertThat(underTest.getPassword()).contains("bar+baz"); - } - - @Test - public void parsePasswordWithPlusSignEncoded() { - final ImmutableConnection.ConnectionUri underTest = - ImmutableConnection.ConnectionUri.of("amqps://foo:bar%2Bbaz@hono.eclipse.org:5671/vhost"); - assertThat(underTest.getPassword()).contains("bar+baz"); - } - - @Test - public void parsePasswordWithPlusSignDoubleEncoded() { - final ImmutableConnection.ConnectionUri underTest = - ImmutableConnection.ConnectionUri.of("amqps://foo:bar%252Bbaz@hono.eclipse.org:5671/vhost"); - assertThat(underTest.getPassword()).contains("bar%2Bbaz"); - } - - @Test - public void parseUriWithoutCredentials() { - final ImmutableConnection.ConnectionUri underTest = - ImmutableConnection.ConnectionUri.of("amqps://hono.eclipse.org:5671"); - - assertThat(underTest.getUserName()).isEmpty(); - assertThat(underTest.getPassword()).isEmpty(); - } - - @Test - public void parseUriWithoutPath() { - final ImmutableConnection.ConnectionUri underTest = - ImmutableConnection.ConnectionUri.of("amqps://foo:bar@hono.eclipse.org:5671"); - - assertThat(underTest.getPath()).isEmpty(); - } - - @Test(expected = ConnectionUriInvalidException.class) - public void cannotParseUriWithoutPort() { - ImmutableConnection.ConnectionUri.of("amqps://foo:bar@hono.eclipse.org"); - } - - @Test(expected = ConnectionUriInvalidException.class) - public void cannotParseUriWithoutHost() { - ImmutableConnection.ConnectionUri.of("amqps://foo:bar@:5671"); - } - - - /** - * Permit construction of connection URIs with username and without password because RFC-3986 permits it. - */ - @Test - public void canParseUriWithUsernameWithoutPassword() { - final ImmutableConnection.ConnectionUri underTest = - ImmutableConnection.ConnectionUri.of("amqps://foo:@hono.eclipse.org:5671"); - - assertThat(underTest.getUserName()).contains("foo"); - assertThat(underTest.getPassword()).contains(""); - } - - @Test - public void canParseUriWithoutUsernameWithPassword() { - final ImmutableConnection.ConnectionUri underTest = - ImmutableConnection.ConnectionUri.of("amqps://:bar@hono.eclipse.org:5671"); - - assertThat(underTest.getUserName()).contains(""); - assertThat(underTest.getPassword()).contains("bar"); - } - - @Test(expected = ConnectionUriInvalidException.class) - public void uriRegexFailsWithoutProtocol() { - ImmutableConnection.ConnectionUri.of("://foo:bar@hono.eclipse.org:5671"); - } - - @Test - public void toStringDoesNotContainPassword() { - final String password = "thePassword"; - - final String uri = "amqps://foo:" + password + "@host.com:5671"; - - final Connection connection = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, uri) - .sources(Collections.singletonList(SOURCE1)) - .build(); - - assertThat(connection.toString()).doesNotContain(password); - } - @Test public void providesDefaultHeaderMappings() { From 83ca606e614cec7a3779ed8bbf7ec92633f891e2 Mon Sep 17 00:00:00 2001 From: "Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM)" Date: Thu, 24 Nov 2022 11:23:35 +0200 Subject: [PATCH 57/65] Introduce AbstractConnection and AbstractConnectionBuilder Signed-off-by: Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM) --- .../model/AbstractConnection.java | 427 +++++++++++ .../model/AbstractConnectionBuilder.java | 325 ++++++++ .../connectivity/model/ConnectionUri.java | 3 +- .../model/ConnectivityModelFactory.java | 15 +- .../connectivity/model/HonoConnection.java | 674 ++--------------- .../model/ImmutableConnection.java | 701 +----------------- .../connectivity/model/ConnectionUriTest.java | 45 +- .../model/HonoConnectionTest.java | 89 +-- .../model/ImmutableConnectionTest.java | 4 +- 9 files changed, 877 insertions(+), 1406 deletions(-) create mode 100644 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java create mode 100644 connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java new file mode 100644 index 0000000000..b967ee4308 --- /dev/null +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2017 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model; + +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.annotation.Nullable; + +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonCollectors; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonField; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonObjectBuilder; +import org.eclipse.ditto.json.JsonParseException; +import org.eclipse.ditto.json.JsonValue; + +abstract class AbstractConnection implements Connection { + + private final ConnectionId id; + @Nullable private final String name; + private final ConnectionType connectionType; + private final ConnectivityStatus connectionStatus; + final ConnectionUri uri; + @Nullable private final Credentials credentials; + @Nullable private final String trustedCertificates; + @Nullable private final ConnectionLifecycle lifecycle; + private final List sources; + private final List targets; + private final int clientCount; + private final boolean failOverEnabled; + private final boolean validateCertificate; + private final int processorPoolSize; + private final Map specificConfig; + private final PayloadMappingDefinition payloadMappingDefinition; + private final Set tags; + @Nullable private final SshTunnel sshTunnel; + + AbstractConnection(final AbstractConnectionBuilder builder) { + id = checkNotNull(builder.id, "id"); + name = builder.name; + connectionType = builder.connectionType; + connectionStatus = checkNotNull(builder.connectionStatus, "connectionStatus"); + credentials = builder.credentials; + trustedCertificates = builder.trustedCertificates; + uri = getConnectionUri(builder.uri); + sources = Collections.unmodifiableList(new ArrayList<>(builder.sources)); + targets = Collections.unmodifiableList(new ArrayList<>(builder.targets)); + clientCount = builder.clientCount; + failOverEnabled = builder.failOverEnabled; + validateCertificate = builder.validateCertificate; + processorPoolSize = builder.processorPoolSize; + specificConfig = Collections.unmodifiableMap(new HashMap<>(builder.specificConfig)); + payloadMappingDefinition = builder.payloadMappingDefinition; + tags = Collections.unmodifiableSet(new LinkedHashSet<>(builder.tags)); + lifecycle = builder.lifecycle; + sshTunnel = builder.sshTunnel; + } + + abstract ConnectionUri getConnectionUri(@Nullable String builderConnectionUri); + + static void fromJson(final JsonObject jsonObject, final AbstractConnectionBuilder builder) { + final MappingContext mappingContext = jsonObject.getValue(JsonFields.MAPPING_CONTEXT) + .map(ConnectivityModelFactory::mappingContextFromJson) + .orElse(null); + + final PayloadMappingDefinition payloadMappingDefinition = + jsonObject.getValue(JsonFields.MAPPING_DEFINITIONS) + .map(ImmutablePayloadMappingDefinition::fromJson) + .orElse(ConnectivityModelFactory.emptyPayloadMappingDefinition()); + builder.id(ConnectionId.of(jsonObject.getValueOrThrow(JsonFields.ID))) + .connectionStatus(getConnectionStatusOrThrow(jsonObject)) + .uri(jsonObject.getValueOrThrow(JsonFields.URI)) + .sources(getSources(jsonObject)) + .targets(getTargets(jsonObject)) + .name(jsonObject.getValue(JsonFields.NAME).orElse(null)) + .mappingContext(mappingContext) + .payloadMappingDefinition(payloadMappingDefinition) + .specificConfig(getSpecificConfiguration(jsonObject)) + .tags(getTags(jsonObject)); + + jsonObject.getValue(JsonFields.LIFECYCLE) + .flatMap(ConnectionLifecycle::forName).ifPresent(builder::lifecycle); + jsonObject.getValue(JsonFields.CREDENTIALS).ifPresent(builder::credentialsFromJson); + jsonObject.getValue(JsonFields.CLIENT_COUNT).ifPresent(builder::clientCount); + jsonObject.getValue(JsonFields.FAILOVER_ENABLED).ifPresent(builder::failoverEnabled); + jsonObject.getValue(JsonFields.VALIDATE_CERTIFICATES).ifPresent(builder::validateCertificate); + jsonObject.getValue(JsonFields.PROCESSOR_POOL_SIZE).ifPresent(builder::processorPoolSize); + jsonObject.getValue(JsonFields.TRUSTED_CERTIFICATES).ifPresent(builder::trustedCertificates); + jsonObject.getValue(JsonFields.SSH_TUNNEL) + .ifPresent(jsonFields -> builder.sshTunnel(ImmutableSshTunnel.fromJson(jsonFields))); + } + + static ConnectivityStatus getConnectionStatusOrThrow(final JsonObject jsonObject) { + final String readConnectionStatus = jsonObject.getValueOrThrow(JsonFields.CONNECTION_STATUS); + return ConnectivityStatus.forName(readConnectionStatus) + .orElseThrow(() -> JsonParseException.newBuilder() + .message(MessageFormat.format("Connection status <{0}> is invalid!", readConnectionStatus)) + .build()); + } + + private static List getSources(final JsonObject jsonObject) { + final Optional sourcesArray = jsonObject.getValue(JsonFields.SOURCES); + if (sourcesArray.isPresent()) { + final JsonArray values = sourcesArray.get(); + return IntStream.range(0, values.getSize()) + .mapToObj(index -> values.get(index) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .map(valueAsObject -> ConnectivityModelFactory.sourceFromJson(valueAsObject, index))) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } + + private static List getTargets(final JsonObject jsonObject) { + return jsonObject.getValue(JsonFields.TARGETS) + .map(array -> array.stream() + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .map(ConnectivityModelFactory::targetFromJson) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()); + } + + private static Map getSpecificConfiguration(final JsonObject jsonObject) { + return jsonObject.getValue(JsonFields.SPECIFIC_CONFIG) + .filter(JsonValue::isObject) + .map(JsonValue::asObject) + .map(JsonObject::stream) + .map(jsonFields -> jsonFields.collect(Collectors.toMap(JsonField::getKeyName, + f -> f.getValue().isString() ? f.getValue().asString() : f.getValue().toString()))) + .orElse(Collections.emptyMap()); + } + + private static Set getTags(final JsonObject jsonObject) { + return jsonObject.getValue(JsonFields.TAGS) + .map(array -> array.stream() + .filter(JsonValue::isString) + .map(JsonValue::asString) + .collect(Collectors.toCollection(LinkedHashSet::new))) + .orElseGet(LinkedHashSet::new); + } + + @Override + public ConnectionId getId() { + return id; + } + + @Override + public Optional getName() { + return Optional.ofNullable(name); + } + + @Override + public ConnectionType getConnectionType() { + return connectionType; + } + + @Override + public ConnectivityStatus getConnectionStatus() { + return connectionStatus; + } + + @Override + public List getSources() { + return sources; + } + + @Override + public List getTargets() { + return targets; + } + + @Override + public Optional getSshTunnel() { + return Optional.ofNullable(sshTunnel); + } + + @Override + public int getClientCount() { + return clientCount; + } + + @Override + public boolean isFailoverEnabled() { + return failOverEnabled; + } + + @Override + public Optional getCredentials() { + return Optional.ofNullable(credentials); + } + + @Override + public Optional getTrustedCertificates() { + return Optional.ofNullable(trustedCertificates); + } + + @Override + public String getUri() { + return uri.toString(); + } + + @Override + public String getProtocol() { + return uri.getProtocol(); + } + + @Override + public Optional getUsername() { + return uri.getUserName(); + } + + @Override + public Optional getPassword() { + return uri.getPassword(); + } + + @Override + public String getHostname() { + return uri.getHostname(); + } + + @Override + public int getPort() { + return uri.getPort(); + } + + @Override + public Optional getPath() { + return uri.getPath(); + } + + @Override + public boolean isValidateCertificates() { + return validateCertificate; + } + + @Override + public int getProcessorPoolSize() { + return processorPoolSize; + } + + @Override + public Map getSpecificConfig() { + return specificConfig; + } + + @Override + public PayloadMappingDefinition getPayloadMappingDefinition() { + return payloadMappingDefinition; + } + + @Override + public Set getTags() { + return tags; + } + + @Override + public Optional getLifecycle() { + return Optional.ofNullable(lifecycle); + } + + static ConnectionBuilder fromConnection(final Connection connection, final AbstractConnectionBuilder builder) { + checkNotNull(connection, "Connection"); + + return builder + .id(connection.getId()) + .connectionStatus(connection.getConnectionStatus()) + .credentials(connection.getCredentials().orElse(null)) + .uri(connection.getUri()) + .trustedCertificates(connection.getTrustedCertificates().orElse(null)) + .failoverEnabled(connection.isFailoverEnabled()) + .validateCertificate(connection.isValidateCertificates()) + .processorPoolSize(connection.getProcessorPoolSize()) + .sources(connection.getSources()) + .targets(connection.getTargets()) + .clientCount(connection.getClientCount()) + .specificConfig(connection.getSpecificConfig()) + .payloadMappingDefinition(connection.getPayloadMappingDefinition()) + .name(connection.getName().orElse(null)) + .sshTunnel(connection.getSshTunnel().orElse(null)) + .tags(connection.getTags()) + .lifecycle(connection.getLifecycle().orElse(null)); + } + + @Override + public JsonObject toJson(final JsonSchemaVersion schemaVersion, final Predicate thePredicate) { + final Predicate predicate = schemaVersion.and(thePredicate); + final JsonObjectBuilder jsonObjectBuilder = JsonFactory.newObjectBuilder(); + + if (null != lifecycle) { + jsonObjectBuilder.set(JsonFields.LIFECYCLE, lifecycle.name(), predicate); + } + jsonObjectBuilder.set(JsonFields.ID, String.valueOf(id), predicate); + jsonObjectBuilder.set(JsonFields.NAME, name, predicate); + jsonObjectBuilder.set(JsonFields.CONNECTION_TYPE, connectionType.getName(), predicate); + jsonObjectBuilder.set(JsonFields.CONNECTION_STATUS, connectionStatus.getName(), predicate); + jsonObjectBuilder.set(JsonFields.URI, uri.toString(), predicate); + jsonObjectBuilder.set(JsonFields.SOURCES, sources.stream() + .sorted(Comparator.comparingInt(Source::getIndex)) + .map(source -> source.toJson(schemaVersion, thePredicate)) + .collect(JsonCollectors.valuesToArray()), predicate.and(Objects::nonNull)); + jsonObjectBuilder.set(JsonFields.TARGETS, targets.stream() + .map(target -> target.toJson(schemaVersion, thePredicate)) + .collect(JsonCollectors.valuesToArray()), predicate.and(Objects::nonNull)); + jsonObjectBuilder.set(JsonFields.CLIENT_COUNT, clientCount, predicate); + jsonObjectBuilder.set(JsonFields.FAILOVER_ENABLED, failOverEnabled, predicate); + jsonObjectBuilder.set(JsonFields.VALIDATE_CERTIFICATES, validateCertificate, predicate); + jsonObjectBuilder.set(JsonFields.PROCESSOR_POOL_SIZE, processorPoolSize, predicate); + if (!specificConfig.isEmpty()) { + jsonObjectBuilder.set(JsonFields.SPECIFIC_CONFIG, specificConfig.entrySet() + .stream() + .map(entry -> JsonField.newInstance(entry.getKey(), JsonValue.of(entry.getValue()))) + .collect(JsonCollectors.fieldsToObject()), predicate); + } + if (!payloadMappingDefinition.isEmpty()) { + jsonObjectBuilder.set(JsonFields.MAPPING_DEFINITIONS, + payloadMappingDefinition.toJson(schemaVersion, thePredicate)); + } + if (credentials != null) { + jsonObjectBuilder.set(JsonFields.CREDENTIALS, credentials.toJson()); + } + if (trustedCertificates != null) { + jsonObjectBuilder.set(JsonFields.TRUSTED_CERTIFICATES, trustedCertificates, predicate); + } + if (sshTunnel != null) { + jsonObjectBuilder.set(JsonFields.SSH_TUNNEL, sshTunnel.toJson(predicate), predicate); + } + jsonObjectBuilder.set(JsonFields.TAGS, tags.stream() + .map(JsonFactory::newValue) + .collect(JsonCollectors.valuesToArray()), predicate); + return jsonObjectBuilder.build(); + } + + @SuppressWarnings("OverlyComplexMethod") + @Override + public boolean equals(@Nullable final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final AbstractConnection that = (AbstractConnection) o; + return failOverEnabled == that.failOverEnabled && + Objects.equals(id, that.id) && + Objects.equals(name, that.name) && + Objects.equals(connectionType, that.connectionType) && + Objects.equals(connectionStatus, that.connectionStatus) && + Objects.equals(sources, that.sources) && + Objects.equals(targets, that.targets) && + Objects.equals(clientCount, that.clientCount) && + Objects.equals(credentials, that.credentials) && + Objects.equals(trustedCertificates, that.trustedCertificates) && + Objects.equals(uri, that.uri) && + Objects.equals(processorPoolSize, that.processorPoolSize) && + Objects.equals(validateCertificate, that.validateCertificate) && + Objects.equals(specificConfig, that.specificConfig) && + Objects.equals(payloadMappingDefinition, that.payloadMappingDefinition) && + Objects.equals(lifecycle, that.lifecycle) && + Objects.equals(sshTunnel, that.sshTunnel) && + Objects.equals(tags, that.tags); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, connectionType, connectionStatus, sources, targets, clientCount, failOverEnabled, + credentials, trustedCertificates, uri, validateCertificate, processorPoolSize, specificConfig, + payloadMappingDefinition, sshTunnel, tags, lifecycle); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + + "id=" + id + + ", name=" + name + + ", connectionType=" + connectionType + + ", connectionStatus=" + connectionStatus + + ", failoverEnabled=" + failOverEnabled + + ", credentials=" + credentials + + ", trustedCertificates=hash:" + Objects.hash(trustedCertificates) + + ", uri=" + uri.getUriStringWithMaskedPassword() + + ", sources=" + sources + + ", targets=" + targets + + ", sshTunnel=" + sshTunnel + + ", clientCount=" + clientCount + + ", validateCertificate=" + validateCertificate + + ", processorPoolSize=" + processorPoolSize + + ", specificConfig=" + specificConfig + + ", payloadMappingDefinition=" + payloadMappingDefinition + + ", tags=" + tags + + ", lifecycle=" + lifecycle + + "]"; + } + +} \ No newline at end of file diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java new file mode 100644 index 0000000000..8ae948504d --- /dev/null +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2017 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.connectivity.model; + +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkArgument; +import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Abstract implementation for common aspects of + * {@link org.eclipse.ditto.connectivity.model.ConnectionBuilder}. + * + */ + +@Immutable +abstract class AbstractConnectionBuilder implements ConnectionBuilder { + + private static final String MIGRATED_MAPPER_ID = "javascript"; + + // required but changeable: + @Nullable ConnectionId id; + @Nullable ConnectivityStatus connectionStatus; + String uri; + // optional: + @Nullable String name = null; + @Nullable Credentials credentials; + @Nullable MappingContext mappingContext = null; + @Nullable String trustedCertificates; + @Nullable ConnectionLifecycle lifecycle = null; + @Nullable SshTunnel sshTunnel = null; + + // optional with default: + Set tags = new LinkedHashSet<>(); + boolean failOverEnabled = true; + boolean validateCertificate = true; + final List sources = new ArrayList<>(); + final List targets = new ArrayList<>(); + int clientCount = 1; + int processorPoolSize = 1; + PayloadMappingDefinition payloadMappingDefinition = + ConnectivityModelFactory.emptyPayloadMappingDefinition(); + final Map specificConfig = new HashMap<>(); + ConnectionType connectionType; + + AbstractConnectionBuilder(final ConnectionType connectionType) { + this.connectionType = connectionType; + } + + private static boolean isBlankOrNull(@Nullable final String toTest) { + return null == toTest || toTest.trim().isEmpty(); + } + + @Override + public ConnectionBuilder id(final ConnectionId id) { + this.id = checkNotNull(id, "id"); + return this; + } + + @Override + public ConnectionBuilder name(@Nullable final String name) { + this.name = name; + return this; + } + + @Override + public ConnectionBuilder credentials(@Nullable final Credentials credentials) { + this.credentials = credentials; + return this; + } + + @Override + public ConnectionBuilder trustedCertificates(@Nullable final String trustedCertificates) { + if (isBlankOrNull(trustedCertificates)) { + this.trustedCertificates = null; + } else { + this.trustedCertificates = trustedCertificates; + } + return this; + } + + @Override + public ConnectionBuilder uri(final String uri) { + this.uri = checkNotNull(uri, "uri"); + return this; + } + + @Override + public ConnectionBuilder connectionStatus(final ConnectivityStatus connectionStatus) { + this.connectionStatus = checkNotNull(connectionStatus, "connectionStatus"); + return this; + } + + @Override + public ConnectionBuilder failoverEnabled(final boolean failOverEnabled) { + this.failOverEnabled = failOverEnabled; + return this; + } + + @Override + public ConnectionBuilder validateCertificate(final boolean validateCertificate) { + this.validateCertificate = validateCertificate; + return this; + } + + @Override + public ConnectionBuilder processorPoolSize(final int processorPoolSize) { + checkArgument(processorPoolSize, ps -> ps > 0, () -> "The processor pool size must be positive!"); + this.processorPoolSize = processorPoolSize; + return this; + } + + @Override + public ConnectionBuilder sources(final List sources) { + this.sources.addAll(checkNotNull(sources, "sources")); + return this; + } + + @Override + public ConnectionBuilder targets(final List targets) { + this.targets.addAll(checkNotNull(targets, "targets")); + return this; + } + + @Override + public ConnectionBuilder setSources(final List sources) { + this.sources.clear(); + return sources(sources); + } + + @Override + public ConnectionBuilder setTargets(final List targets) { + this.targets.clear(); + return targets(targets); + } + + @Override + public ConnectionBuilder clientCount(final int clientCount) { + checkArgument(clientCount, ps -> ps > 0, () -> "The client count must be positive!"); + this.clientCount = clientCount; + return this; + } + + @Override + public ConnectionBuilder specificConfig(final Map specificConfig) { + this.specificConfig.putAll(checkNotNull(specificConfig, "specificConfig")); + return this; + } + + @Override + public ConnectionBuilder mappingContext(@Nullable final MappingContext mappingContext) { + this.mappingContext = mappingContext; + return this; + } + + @Override + public ConnectionBuilder tags(final Collection tags) { + this.tags = new LinkedHashSet<>(checkNotNull(tags, "tags to set")); + return this; + } + + @Override + public ConnectionBuilder tag(final String tag) { + tags.add(checkNotNull(tag, "tag to set")); + return this; + } + + @Override + public ConnectionBuilder lifecycle(@Nullable final ConnectionLifecycle lifecycle) { + this.lifecycle = lifecycle; + return this; + } + + @Override + public ConnectionBuilder sshTunnel(@Nullable final SshTunnel sshTunnel) { + this.sshTunnel = sshTunnel; + return this; + } + + @Override + public ConnectionBuilder payloadMappingDefinition(final PayloadMappingDefinition payloadMappingDefinition) { + this.payloadMappingDefinition = payloadMappingDefinition; + return this; + } + + private boolean shouldMigrateMappingContext() { + return mappingContext != null; + } + + void migrateLegacyConfigurationOnTheFly() { + if (shouldMigrateMappingContext()) { + this.payloadMappingDefinition = + payloadMappingDefinition.withDefinition(MIGRATED_MAPPER_ID, mappingContext); + } + setSources(sources.stream().map(this::migrateSource).collect(Collectors.toList())); + setTargets(targets.stream().map(this::migrateTarget).collect(Collectors.toList())); + } + + private Source migrateSource(final Source source) { + final Source sourceAfterReplyTargetMigration = ImmutableSource.migrateReplyTarget(source, connectionType); + if (shouldMigrateMappingContext()) { + return new ImmutableSource.Builder(sourceAfterReplyTargetMigration) + .payloadMapping(addMigratedPayloadMappings(source.getPayloadMapping())) + .build(); + } else { + return sourceAfterReplyTargetMigration; + } + } + + private Target migrateTarget(final Target target) { + final boolean shouldAddHeaderMapping = shouldAddDefaultHeaderMappingToTarget(connectionType); + final boolean shouldMigrateMappingContext = shouldMigrateMappingContext(); + if (shouldMigrateMappingContext || shouldAddHeaderMapping) { + final TargetBuilder builder = new ImmutableTarget.Builder(target); + if (shouldMigrateMappingContext) { + builder.payloadMapping(addMigratedPayloadMappings(target.getPayloadMapping())); + } + if (shouldAddHeaderMapping) { + builder.headerMapping(target.getHeaderMapping()); + } + return builder.build(); + } else { + return target; + } + } + + private boolean shouldAddDefaultHeaderMappingToTarget(final ConnectionType connectionType) { + switch (connectionType) { + case AMQP_091: + case AMQP_10: + case KAFKA: + case MQTT_5: + case HONO: + return true; + case MQTT: + case HTTP_PUSH: + default: + return false; + } + } + + + private PayloadMapping addMigratedPayloadMappings(final PayloadMapping payloadMapping) { + final ArrayList merged = new ArrayList<>(payloadMapping.getMappings()); + merged.add(MIGRATED_MAPPER_ID); + return ConnectivityModelFactory.newPayloadMapping(merged); + } + + void checkSourceAndTargetAreValid() { + if (sources.isEmpty() && targets.isEmpty()) { + throw ConnectionConfigurationInvalidException.newBuilder("Either a source or a target must be " + + "specified in the configuration of a connection!").build(); + } + } + + /** + * If no context is set on connection level each target and source must have its own context. + */ + void checkAuthorizationContextsAreValid() { + // if the auth context on connection level is empty, + // an auth context is required to be set on each source/target + final Set sourcesWithoutAuthContext = sources.stream() + .filter(source -> source.getAuthorizationContext().isEmpty()) + .flatMap(source -> source.getAddresses().stream()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + final Set targetsWithoutAuthContext = targets.stream() + .filter(target -> target.getAuthorizationContext().isEmpty()) + .map(Target::getAddress) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + if (!sourcesWithoutAuthContext.isEmpty() || !targetsWithoutAuthContext.isEmpty()) { + final StringBuilder message = new StringBuilder("The "); + if (!sourcesWithoutAuthContext.isEmpty()) { + message.append("Sources ").append(sourcesWithoutAuthContext); + } + if (!sourcesWithoutAuthContext.isEmpty() && !targetsWithoutAuthContext.isEmpty()) { + message.append(" and "); + } + if (!targetsWithoutAuthContext.isEmpty()) { + message.append("Targets ").append(targetsWithoutAuthContext); + } + message.append(" are missing an authorization context."); + throw ConnectionConfigurationInvalidException.newBuilder(message.toString()).build(); + } + } + + void checkConnectionAnnouncementsOnlySetIfClientCount1() { + if (clientCount > 1 && containsTargetWithConnectionAnnouncementsTopic()) { + final String message = MessageFormat.format("Connection announcements (topic {0}) can" + + " only be used with client count 1.", Topic.CONNECTION_ANNOUNCEMENTS.getName()); + throw ConnectionConfigurationInvalidException.newBuilder(message) + .build(); + } + } + + private boolean containsTargetWithConnectionAnnouncementsTopic() { + return targets.stream() + .map(Target::getTopics) + .flatMap(Set::stream) + .map(FilteredTopic::getTopic) + .anyMatch(Topic.CONNECTION_ANNOUNCEMENTS::equals); + } + +} \ No newline at end of file diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java index c4e1329879..d085603ad8 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java @@ -117,8 +117,7 @@ static boolean isValid(final URI uri) { * @param uriString the string representation of the Connection URI. * @return the instance. * @throws NullPointerException if {@code uriString} is {@code null}. - * @throws ConnectionUriInvalidException if {@code uriString} is not a - * valid URI. + * @throws ConnectionUriInvalidException if {@code uriString} is not a valid URI. * @see #toString() */ static ConnectionUri of( @Nullable final String uriString) { diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java index 69a25a1819..61ca499af9 100755 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectivityModelFactory.java @@ -89,7 +89,7 @@ public static ConnectionBuilder newConnectionBuilder(final ConnectionId id, */ public static ConnectionBuilder newConnectionBuilder(final Connection connection) { final ConnectionBuilder builder; - if (HonoConnection.isHonoConnectionType(connection)) { + if (isHonoConnectionType(connection)) { builder = HonoConnection.getBuilder(connection); } else { builder = ImmutableConnection.getBuilder(connection); @@ -97,6 +97,10 @@ public static ConnectionBuilder newConnectionBuilder(final Connection connection return builder; } + private static boolean isHonoConnectionType(final Connection connection) { + return connection.getConnectionType() == ConnectionType.HONO; + } + /** * Creates a new {@code Connection} object from the specified JSON object. * @@ -107,7 +111,7 @@ public static ConnectionBuilder newConnectionBuilder(final Connection connection */ public static Connection connectionFromJson(final JsonObject jsonObject) { final Connection connection; - if (HonoConnection.isHonoConnectionType(jsonObject)) { + if (isHonoConnectionType(jsonObject)) { connection = HonoConnection.fromJson(jsonObject); } else { connection = ImmutableConnection.fromJson(jsonObject); @@ -115,6 +119,13 @@ public static Connection connectionFromJson(final JsonObject jsonObject) { return connection; } + private static boolean isHonoConnectionType(final JsonObject connectionJsonObject) { + return connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_TYPE) + .flatMap(ConnectionType::forName) + .filter(ConnectionType.HONO::equals) + .isPresent(); + } + /** * Creates a new {@code Measurement} with the provided parameters. * diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java index 413a6988af..78494e0ffb 100755 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java @@ -12,82 +12,42 @@ */ package org.eclipse.ditto.connectivity.model; -import static org.eclipse.ditto.base.model.common.ConditionChecker.checkArgument; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.NotThreadSafe; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; - -import org.eclipse.ditto.json.JsonCollectors; -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonField; import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonObjectBuilder; -import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.json.JsonParseException; + + /** - * Immutable implementation of {@link Connection} of type + * Immutable implementation of {@link org.eclipse.ditto.connectivity.model.AbstractConnection} of type * {@link org.eclipse.ditto.connectivity.model.ConnectionType} HONO. */ @Immutable -final class HonoConnection implements Connection { +final class HonoConnection extends AbstractConnection { - private final ConnectionId id; - @Nullable private final String name; - private final ConnectionType connectionType; - private final ConnectivityStatus connectionStatus; - private final ConnectionUri uri; - @Nullable private final Credentials credentials; - @Nullable private final String trustedCertificates; - @Nullable private final ConnectionLifecycle lifecycle; - private final List sources; - private final List targets; - private final int clientCount; - private final boolean failOverEnabled; - private final boolean validateCertificate; - private final int processorPoolSize; - private final Map specificConfig; - private final PayloadMappingDefinition payloadMappingDefinition; - private final Set tags; - @Nullable private final SshTunnel sshTunnel; + private HonoConnection(final Builder builder) { + super(builder); + } - private HonoConnection(final HonoConnection.Builder builder) { - id = checkNotNull(builder.id, "id"); - name = builder.name; - connectionType = builder.connectionType; - connectionStatus = checkNotNull(builder.connectionStatus, "connectionStatus"); - credentials = builder.credentials; - trustedCertificates = builder.trustedCertificates; - uri = ConnectionUri.of(builder.uri); - sources = Collections.unmodifiableList(new ArrayList<>(builder.sources)); - targets = Collections.unmodifiableList(new ArrayList<>(builder.targets)); - clientCount = builder.clientCount; - failOverEnabled = builder.failOverEnabled; - validateCertificate = builder.validateCertificate; - processorPoolSize = builder.processorPoolSize; - specificConfig = Collections.unmodifiableMap(new HashMap<>(builder.specificConfig)); - payloadMappingDefinition = builder.payloadMappingDefinition; - tags = Collections.unmodifiableSet(new LinkedHashSet<>(builder.tags)); - lifecycle = builder.lifecycle; - sshTunnel = builder.sshTunnel; + @Override + ConnectionUri getConnectionUri(@Nullable String builderConnectionUri){ + return ConnectionUri.of(builderConnectionUri); + } + + + static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { + final String readConnectionType = jsonObject.getValueOrThrow(JsonFields.CONNECTION_TYPE); + return ConnectionType.forName(readConnectionType) + .orElseThrow(() -> JsonParseException.newBuilder() + .message(MessageFormat.format("Connection type <{0}> is invalid!", readConnectionType)) + .build()); } /** @@ -103,12 +63,12 @@ private HonoConnection(final HonoConnection.Builder builder) { public static ConnectionBuilder getBuilder(final ConnectionId id, final ConnectionType connectionType, final ConnectivityStatus connectionStatus, - @Nullable final String uri) { + final String uri) { return new HonoConnection.Builder(connectionType) .id(id) .connectionStatus(connectionStatus) - .uri(uri); + .uri(ConnectionUri.of(uri).toString()); } /** @@ -119,26 +79,9 @@ public static ConnectionBuilder getBuilder(final ConnectionId id, * @throws NullPointerException if {@code connection} is {@code null}. */ public static ConnectionBuilder getBuilder(final Connection connection) { - checkNotNull(connection, "Connection"); - - return new HonoConnection.Builder(connection.getConnectionType()) - .id(connection.getId()) - .connectionStatus(connection.getConnectionStatus()) - .credentials(connection.getCredentials().orElse(null)) - .uri(connection.getUri()) - .trustedCertificates(connection.getTrustedCertificates().orElse(null)) - .failoverEnabled(connection.isFailoverEnabled()) - .validateCertificate(connection.isValidateCertificates()) - .processorPoolSize(connection.getProcessorPoolSize()) - .sources(connection.getSources()) - .targets(connection.getTargets()) - .clientCount(connection.getClientCount()) - .specificConfig(connection.getSpecificConfig()) - .payloadMappingDefinition(connection.getPayloadMappingDefinition()) - .name(connection.getName().orElse(null)) - .sshTunnel(connection.getSshTunnel().orElse(null)) - .tags(connection.getTags()) - .lifecycle(connection.getLifecycle().orElse(null)); + checkNotNull(connection, "connection"); + return fromConnection(connection, + new HonoConnection.Builder(connection.getConnectionType())); } /** @@ -151,573 +94,42 @@ public static ConnectionBuilder getBuilder(final Connection connection) { * @throws org.eclipse.ditto.json.JsonMissingFieldException if {@code jsonObject} does not contain a value at the defined location. */ public static Connection fromJson(final JsonObject jsonObject) { - final ConnectionType type = ImmutableConnection.getConnectionTypeOrThrow(jsonObject); - final MappingContext mappingContext = jsonObject.getValue(JsonFields.MAPPING_CONTEXT) - .map(ConnectivityModelFactory::mappingContextFromJson) - .orElse(null); - - final PayloadMappingDefinition payloadMappingDefinition = - jsonObject.getValue(JsonFields.MAPPING_DEFINITIONS) - .map(ImmutablePayloadMappingDefinition::fromJson) - .orElse(ConnectivityModelFactory.emptyPayloadMappingDefinition()); - - final ConnectionBuilder builder = new HonoConnection.Builder(type) - .id(ConnectionId.of(jsonObject.getValueOrThrow(JsonFields.ID))) - .connectionStatus(ImmutableConnection.getConnectionStatusOrThrow(jsonObject)) - .uri(jsonObject.getValue(JsonFields.URI).orElse("")) - .sources(ImmutableConnection.getSources(jsonObject)) - .targets(ImmutableConnection.getTargets(jsonObject)) - .name(jsonObject.getValue(JsonFields.NAME).orElse(null)) - .mappingContext(mappingContext) - .payloadMappingDefinition(payloadMappingDefinition) - .specificConfig(ImmutableConnection.getSpecificConfiguration(jsonObject)) - .tags(ImmutableConnection.getTags(jsonObject)); - - jsonObject.getValue(JsonFields.LIFECYCLE) - .flatMap(ConnectionLifecycle::forName).ifPresent(builder::lifecycle); - jsonObject.getValue(JsonFields.CREDENTIALS).ifPresent(builder::credentialsFromJson); - jsonObject.getValue(JsonFields.CLIENT_COUNT).ifPresent(builder::clientCount); - jsonObject.getValue(JsonFields.FAILOVER_ENABLED).ifPresent(builder::failoverEnabled); - jsonObject.getValue(JsonFields.VALIDATE_CERTIFICATES).ifPresent(builder::validateCertificate); - jsonObject.getValue(JsonFields.PROCESSOR_POOL_SIZE).ifPresent(builder::processorPoolSize); - jsonObject.getValue(JsonFields.TRUSTED_CERTIFICATES).ifPresent(builder::trustedCertificates); - jsonObject.getValue(JsonFields.SSH_TUNNEL) - .ifPresent(jsonFields -> builder.sshTunnel(ImmutableSshTunnel.fromJson(jsonFields))); - - return builder.build(); - } - - static boolean isHonoConnectionType(final JsonObject connectionJsonObject) { - return connectionJsonObject.getValue(Connection.JsonFields.CONNECTION_TYPE) - .flatMap(ConnectionType::forName) - .filter(ConnectionType.HONO::equals) - .isPresent(); - } - - static boolean isHonoConnectionType(final Connection connection) { - return connection.getConnectionType() == ConnectionType.HONO; - } - - @Override - public ConnectionId getId() { - return id; - } - - @Override - public Optional getName() { - return Optional.ofNullable(name); - } - - @Override - public ConnectionType getConnectionType() { - return connectionType; - } - - @Override - public ConnectivityStatus getConnectionStatus() { - return connectionStatus; - } - - @Override - public List getSources() { - return sources; - } - - @Override - public List getTargets() { - return targets; - } - - @Override - public Optional getSshTunnel() { - return Optional.ofNullable(sshTunnel); - } - - @Override - public int getClientCount() { - return clientCount; - } - - @Override - public boolean isFailoverEnabled() { - return failOverEnabled; - } - - @Override - public Optional getCredentials() { - return Optional.ofNullable(credentials); - } - - @Override - public Optional getTrustedCertificates() { - return Optional.ofNullable(trustedCertificates); - } - - @Override - public String getUri() { - return uri.toString(); - } - - @Override - public String getProtocol() { - return uri.getProtocol(); - } - - @Override - public Optional getUsername() { - return uri.getUserName(); - } - - @Override - public Optional getPassword() { - return uri.getPassword(); - } - - @Override - public String getHostname() { - return uri.getHostname(); - } - - @Override - public int getPort() { - return uri.getPort(); - } - - @Override - public Optional getPath() { - return uri.getPath(); - } - - @Override - public boolean isValidateCertificates() { - return validateCertificate; - } - - @Override - public int getProcessorPoolSize() { - return processorPoolSize; - } - - @Override - public Map getSpecificConfig() { - return specificConfig; - } - - @Override - public PayloadMappingDefinition getPayloadMappingDefinition() { - return payloadMappingDefinition; - } - - @Override - public Set getTags() { - return tags; - } - - @Override - public Optional getLifecycle() { - return Optional.ofNullable(lifecycle); - } - - @Override - public JsonObject toJson(final JsonSchemaVersion schemaVersion, final Predicate thePredicate) { - final Predicate predicate = schemaVersion.and(thePredicate); - final JsonObjectBuilder jsonObjectBuilder = JsonFactory.newObjectBuilder(); - - if (null != lifecycle) { - jsonObjectBuilder.set(JsonFields.LIFECYCLE, lifecycle.name(), predicate); - } - jsonObjectBuilder.set(JsonFields.ID, String.valueOf(id), predicate); - jsonObjectBuilder.set(JsonFields.NAME, name, predicate); - jsonObjectBuilder.set(JsonFields.CONNECTION_TYPE, connectionType.getName(), predicate); - jsonObjectBuilder.set(JsonFields.CONNECTION_STATUS, connectionStatus.getName(), predicate); - jsonObjectBuilder.set(JsonFields.URI, uri.toString(), predicate); - jsonObjectBuilder.set(JsonFields.SOURCES, sources.stream() - .sorted(Comparator.comparingInt(Source::getIndex)) - .map(source -> source.toJson(schemaVersion, thePredicate)) - .collect(JsonCollectors.valuesToArray()), predicate.and(Objects::nonNull)); - jsonObjectBuilder.set(JsonFields.TARGETS, targets.stream() - .map(target -> target.toJson(schemaVersion, thePredicate)) - .collect(JsonCollectors.valuesToArray()), predicate.and(Objects::nonNull)); - jsonObjectBuilder.set(JsonFields.CLIENT_COUNT, clientCount, predicate); - jsonObjectBuilder.set(JsonFields.FAILOVER_ENABLED, failOverEnabled, predicate); - jsonObjectBuilder.set(JsonFields.VALIDATE_CERTIFICATES, validateCertificate, predicate); - jsonObjectBuilder.set(JsonFields.PROCESSOR_POOL_SIZE, processorPoolSize, predicate); - if (!specificConfig.isEmpty()) { - jsonObjectBuilder.set(JsonFields.SPECIFIC_CONFIG, specificConfig.entrySet() - .stream() - .map(entry -> JsonField.newInstance(entry.getKey(), JsonValue.of(entry.getValue()))) - .collect(JsonCollectors.fieldsToObject()), predicate); - } - if (!payloadMappingDefinition.isEmpty()) { - jsonObjectBuilder.set(JsonFields.MAPPING_DEFINITIONS, - payloadMappingDefinition.toJson(schemaVersion, thePredicate)); - } - if (credentials != null) { - jsonObjectBuilder.set(JsonFields.CREDENTIALS, credentials.toJson()); - } - if (trustedCertificates != null) { - jsonObjectBuilder.set(JsonFields.TRUSTED_CERTIFICATES, trustedCertificates, predicate); - } - if (sshTunnel != null) { - jsonObjectBuilder.set(JsonFields.SSH_TUNNEL, sshTunnel.toJson(predicate), predicate); - } - jsonObjectBuilder.set(JsonFields.TAGS, tags.stream() - .map(JsonFactory::newValue) - .collect(JsonCollectors.valuesToArray()), predicate); - return jsonObjectBuilder.build(); + final ConnectionType type = getConnectionTypeOrThrow(jsonObject); + final HonoConnection.Builder builder = new HonoConnection.Builder(type); + fromJson(getJsonObjectWithEmptyUri(jsonObject), builder); + return builder.build(); } - @SuppressWarnings("OverlyComplexMethod") - @Override - public boolean equals(@Nullable final Object o) { - if (this == o) { - return true; + private static JsonObject getJsonObjectWithEmptyUri(final JsonObject jsonObject) { + if (!jsonObject.contains(JsonFields.URI.getPointer())) { + return jsonObject.set(JsonFields.URI, ""); } - if (o == null || getClass() != o.getClass()) { - return false; + if (!jsonObject.getValue(Connection.JsonFields.URI).isPresent()) { + return jsonObject.set(JsonFields.URI, ""); } - final HonoConnection that = (HonoConnection) o; - return failOverEnabled == that.failOverEnabled && - Objects.equals(id, that.id) && - Objects.equals(name, that.name) && - Objects.equals(connectionType, that.connectionType) && - Objects.equals(connectionStatus, that.connectionStatus) && - Objects.equals(sources, that.sources) && - Objects.equals(targets, that.targets) && - Objects.equals(clientCount, that.clientCount) && - Objects.equals(credentials, that.credentials) && - Objects.equals(trustedCertificates, that.trustedCertificates) && - Objects.equals(uri, that.uri) && - Objects.equals(processorPoolSize, that.processorPoolSize) && - Objects.equals(validateCertificate, that.validateCertificate) && - Objects.equals(specificConfig, that.specificConfig) && - Objects.equals(payloadMappingDefinition, that.payloadMappingDefinition) && - Objects.equals(lifecycle, that.lifecycle) && - Objects.equals(sshTunnel, that.sshTunnel) && - Objects.equals(tags, that.tags); + return jsonObject; } - @Override - public int hashCode() { - return Objects.hash(id, name, connectionType, connectionStatus, sources, targets, clientCount, failOverEnabled, - credentials, trustedCertificates, uri, validateCertificate, processorPoolSize, specificConfig, - payloadMappingDefinition, sshTunnel, tags, lifecycle); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " [" + - "id=" + id + - ", name=" + name + - ", connectionType=" + connectionType + - ", connectionStatus=" + connectionStatus + - ", failoverEnabled=" + failOverEnabled + - ", credentials=" + credentials + - ", trustedCertificates=hash:" + Objects.hash(trustedCertificates) + - ", uri=\"\"" + - ", sources=" + sources + - ", targets=" + targets + - ", sshTunnel=" + sshTunnel + - ", clientCount=" + clientCount + - ", validateCertificate=" + validateCertificate + - ", processorPoolSize=" + processorPoolSize + - ", specificConfig=" + specificConfig + - ", payloadMappingDefinition=" + payloadMappingDefinition + - ", tags=" + tags + - ", lifecycle=" + lifecycle + - "]"; - } /** - * Builder for {@code HonoConnection}. + * Builder for {@code AbstractConnectionBuilder}. */ @NotThreadSafe - private static final class Builder implements ConnectionBuilder { - - private static final String MIGRATED_MAPPER_ID = "javascript"; - private final ConnectionType connectionType; - - // required but changeable: - @Nullable private ConnectionId id; - @Nullable private ConnectivityStatus connectionStatus; - @Nullable private String uri; - - // optional: - @Nullable private String name = null; - @Nullable private Credentials credentials; - @Nullable private MappingContext mappingContext = null; - @Nullable private String trustedCertificates; - @Nullable private ConnectionLifecycle lifecycle = null; - @Nullable private SshTunnel sshTunnel = null; - - // optional with default: - private Set tags = new LinkedHashSet<>(); - private boolean failOverEnabled = true; - private boolean validateCertificate = true; - private final List sources = new ArrayList<>(); - private final List targets = new ArrayList<>(); - private int clientCount = 1; - private int processorPoolSize = 1; - private PayloadMappingDefinition payloadMappingDefinition = - ConnectivityModelFactory.emptyPayloadMappingDefinition(); - private final Map specificConfig = new HashMap<>(); - - private Builder(final ConnectionType connectionType) { - this.connectionType = checkNotNull(connectionType, "connectionType"); - } - - @Override - public ConnectionBuilder id(final ConnectionId id) { - this.id = checkNotNull(id, "id"); - return this; - } - - @Override - public ConnectionBuilder name(@Nullable final String name) { - this.name = name; - return this; - } - - @Override - public ConnectionBuilder credentials(@Nullable final Credentials credentials) { - this.credentials = credentials; - return this; - } - - static boolean isBlankOrNull(@Nullable final String toTest) { - return null == toTest || toTest.trim().isEmpty(); - } - - @Override - public HonoConnection.Builder trustedCertificates(@Nullable final String trustedCertificates) { - if (isBlankOrNull(trustedCertificates)) { - this.trustedCertificates = null; - } else { - this.trustedCertificates = trustedCertificates; - } - return this; - } - - @Override - public ConnectionBuilder uri(@Nullable final String uri) { - this.uri = uri; - return this; - } - - @Override - public ConnectionBuilder connectionStatus(final ConnectivityStatus connectionStatus) { - this.connectionStatus = checkNotNull(connectionStatus, "connectionStatus"); - return this; - } - - @Override - public ConnectionBuilder failoverEnabled(final boolean failOverEnabled) { - this.failOverEnabled = failOverEnabled; - return this; - } - - @Override - public ConnectionBuilder validateCertificate(final boolean validateCertificate) { - this.validateCertificate = validateCertificate; - return this; - } - - @Override - public ConnectionBuilder processorPoolSize(final int processorPoolSize) { - checkArgument(processorPoolSize, ps -> ps > 0, () -> "The processor pool size must be positive!"); - this.processorPoolSize = processorPoolSize; - return this; - } - - @Override - public ConnectionBuilder sources(final List sources) { - this.sources.addAll(checkNotNull(sources, "sources")); - return this; - } - - @Override - public ConnectionBuilder targets(final List targets) { - this.targets.addAll(checkNotNull(targets, "targets")); - return this; - } - - @Override - public ConnectionBuilder setSources(final List sources) { - this.sources.clear(); - return sources(sources); - } + private static final class Builder extends AbstractConnectionBuilder { - @Override - public ConnectionBuilder setTargets(final List targets) { - this.targets.clear(); - return targets(targets); - } - - @Override - public ConnectionBuilder clientCount(final int clientCount) { - checkArgument(clientCount, ps -> ps > 0, () -> "The client count must be positive!"); - this.clientCount = clientCount; - return this; - } - - @Override - public ConnectionBuilder specificConfig(final Map specificConfig) { - this.specificConfig.putAll(checkNotNull(specificConfig, "specificConfig")); - return this; - } - - @Override - public ConnectionBuilder mappingContext(@Nullable final MappingContext mappingContext) { - this.mappingContext = mappingContext; - return this; - } - - @Override - public ConnectionBuilder tags(final Collection tags) { - this.tags = new LinkedHashSet<>(checkNotNull(tags, "tags")); - return this; - } - - @Override - public ConnectionBuilder tag(final String tag) { - tags.add(checkNotNull(tag, "tag")); - return this; - } - - @Override - public ConnectionBuilder lifecycle(@Nullable final ConnectionLifecycle lifecycle) { - this.lifecycle = lifecycle; - return this; - } - - @Override - public ConnectionBuilder sshTunnel(@Nullable final SshTunnel sshTunnel) { - this.sshTunnel = sshTunnel; - return this; - } - - @Override - public ConnectionBuilder payloadMappingDefinition(final PayloadMappingDefinition payloadMappingDefinition) { - this.payloadMappingDefinition = payloadMappingDefinition; - return this; + Builder(final ConnectionType connectionType) { + super(connectionType); } @Override public Connection build() { - checkSourceAndTargetAreValid(); - checkAuthorizationContextsAreValid(); - checkConnectionAnnouncementsOnlySetIfClientCount1(); - migrateLegacyConfigurationOnTheFly(); + super.checkSourceAndTargetAreValid(); + super.checkAuthorizationContextsAreValid(); + super.checkConnectionAnnouncementsOnlySetIfClientCount1(); + super.migrateLegacyConfigurationOnTheFly(); return new HonoConnection(this); } - private boolean shouldMigrateMappingContext() { - return mappingContext != null; - } - - private void migrateLegacyConfigurationOnTheFly() { - if (shouldMigrateMappingContext()) { - this.payloadMappingDefinition = - payloadMappingDefinition.withDefinition(MIGRATED_MAPPER_ID, mappingContext); - } - setSources(sources.stream().map(this::migrateSource).collect(Collectors.toList())); - setTargets(targets.stream().map(this::migrateTarget).collect(Collectors.toList())); - } - - private Source migrateSource(final Source source) { - final Source sourceAfterReplyTargetMigration = ImmutableSource.migrateReplyTarget(source, connectionType); - if (shouldMigrateMappingContext()) { - return new ImmutableSource.Builder(sourceAfterReplyTargetMigration) - .payloadMapping(addMigratedPayloadMappings(source.getPayloadMapping())) - .build(); - } else { - return sourceAfterReplyTargetMigration; - } - } - - private Target migrateTarget(final Target target) { - final boolean shouldAddHeaderMapping = shouldAddDefaultHeaderMappingToTarget(connectionType); - final boolean shouldMigrateMappingContext = shouldMigrateMappingContext(); - if (shouldMigrateMappingContext || shouldAddHeaderMapping) { - final TargetBuilder builder = new ImmutableTarget.Builder(target); - if (shouldMigrateMappingContext) { - builder.payloadMapping(addMigratedPayloadMappings(target.getPayloadMapping())); - } - if (shouldAddHeaderMapping) { - builder.headerMapping(target.getHeaderMapping()); - } - return builder.build(); - } else { - return target; - } - } - - private boolean shouldAddDefaultHeaderMappingToTarget(final ConnectionType connectionType) { - return connectionType == ConnectionType.HONO; - } - - - private PayloadMapping addMigratedPayloadMappings(final PayloadMapping payloadMapping) { - final ArrayList merged = new ArrayList<>(payloadMapping.getMappings()); - merged.add(MIGRATED_MAPPER_ID); - return ConnectivityModelFactory.newPayloadMapping(merged); - } - - private void checkSourceAndTargetAreValid() { - if (sources.isEmpty() && targets.isEmpty()) { - throw ConnectionConfigurationInvalidException.newBuilder("Either a source or a target must be " + - "specified in the configuration of a connection!").build(); - } - } - - /** - * If no context is set on connection level each target and source must have its own context. - */ - private void checkAuthorizationContextsAreValid() { - // if the auth context on connection level is empty, - // an auth context is required to be set on each source/target - final Set sourcesWithoutAuthContext = sources.stream() - .filter(source -> source.getAuthorizationContext().isEmpty()) - .flatMap(source -> source.getAddresses().stream()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - final Set targetsWithoutAuthContext = targets.stream() - .filter(target -> target.getAuthorizationContext().isEmpty()) - .map(Target::getAddress) - .collect(Collectors.toCollection(LinkedHashSet::new)); - - if (!sourcesWithoutAuthContext.isEmpty() || !targetsWithoutAuthContext.isEmpty()) { - final StringBuilder message = new StringBuilder("The "); - if (!sourcesWithoutAuthContext.isEmpty()) { - message.append("Sources ").append(sourcesWithoutAuthContext); - } - if (!sourcesWithoutAuthContext.isEmpty() && !targetsWithoutAuthContext.isEmpty()) { - message.append(" and "); - } - if (!targetsWithoutAuthContext.isEmpty()) { - message.append("Targets ").append(targetsWithoutAuthContext); - } - message.append(" are missing an authorization context."); - throw ConnectionConfigurationInvalidException.newBuilder(message.toString()).build(); - } - } - - private void checkConnectionAnnouncementsOnlySetIfClientCount1() { - if (clientCount > 1 && containsTargetWithConnectionAnnouncementsTopic()) { - final String message = MessageFormat.format("Connection announcements (topic {0}) can" + - " only be used with client count 1.", Topic.CONNECTION_ANNOUNCEMENTS.getName()); - throw ConnectionConfigurationInvalidException.newBuilder(message) - .build(); - } - } - - private boolean containsTargetWithConnectionAnnouncementsTopic() { - return targets.stream() - .map(Target::getTopics) - .flatMap(Set::stream) - .map(FilteredTopic::getTopic) - .anyMatch(Topic.CONNECTION_ANNOUNCEMENTS::equals); - } - } } diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java index 35ca605214..d448e50e1f 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java @@ -12,85 +12,24 @@ */ package org.eclipse.ditto.connectivity.model; -import static org.eclipse.ditto.base.model.common.ConditionChecker.checkArgument; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.NotThreadSafe; - -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; -import org.eclipse.ditto.json.JsonArray; -import org.eclipse.ditto.json.JsonCollectors; -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonField; import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonObjectBuilder; import org.eclipse.ditto.json.JsonParseException; -import org.eclipse.ditto.json.JsonValue; /** * Immutable implementation of {@link Connection}. */ @Immutable -final class ImmutableConnection implements Connection { - - private final ConnectionId id; - @Nullable private final String name; - private final ConnectionType connectionType; - private final ConnectivityStatus connectionStatus; - private final ConnectionUri uri; - @Nullable private final Credentials credentials; - @Nullable private final String trustedCertificates; - @Nullable private final ConnectionLifecycle lifecycle; - - - private final List sources; - private final List targets; - private final int clientCount; - private final boolean failOverEnabled; - private final boolean validateCertificate; - private final int processorPoolSize; - private final Map specificConfig; - private final PayloadMappingDefinition payloadMappingDefinition; - private final Set tags; - @Nullable private final SshTunnel sshTunnel; +final class ImmutableConnection extends AbstractConnection { - private ImmutableConnection(final Builder builder) { - id = checkNotNull(builder.id, "id"); - name = builder.name; - connectionType = builder.connectionType; - connectionStatus = checkNotNull(builder.connectionStatus, "connectionStatus"); - credentials = builder.credentials; - trustedCertificates = builder.trustedCertificates; - uri = ConnectionUri.of(checkNotNull(builder.uri, "uri")); - sources = Collections.unmodifiableList(new ArrayList<>(builder.sources)); - targets = Collections.unmodifiableList(new ArrayList<>(builder.targets)); - clientCount = builder.clientCount; - failOverEnabled = builder.failOverEnabled; - validateCertificate = builder.validateCertificate; - processorPoolSize = builder.processorPoolSize; - specificConfig = Collections.unmodifiableMap(new HashMap<>(builder.specificConfig)); - payloadMappingDefinition = builder.payloadMappingDefinition; - tags = Collections.unmodifiableSet(new LinkedHashSet<>(builder.tags)); - lifecycle = builder.lifecycle; - sshTunnel = builder.sshTunnel; + private ImmutableConnection(final ImmutableConnection.Builder builder) { + super(builder); } /** @@ -122,28 +61,16 @@ public static ConnectionBuilder getBuilder(final ConnectionId id, * @throws NullPointerException if {@code connection} is {@code null}. */ public static ConnectionBuilder getBuilder(final Connection connection) { - checkNotNull(connection, "Connection"); + checkNotNull(connection, "connection"); + return fromConnection(connection, new Builder(connection.getConnectionType())); + } - return new Builder(connection.getConnectionType()) - .id(connection.getId()) - .connectionStatus(connection.getConnectionStatus()) - .credentials(connection.getCredentials().orElse(null)) - .uri(connection.getUri()) - .trustedCertificates(connection.getTrustedCertificates().orElse(null)) - .failoverEnabled(connection.isFailoverEnabled()) - .validateCertificate(connection.isValidateCertificates()) - .processorPoolSize(connection.getProcessorPoolSize()) - .sources(connection.getSources()) - .targets(connection.getTargets()) - .clientCount(connection.getClientCount()) - .specificConfig(connection.getSpecificConfig()) - .payloadMappingDefinition(connection.getPayloadMappingDefinition()) - .name(connection.getName().orElse(null)) - .sshTunnel(connection.getSshTunnel().orElse(null)) - .tags(connection.getTags()) - .lifecycle(connection.getLifecycle().orElse(null)); + @Override + ConnectionUri getConnectionUri(@Nullable String builderConnectionUri) { + return ConnectionUri.of(checkNotNull(builderConnectionUri, "uri")); } + /** * Creates a new {@code Connection} object from the specified JSON object. * @@ -154,40 +81,11 @@ public static ConnectionBuilder getBuilder(final Connection connection) { */ public static Connection fromJson(final JsonObject jsonObject) { final ConnectionType type = getConnectionTypeOrThrow(jsonObject); - final MappingContext mappingContext = jsonObject.getValue(JsonFields.MAPPING_CONTEXT) - .map(ConnectivityModelFactory::mappingContextFromJson) - .orElse(null); - - final PayloadMappingDefinition payloadMappingDefinition = - jsonObject.getValue(JsonFields.MAPPING_DEFINITIONS) - .map(ImmutablePayloadMappingDefinition::fromJson) - .orElse(ConnectivityModelFactory.emptyPayloadMappingDefinition()); - - final ConnectionBuilder builder = new Builder(type) - .id(ConnectionId.of(jsonObject.getValueOrThrow(JsonFields.ID))) - .connectionStatus(getConnectionStatusOrThrow(jsonObject)) - .uri(jsonObject.getValueOrThrow(JsonFields.URI)) - .sources(getSources(jsonObject)) - .targets(getTargets(jsonObject)) - .name(jsonObject.getValue(JsonFields.NAME).orElse(null)) - .mappingContext(mappingContext) - .payloadMappingDefinition(payloadMappingDefinition) - .specificConfig(getSpecificConfiguration(jsonObject)) - .tags(getTags(jsonObject)); - - jsonObject.getValue(JsonFields.LIFECYCLE) - .flatMap(ConnectionLifecycle::forName).ifPresent(builder::lifecycle); - jsonObject.getValue(JsonFields.CREDENTIALS).ifPresent(builder::credentialsFromJson); - jsonObject.getValue(JsonFields.CLIENT_COUNT).ifPresent(builder::clientCount); - jsonObject.getValue(JsonFields.FAILOVER_ENABLED).ifPresent(builder::failoverEnabled); - jsonObject.getValue(JsonFields.VALIDATE_CERTIFICATES).ifPresent(builder::validateCertificate); - jsonObject.getValue(JsonFields.PROCESSOR_POOL_SIZE).ifPresent(builder::processorPoolSize); - jsonObject.getValue(JsonFields.TRUSTED_CERTIFICATES).ifPresent(builder::trustedCertificates); - jsonObject.getValue(JsonFields.SSH_TUNNEL) - .ifPresent(jsonFields -> builder.sshTunnel(ImmutableSshTunnel.fromJson(jsonFields))); + final ImmutableConnection.Builder builder = new Builder(type); + fromJson(jsonObject, builder); return builder.build(); - } + } static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { final String readConnectionType = jsonObject.getValueOrThrow(JsonFields.CONNECTION_TYPE); @@ -197,462 +95,14 @@ static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { .build()); } - static ConnectivityStatus getConnectionStatusOrThrow(final JsonObject jsonObject) { - final String readConnectionStatus = jsonObject.getValueOrThrow(JsonFields.CONNECTION_STATUS); - return ConnectivityStatus.forName(readConnectionStatus) - .orElseThrow(() -> JsonParseException.newBuilder() - .message(MessageFormat.format("Connection status <{0}> is invalid!", readConnectionStatus)) - .build()); - } - - static List getSources(final JsonObject jsonObject) { - final Optional sourcesArray = jsonObject.getValue(JsonFields.SOURCES); - if (sourcesArray.isPresent()) { - final JsonArray values = sourcesArray.get(); - return IntStream.range(0, values.getSize()) - .mapToObj(index -> values.get(index) - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .map(valueAsObject -> ConnectivityModelFactory.sourceFromJson(valueAsObject, index))) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - } else { - return Collections.emptyList(); - } - } - - static List getTargets(final JsonObject jsonObject) { - return jsonObject.getValue(JsonFields.TARGETS) - .map(array -> array.stream() - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .map(ConnectivityModelFactory::targetFromJson) - .collect(Collectors.toList())) - .orElse(Collections.emptyList()); - } - - static Map getSpecificConfiguration(final JsonObject jsonObject) { - return jsonObject.getValue(JsonFields.SPECIFIC_CONFIG) - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .map(JsonObject::stream) - .map(jsonFields -> jsonFields.collect(Collectors.toMap(JsonField::getKeyName, - f -> f.getValue().isString() ? f.getValue().asString() : f.getValue().toString()))) - .orElse(Collections.emptyMap()); - } - - static Set getTags(final JsonObject jsonObject) { - return jsonObject.getValue(JsonFields.TAGS) - .map(array -> array.stream() - .filter(JsonValue::isString) - .map(JsonValue::asString) - .collect(Collectors.toCollection(LinkedHashSet::new))) - .orElseGet(LinkedHashSet::new); - } - - @Override - public ConnectionId getId() { - return id; - } - - @Override - public Optional getName() { - return Optional.ofNullable(name); - } - - @Override - public ConnectionType getConnectionType() { - return connectionType; - } - - @Override - public ConnectivityStatus getConnectionStatus() { - return connectionStatus; - } - - @Override - public List getSources() { - return sources; - } - - @Override - public List getTargets() { - return targets; - } - - @Override - public Optional getSshTunnel() { - return Optional.ofNullable(sshTunnel); - } - - @Override - public int getClientCount() { - return clientCount; - } - - @Override - public boolean isFailoverEnabled() { - return failOverEnabled; - } - - @Override - public Optional getCredentials() { - return Optional.ofNullable(credentials); - } - - @Override - public Optional getTrustedCertificates() { - return Optional.ofNullable(trustedCertificates); - } - - @Override - public String getUri() { - return uri.toString(); - } - - @Override - public String getProtocol() { - return uri.getProtocol(); - } - - @Override - public Optional getUsername() { - return uri.getUserName(); - } - - @Override - public Optional getPassword() { - return uri.getPassword(); - } - - @Override - public String getHostname() { - return uri.getHostname(); - } - - @Override - public int getPort() { - return uri.getPort(); - } - - @Override - public Optional getPath() { - return uri.getPath(); - } - - @Override - public boolean isValidateCertificates() { - return validateCertificate; - } - - @Override - public int getProcessorPoolSize() { - return processorPoolSize; - } - - @Override - public Map getSpecificConfig() { - return specificConfig; - } - - @Override - public PayloadMappingDefinition getPayloadMappingDefinition() { - return payloadMappingDefinition; - } - - @Override - public Set getTags() { - return tags; - } - - @Override - public Optional getLifecycle() { - return Optional.ofNullable(lifecycle); - } - - @Override - public JsonObject toJson(final JsonSchemaVersion schemaVersion, final Predicate thePredicate) { - final Predicate predicate = schemaVersion.and(thePredicate); - final JsonObjectBuilder jsonObjectBuilder = JsonFactory.newObjectBuilder(); - - if (null != lifecycle) { - jsonObjectBuilder.set(JsonFields.LIFECYCLE, lifecycle.name(), predicate); - } - jsonObjectBuilder.set(JsonFields.ID, String.valueOf(id), predicate); - jsonObjectBuilder.set(JsonFields.NAME, name, predicate); - jsonObjectBuilder.set(JsonFields.CONNECTION_TYPE, connectionType.getName(), predicate); - jsonObjectBuilder.set(JsonFields.CONNECTION_STATUS, connectionStatus.getName(), predicate); - jsonObjectBuilder.set(JsonFields.URI, uri.toString(), predicate); - jsonObjectBuilder.set(JsonFields.SOURCES, sources.stream() - .sorted(Comparator.comparingInt(Source::getIndex)) - .map(source -> source.toJson(schemaVersion, thePredicate)) - .collect(JsonCollectors.valuesToArray()), predicate.and(Objects::nonNull)); - jsonObjectBuilder.set(JsonFields.TARGETS, targets.stream() - .map(target -> target.toJson(schemaVersion, thePredicate)) - .collect(JsonCollectors.valuesToArray()), predicate.and(Objects::nonNull)); - jsonObjectBuilder.set(JsonFields.CLIENT_COUNT, clientCount, predicate); - jsonObjectBuilder.set(JsonFields.FAILOVER_ENABLED, failOverEnabled, predicate); - jsonObjectBuilder.set(JsonFields.VALIDATE_CERTIFICATES, validateCertificate, predicate); - jsonObjectBuilder.set(JsonFields.PROCESSOR_POOL_SIZE, processorPoolSize, predicate); - if (!specificConfig.isEmpty()) { - jsonObjectBuilder.set(JsonFields.SPECIFIC_CONFIG, specificConfig.entrySet() - .stream() - .map(entry -> JsonField.newInstance(entry.getKey(), JsonValue.of(entry.getValue()))) - .collect(JsonCollectors.fieldsToObject()), predicate); - } - if (!payloadMappingDefinition.isEmpty()) { - jsonObjectBuilder.set(JsonFields.MAPPING_DEFINITIONS, - payloadMappingDefinition.toJson(schemaVersion, thePredicate)); - } - if (credentials != null) { - jsonObjectBuilder.set(JsonFields.CREDENTIALS, credentials.toJson()); - } - if (trustedCertificates != null) { - jsonObjectBuilder.set(JsonFields.TRUSTED_CERTIFICATES, trustedCertificates, predicate); - } - if (sshTunnel != null) { - jsonObjectBuilder.set(JsonFields.SSH_TUNNEL, sshTunnel.toJson(predicate), predicate); - } - jsonObjectBuilder.set(JsonFields.TAGS, tags.stream() - .map(JsonFactory::newValue) - .collect(JsonCollectors.valuesToArray()), predicate); - return jsonObjectBuilder.build(); - } - - @SuppressWarnings("OverlyComplexMethod") - @Override - public boolean equals(@Nullable final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final ImmutableConnection that = (ImmutableConnection) o; - return failOverEnabled == that.failOverEnabled && - Objects.equals(id, that.id) && - Objects.equals(name, that.name) && - Objects.equals(connectionType, that.connectionType) && - Objects.equals(connectionStatus, that.connectionStatus) && - Objects.equals(sources, that.sources) && - Objects.equals(targets, that.targets) && - Objects.equals(clientCount, that.clientCount) && - Objects.equals(credentials, that.credentials) && - Objects.equals(trustedCertificates, that.trustedCertificates) && - Objects.equals(uri, that.uri) && - Objects.equals(processorPoolSize, that.processorPoolSize) && - Objects.equals(validateCertificate, that.validateCertificate) && - Objects.equals(specificConfig, that.specificConfig) && - Objects.equals(payloadMappingDefinition, that.payloadMappingDefinition) && - Objects.equals(lifecycle, that.lifecycle) && - Objects.equals(sshTunnel, that.sshTunnel) && - Objects.equals(tags, that.tags); - } - - @Override - public int hashCode() { - return Objects.hash(id, name, connectionType, connectionStatus, sources, targets, clientCount, failOverEnabled, - credentials, trustedCertificates, uri, validateCertificate, processorPoolSize, specificConfig, - payloadMappingDefinition, sshTunnel, tags, lifecycle); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " [" + - "id=" + id + - ", name=" + name + - ", connectionType=" + connectionType + - ", connectionStatus=" + connectionStatus + - ", failoverEnabled=" + failOverEnabled + - ", credentials=" + credentials + - ", trustedCertificates=hash:" + Objects.hash(trustedCertificates) + - ", uri=" + uri.getUriStringWithMaskedPassword() + - ", sources=" + sources + - ", targets=" + targets + - ", sshTunnel=" + sshTunnel + - ", clientCount=" + clientCount + - ", validateCertificate=" + validateCertificate + - ", processorPoolSize=" + processorPoolSize + - ", specificConfig=" + specificConfig + - ", payloadMappingDefinition=" + payloadMappingDefinition + - ", tags=" + tags + - ", lifecycle=" + lifecycle + - "]"; - } - /** * Builder for {@code ImmutableConnection}. */ @NotThreadSafe - private static final class Builder implements ConnectionBuilder { - - private static final String MIGRATED_MAPPER_ID = "javascript"; - private final ConnectionType connectionType; - - // required but changeable: - @Nullable private ConnectionId id; - @Nullable private ConnectivityStatus connectionStatus; - @Nullable private String uri; - - // optional: - @Nullable private String name = null; - @Nullable private Credentials credentials; - @Nullable private MappingContext mappingContext = null; - @Nullable private String trustedCertificates; - @Nullable private ConnectionLifecycle lifecycle = null; - @Nullable private SshTunnel sshTunnel = null; - - // optional with default: - private Set tags = new LinkedHashSet<>(); - private boolean failOverEnabled = true; - private boolean validateCertificate = true; - private final List sources = new ArrayList<>(); - private final List targets = new ArrayList<>(); - private int clientCount = 1; - private int processorPoolSize = 1; - private PayloadMappingDefinition payloadMappingDefinition = - ConnectivityModelFactory.emptyPayloadMappingDefinition(); - private final Map specificConfig = new HashMap<>(); - - private Builder(final ConnectionType connectionType) { - this.connectionType = checkNotNull(connectionType, "Connection Type"); - } - - private static boolean isBlankOrNull(@Nullable final String toTest) { - return null == toTest || toTest.trim().isEmpty(); - } - - @Override - public ConnectionBuilder id(final ConnectionId id) { - this.id = checkNotNull(id, "ID"); - return this; - } - - @Override - public ConnectionBuilder name(@Nullable final String name) { - this.name = name; - return this; - } - - @Override - public ConnectionBuilder credentials(@Nullable final Credentials credentials) { - this.credentials = credentials; - return this; - } - - @Override - public Builder trustedCertificates(@Nullable final String trustedCertificates) { - if (isBlankOrNull(trustedCertificates)) { - this.trustedCertificates = null; - } else { - this.trustedCertificates = trustedCertificates; - } - return this; - } - - @Override - public ConnectionBuilder uri(final String uri) { - this.uri = checkNotNull(uri, "URI"); - return this; - } - - @Override - public ConnectionBuilder connectionStatus(final ConnectivityStatus connectionStatus) { - this.connectionStatus = checkNotNull(connectionStatus, "ConnectionStatus"); - return this; - } - - @Override - public ConnectionBuilder failoverEnabled(final boolean failOverEnabled) { - this.failOverEnabled = failOverEnabled; - return this; - } - - @Override - public ConnectionBuilder validateCertificate(final boolean validateCertificate) { - this.validateCertificate = validateCertificate; - return this; - } - - @Override - public ConnectionBuilder processorPoolSize(final int processorPoolSize) { - checkArgument(processorPoolSize, ps -> ps > 0, () -> "The processor pool size must be positive!"); - this.processorPoolSize = processorPoolSize; - return this; - } - - @Override - public ConnectionBuilder sources(final List sources) { - this.sources.addAll(checkNotNull(sources, "sources")); - return this; - } - - @Override - public ConnectionBuilder targets(final List targets) { - this.targets.addAll(checkNotNull(targets, "targets")); - return this; - } - - @Override - public ConnectionBuilder setSources(final List sources) { - this.sources.clear(); - return sources(sources); - } - - @Override - public ConnectionBuilder setTargets(final List targets) { - this.targets.clear(); - return targets(targets); - } - - @Override - public ConnectionBuilder clientCount(final int clientCount) { - checkArgument(clientCount, ps -> ps > 0, () -> "The client count must be > 0!"); - this.clientCount = clientCount; - return this; - } - - @Override - public ConnectionBuilder specificConfig(final Map specificConfig) { - this.specificConfig.putAll(checkNotNull(specificConfig, "Specific Config")); - return this; - } - - @Override - public ConnectionBuilder mappingContext(@Nullable final MappingContext mappingContext) { - this.mappingContext = mappingContext; - return this; - } - - @Override - public ConnectionBuilder tags(final Collection tags) { - this.tags = new LinkedHashSet<>(checkNotNull(tags, "tags to set")); - return this; - } - - @Override - public ConnectionBuilder tag(final String tag) { - tags.add(checkNotNull(tag, "tag to set")); - return this; - } - - @Override - public ConnectionBuilder lifecycle(@Nullable final ConnectionLifecycle lifecycle) { - this.lifecycle = lifecycle; - return this; - } - - @Override - public ConnectionBuilder sshTunnel(@Nullable final SshTunnel sshTunnel) { - this.sshTunnel = sshTunnel; - return this; - } - - @Override - public ConnectionBuilder payloadMappingDefinition(final PayloadMappingDefinition payloadMappingDefinition) { - this.payloadMappingDefinition = payloadMappingDefinition; - return this; + private static final class Builder extends AbstractConnectionBuilder { + Builder(final ConnectionType connectionType) { + super(connectionType); + this.connectionType = checkNotNull(connectionType, "connectionType"); } @Override @@ -664,123 +114,6 @@ public Connection build() { return new ImmutableConnection(this); } - private boolean shouldMigrateMappingContext() { - return mappingContext != null; - } - - private void migrateLegacyConfigurationOnTheFly() { - if (shouldMigrateMappingContext()) { - this.payloadMappingDefinition = - payloadMappingDefinition.withDefinition(MIGRATED_MAPPER_ID, mappingContext); - } - setSources(sources.stream().map(this::migrateSource).collect(Collectors.toList())); - setTargets(targets.stream().map(this::migrateTarget).collect(Collectors.toList())); - } - - private Source migrateSource(final Source source) { - final Source sourceAfterReplyTargetMigration = ImmutableSource.migrateReplyTarget(source, connectionType); - if (shouldMigrateMappingContext()) { - return new ImmutableSource.Builder(sourceAfterReplyTargetMigration) - .payloadMapping(addMigratedPayloadMappings(source.getPayloadMapping())) - .build(); - } else { - return sourceAfterReplyTargetMigration; - } - } - - private Target migrateTarget(final Target target) { - final boolean shouldAddHeaderMapping = shouldAddDefaultHeaderMappingToTarget(connectionType); - final boolean shouldMigrateMappingContext = shouldMigrateMappingContext(); - if (shouldMigrateMappingContext || shouldAddHeaderMapping) { - final TargetBuilder builder = new ImmutableTarget.Builder(target); - if (shouldMigrateMappingContext) { - builder.payloadMapping(addMigratedPayloadMappings(target.getPayloadMapping())); - } - if (shouldAddHeaderMapping) { - builder.headerMapping(target.getHeaderMapping()); - } - return builder.build(); - } else { - return target; - } - } - - private boolean shouldAddDefaultHeaderMappingToTarget(final ConnectionType connectionType) { - switch (connectionType) { - case AMQP_091: - case AMQP_10: - case KAFKA: - case MQTT_5: - return true; - case MQTT: - case HTTP_PUSH: - default: - return false; - } - } - - - private PayloadMapping addMigratedPayloadMappings(final PayloadMapping payloadMapping) { - final ArrayList merged = new ArrayList<>(payloadMapping.getMappings()); - merged.add(MIGRATED_MAPPER_ID); - return ConnectivityModelFactory.newPayloadMapping(merged); - } - - private void checkSourceAndTargetAreValid() { - if (sources.isEmpty() && targets.isEmpty()) { - throw ConnectionConfigurationInvalidException.newBuilder("Either a source or a target must be " + - "specified in the configuration of a connection!").build(); - } - } - - /** - * If no context is set on connection level each target and source must have its own context. - */ - private void checkAuthorizationContextsAreValid() { - // if the auth context on connection level is empty, - // an auth context is required to be set on each source/target - final Set sourcesWithoutAuthContext = sources.stream() - .filter(source -> source.getAuthorizationContext().isEmpty()) - .flatMap(source -> source.getAddresses().stream()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - final Set targetsWithoutAuthContext = targets.stream() - .filter(target -> target.getAuthorizationContext().isEmpty()) - .map(Target::getAddress) - .collect(Collectors.toCollection(LinkedHashSet::new)); - - if (!sourcesWithoutAuthContext.isEmpty() || !targetsWithoutAuthContext.isEmpty()) { - final StringBuilder message = new StringBuilder("The "); - if (!sourcesWithoutAuthContext.isEmpty()) { - message.append("Sources ").append(sourcesWithoutAuthContext); - } - if (!sourcesWithoutAuthContext.isEmpty() && !targetsWithoutAuthContext.isEmpty()) { - message.append(" and "); - } - if (!targetsWithoutAuthContext.isEmpty()) { - message.append("Targets ").append(targetsWithoutAuthContext); - } - message.append(" are missing an authorization context."); - throw ConnectionConfigurationInvalidException.newBuilder(message.toString()).build(); - } - } - - private void checkConnectionAnnouncementsOnlySetIfClientCount1() { - if (clientCount > 1 && containsTargetWithConnectionAnnouncementsTopic()) { - final String message = MessageFormat.format("Connection announcements (topic {0}) can" + - " only be used with client count 1.", Topic.CONNECTION_ANNOUNCEMENTS.getName()); - throw ConnectionConfigurationInvalidException.newBuilder(message) - .build(); - } - } - - private boolean containsTargetWithConnectionAnnouncementsTopic() { - return targets.stream() - .map(Target::getTopics) - .flatMap(Set::stream) - .map(FilteredTopic::getTopic) - .anyMatch(Topic.CONNECTION_ANNOUNCEMENTS::equals); - } - } } diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java index bde214c9c9..60c9da6509 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java @@ -16,7 +16,7 @@ import org.junit.Test; -public class ConnectionUriTest { +public final class ConnectionUriTest { @Test public void parseUriAsExpected() { @@ -64,7 +64,6 @@ public void parseUriWithoutCredentials() { @Test public void parseUriWithoutPath() { final ConnectionUri underTest = ConnectionUri.of("amqps://foo:bar@hono.eclipse.org:5671"); - assertThat(underTest.getPath()).isEmpty(); } @@ -107,99 +106,85 @@ public void uriRegexFailsWithoutProtocol() { @Test public void testPasswordFromUriWithNullValue() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getPassword()).isEmpty(); } @Test public void testUserFromUriWithNullValue() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getUserName()).isEmpty(); } @Test public void testPortFromUriWithNullValue() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getPort()).isEqualTo(9999); } @Test public void testHostFromUriWithNullValue() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getHostname()).isEmpty(); } @Test public void testProtocolFromUriWithNullValue() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getPassword()).isEmpty(); } @Test public void testPathFromUriWithNullValue() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getPath()).isEmpty(); } @Test public void testUriStringWithMaskedPasswordFromUriWithNullValue() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getUriStringWithMaskedPassword()).isEmpty(); } @Test public void testPasswordFromUriWithEmptyString() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getPassword()).isEmpty(); } @Test public void testUserFromUriWithEmptyString() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getUserName()).isEmpty(); } @Test public void testPortFromUriWithEmptyString() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getPort()).isEqualTo(9999); } @Test public void testHostFromUriWithEmptyString() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getHostname()).isEmpty(); } @Test public void testProtocolFromUriWithEmptyString() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getPassword()).isEmpty(); } @Test public void testPathFromUriWithEmptyString() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getPath()).isEmpty(); } @Test public void testUriStringWithMaskedPasswordFromUriWithEmptyString() { - final ConnectionUri underTest = - ConnectionUri.of(null); + final ConnectionUri underTest = ConnectionUri.of(null); assertThat(underTest.getUriStringWithMaskedPassword()).isEmpty(); } diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java index 305291f49f..6bfb65a206 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java @@ -28,8 +28,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.annotation.Nullable; - import org.eclipse.ditto.base.model.auth.AuthorizationContext; import org.eclipse.ditto.base.model.auth.AuthorizationSubject; import org.eclipse.ditto.base.model.auth.DittoAuthorizationContextType; @@ -43,7 +41,7 @@ import nl.jqno.equalsverifier.EqualsVerifier; /** - * Unit test for {@link ImmutableConnection}. + * Unit test for {@link org.eclipse.ditto.connectivity.model.HonoConnection}. */ public final class HonoConnectionTest { private static final ConnectionType TYPE = ConnectionType.HONO; @@ -52,7 +50,6 @@ public final class HonoConnectionTest { private static final ConnectionId ID = ConnectionId.of("myHonoConnectionId"); private static final String NAME = "myHonoConnection"; - @Nullable private static final String URI = null; private static final String URI_EMPTY = ""; private static final Credentials CREDENTIALS = ClientCertificateCredentials.newBuilder().build(); @@ -64,27 +61,27 @@ public final class HonoConnectionTest { private static final String JAVA_SCRIPT_MAPPING = "JavaScript"; private static final String MIGRATED_MAPPER_ID = "javascript"; - private static final Source SOURCE1 = ConnectivityModelFactory.newSource(AUTHORIZATION_CONTEXT, "amqp/source1"); - private static final Source SOURCE2 = ConnectivityModelFactory.newSource(AUTHORIZATION_CONTEXT, "amqp/source2", 1); + private static final Source SOURCE1 = ConnectivityModelFactory.newSource(AUTHORIZATION_CONTEXT, "source"); + private static final Source SOURCE2 = ConnectivityModelFactory.newSource(AUTHORIZATION_CONTEXT, "source", 1); private static final List SOURCES = Arrays.asList(SOURCE1, SOURCE2); private static final List SOURCES_WITH_REPLY_TARGET_DISABLED = SOURCES.stream() .map(s -> ConnectivityModelFactory.newSourceBuilder(s).replyTargetEnabled(false).build()) .collect(Collectors.toList()); private static final HeaderMapping HEADER_MAPPING = ConnectivityModelFactory.emptyHeaderMapping(); private static final Target TARGET1 = ConnectivityModelFactory.newTargetBuilder() - .address("amqp/target1") + .address("target") .authorizationContext(AUTHORIZATION_CONTEXT) .headerMapping(HEADER_MAPPING) .topics(Topic.TWIN_EVENTS, Topic.LIVE_EVENTS) .build(); private static final Target TARGET2 = ConnectivityModelFactory.newTargetBuilder() - .address("amqp/target2") + .address("target") .authorizationContext(AUTHORIZATION_CONTEXT) .headerMapping(HEADER_MAPPING) .topics(Topic.LIVE_MESSAGES, Topic.LIVE_MESSAGES, Topic.LIVE_EVENTS) .build(); private static final Target TARGET3 = ConnectivityModelFactory.newTargetBuilder() - .address("amqp/target3") + .address("target") .authorizationContext(AUTHORIZATION_CONTEXT) .headerMapping(HEADER_MAPPING) .topics(Topic.LIVE_MESSAGES, Topic.LIVE_MESSAGES, Topic.LIVE_COMMANDS) @@ -178,13 +175,12 @@ public final class HonoConnectionTest { private static final Set KNOWN_TAGS = Collections.singleton("HONO"); - private static final JsonObject KNOWN_JSON= JsonObject.newBuilder() + private static final JsonObject KNOWN_JSON_WITHOUT_URI= JsonObject.newBuilder() .set(Connection.JsonFields.ID, ID.toString()) .set(Connection.JsonFields.NAME, NAME) .set(Connection.JsonFields.CONNECTION_TYPE, TYPE.getName()) .set(Connection.JsonFields.CONNECTION_STATUS, STATUS.getName()) .set(Connection.JsonFields.CREDENTIALS, CREDENTIALS.toJson()) - .set(Connection.JsonFields.URI, URI_EMPTY) .set(Connection.JsonFields.SOURCES, KNOWN_SOURCES_WITH_MAPPING_JSON) .set(Connection.JsonFields.TARGETS, KNOWN_TARGETS_WITH_MAPPING_JSON) .set(Connection.JsonFields.CLIENT_COUNT, 2) @@ -201,11 +197,15 @@ public final class HonoConnectionTest { .collect(JsonCollectors.valuesToArray())) .build(); - private static final JsonObject KNOWN_JSON_WITH_REPLY_TARGET = KNOWN_JSON + private final static JsonObject KNOWN_JSON_WITH_EMPTY_URI = KNOWN_JSON_WITHOUT_URI.set(Connection.JsonFields.URI, ""); + + private final static JsonObject KNOWN_JSON_WITH_NULL_URI = KNOWN_JSON_WITHOUT_URI.set(Connection.JsonFields.URI, null); + + private static final JsonObject KNOWN_JSON_WITH_REPLY_TARGET = KNOWN_JSON_WITH_EMPTY_URI .set(Connection.JsonFields.SOURCES, KNOWN_SOURCES_WITH_REPLY_TARGET) .set(Connection.JsonFields.TARGETS, KNOWN_TARGETS_WITH_HEADER_MAPPING); - private static final JsonObject KNOWN_LEGACY_JSON = KNOWN_JSON + private static final JsonObject KNOWN_LEGACY_JSON = KNOWN_JSON_WITH_EMPTY_URI .set(Connection.JsonFields.MAPPING_CONTEXT, KNOWN_MAPPING_CONTEXT.toJson()); @Test @@ -226,10 +226,8 @@ public void assertImmutability() { @Test public void createMinimalConnectionConfigurationInstance() { - final Connection connection = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) - .sources(SOURCES_WITH_REPLY_TARGET_DISABLED) - .targets(TARGETS) - .build(); + Connection connection = HonoConnection.fromJson(KNOWN_JSON_WITHOUT_URI); + connection = connection.toBuilder().setSources(SOURCES_WITH_REPLY_TARGET_DISABLED).build(); assertThat((CharSequence) connection.getId()).isEqualTo(ID); assertThat((Object) connection.getConnectionType()).isEqualTo(TYPE); assertThat(connection.getUri()).isEqualTo(URI_EMPTY); @@ -238,10 +236,8 @@ public void createMinimalConnectionConfigurationInstance() { @Test public void createMinimalConnectionConfigurationInstanceWithEmptyUri() { - final Connection connection = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) - .sources(SOURCES_WITH_REPLY_TARGET_DISABLED) - .targets(TARGETS) - .build(); + Connection connection = HonoConnection.fromJson(KNOWN_JSON_WITH_EMPTY_URI); + connection = connection.toBuilder().setSources(SOURCES_WITH_REPLY_TARGET_DISABLED).build(); assertThat((CharSequence) connection.getId()).isEqualTo(ID); assertThat((Object) connection.getConnectionType()).isEqualTo(TYPE); assertThat(connection.getUri()).isEqualTo(URI_EMPTY); @@ -250,14 +246,6 @@ public void createMinimalConnectionConfigurationInstanceWithEmptyUri() { @Test public void createInstanceWithNullId() { - assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> ConnectivityModelFactory.newConnectionBuilder(null, TYPE, STATUS, URI)) - .withMessage("The %s must not be null!", "id") - .withNoCause(); - } - - @Test - public void createInstanceWithNullIdAndEmptyUri() { assertThatExceptionOfType(NullPointerException.class) .isThrownBy(() -> ConnectivityModelFactory.newConnectionBuilder(null, TYPE, STATUS, URI_EMPTY)) .withMessage("The %s must not be null!", "id") @@ -266,26 +254,21 @@ public void createInstanceWithNullIdAndEmptyUri() { @Test public void createInstanceWithNullUri() { - final Connection connection = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) - .sources(SOURCES_WITH_REPLY_TARGET_DISABLED) - .targets(TARGETS) - .build(); + Connection connection = HonoConnection.fromJson(KNOWN_JSON_WITH_NULL_URI); + connection = connection.toBuilder().setSources(SOURCES_WITH_REPLY_TARGET_DISABLED).build(); assertThat(connection.getUri()).isEmpty(); } @Test public void createInstanceWithEmptyUri() { - final Connection connection = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI_EMPTY) - .sources(SOURCES_WITH_REPLY_TARGET_DISABLED) - .targets(TARGETS) - .build(); + Connection connection = HonoConnection.fromJson(KNOWN_JSON_WITH_EMPTY_URI); assertThat(connection.getUri()).isEmpty(); } @Test public void getBuilderFromConnectionCoversAllFields() { - final Connection connection = HonoConnection.getBuilder(ID, TYPE, STATUS, URI) + final Connection connection = HonoConnection.getBuilder(ID, TYPE, STATUS, URI_EMPTY) .sources(SOURCES) .targets(TARGETS) .connectionStatus(ConnectivityStatus.OPEN) @@ -299,7 +282,7 @@ public void getBuilderFromConnectionCoversAllFields() { .clientCertificate("certificate") .build()) .validateCertificate(true) - .uri(null) + //.uri(null) .id(ID) .payloadMappingDefinition( ConnectivityModelFactory.newPayloadMappingDefinition("test", KNOWN_JAVA_MAPPING_CONTEXT)) @@ -310,7 +293,7 @@ public void getBuilderFromConnectionCoversAllFields() { @Test public void createInstanceWithNullSources() { - final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI); + final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI_EMPTY); assertThatExceptionOfType(NullPointerException.class) .isThrownBy(() -> builder.sources(null)) @@ -320,7 +303,7 @@ public void createInstanceWithNullSources() { @Test public void createInstanceWithNullEventTarget() { - final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI); + final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI_EMPTY); assertThatExceptionOfType(NullPointerException.class) .isThrownBy(() -> builder.targets(null)) @@ -349,13 +332,12 @@ public void createInstanceWithNullEventTargetAndEmptyUri() { @Test public void createInstanceWithConnectionAnnouncementsAndClientCountGreater1() { - final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI) + final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI_EMPTY) .targets(Collections.singletonList( ConnectivityModelFactory.newTargetBuilder(TARGET1) .topics(Topic.CONNECTION_ANNOUNCEMENTS).build()) ) .clientCount(2); - assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) .isThrownBy(builder::build) .withMessageContaining(Topic.CONNECTION_ANNOUNCEMENTS.getName()) @@ -364,10 +346,9 @@ public void createInstanceWithConnectionAnnouncementsAndClientCountGreater1() { @Test public void fromJsonWithLegacyMappingContextReturnsExpected() { - final Map definitions = new HashMap<>(KNOWN_MAPPING_DEFINITIONS.getDefinitions()); definitions.putAll(LEGACY_MAPPINGS.getDefinitions()); - final Connection expected = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + final Connection expected = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI_EMPTY) .credentials(CREDENTIALS) .name(NAME) .setSources(addSourceMapping(SOURCES, JAVA_SCRIPT_MAPPING, "javascript")) @@ -383,7 +364,7 @@ public void fromJsonWithLegacyMappingContextReturnsExpected() { @Test public void fromInvalidJsonFails() { - final JsonObject INVALID_JSON = KNOWN_JSON.remove(Connection.JsonFields.SOURCES.getPointer()) + final JsonObject INVALID_JSON = KNOWN_JSON_WITHOUT_URI.remove(Connection.JsonFields.SOURCES.getPointer()) .remove(Connection.JsonFields.TARGETS.getPointer()); assertThatExceptionOfType(ConnectionConfigurationInvalidException.class) @@ -395,7 +376,7 @@ public void fromInvalidJsonFails() { @Test public void fromJsonReturnsExpected() { - final Connection expected = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + final Connection expected = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI_EMPTY) .credentials(CREDENTIALS) .name(NAME) .setSources(addSourceMapping(SOURCES, JAVA_SCRIPT_MAPPING)) @@ -405,14 +386,13 @@ public void fromJsonReturnsExpected() { .tags(KNOWN_TAGS) .build(); - final Connection actual = HonoConnection.fromJson(KNOWN_JSON); - + final Connection actual = HonoConnection.fromJson(KNOWN_JSON_WITHOUT_URI); assertThat(actual).isEqualTo(expected); } @Test public void toJsonReturnsExpected() { - final Connection underTest = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + final Connection underTest = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI_EMPTY) .credentials(CREDENTIALS) .name(NAME) .sources(addSourceMapping(Arrays.asList(SOURCE2, SOURCE1), @@ -424,13 +404,12 @@ public void toJsonReturnsExpected() { .build(); final JsonObject actual = underTest.toJson(); - System.out.println(underTest.getUri()); assertThat(actual).isEqualTo(KNOWN_JSON_WITH_REPLY_TARGET); } @Test public void emptyCertificatesLeadToEmptyOptional() { - final Connection underTest = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + final Connection underTest = ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI_EMPTY) .targets(TARGETS) .validateCertificate(true) .trustedCertificates("") @@ -441,7 +420,7 @@ public void emptyCertificatesLeadToEmptyOptional() { @Test public void emptyCertificatesFromJsonLeadToEmptyOptional() { - final JsonObject connectionJsonWithEmptyCa = KNOWN_JSON + final JsonObject connectionJsonWithEmptyCa = KNOWN_JSON_WITHOUT_URI .set(Connection.JsonFields.VALIDATE_CERTIFICATES, true) .set(Connection.JsonFields.TRUSTED_CERTIFICATES, ""); final Connection underTest = ConnectivityModelFactory.connectionFromJson(connectionJsonWithEmptyCa); @@ -472,7 +451,7 @@ public void providesDefaultHeaderMappings() { .topics(Topic.TWIN_EVENTS) .build(); final Connection connectionWithoutHeaderMappingForTarget = - ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI) + ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, URI_EMPTY) .targets(Collections.singletonList(targetWithoutHeaderMapping)) .build(); @@ -483,7 +462,7 @@ public void providesDefaultHeaderMappings() { @Test public void providesDefaultHeaderMappingsFromJson() { - final JsonObject connectionJsonWithoutHeaderMappingForTarget = KNOWN_JSON + final JsonObject connectionJsonWithoutHeaderMappingForTarget = KNOWN_JSON_WITHOUT_URI .set(Connection.JsonFields.TARGETS, JsonArray.of( TARGET1.toJson() .remove(Target.JsonFields.HEADER_MAPPING.getPointer()))); diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ImmutableConnectionTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ImmutableConnectionTest.java index 332b21c195..a983978d17 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ImmutableConnectionTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ImmutableConnectionTest.java @@ -243,7 +243,7 @@ public void createMinimalConnectionConfigurationInstance() { public void createInstanceWithNullId() { assertThatExceptionOfType(NullPointerException.class) .isThrownBy(() -> ConnectivityModelFactory.newConnectionBuilder(null, TYPE, STATUS, URI)) - .withMessage("The %s must not be null!", "ID") + .withMessage("The %s must not be null!", "id") .withNoCause(); } @@ -251,7 +251,7 @@ public void createInstanceWithNullId() { public void createInstanceWithNullUri() { assertThatExceptionOfType(NullPointerException.class) .isThrownBy(() -> ConnectivityModelFactory.newConnectionBuilder(ID, TYPE, STATUS, null)) - .withMessage("The %s must not be null!", "URI") + .withMessage("The %s must not be null!", "uri") .withNoCause(); } From 37cdff1b29bf1043866ff003ecd7c8510e5734a2 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Thu, 24 Nov 2022 11:50:10 +0200 Subject: [PATCH 58/65] Hono connection default config kafka parameters changed Signed-off-by: Andrey Balarev --- connectivity/service/src/main/resources/connectivity.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connectivity/service/src/main/resources/connectivity.conf b/connectivity/service/src/main/resources/connectivity.conf index 38d6a76083..c791b9599b 100644 --- a/connectivity/service/src/main/resources/connectivity.conf +++ b/connectivity/service/src/main/resources/connectivity.conf @@ -73,7 +73,7 @@ ditto { connectivity { hono { - base-uri = "tcp://localhost:30092" + base-uri = "tcp://localhost:9092" base-uri = ${?HONO_CONNECTION_URI} validate-certificates = false @@ -82,7 +82,7 @@ ditto { sasl-mechanism = "PLAIN" sasl-mechanism = ${?HONO_CONNECTION_SASL_MECHANISM} - bootstrap-servers = "bootstrap.server:9999" + bootstrap-servers = "localhost:9092" bootstrap-servers = ${?HONO_CONNECTION_BOOTSTRAP_SERVERS} username = "honoUsername" From ef5d05cac6f4495d17198b500cb86dd780baf997 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Thu, 24 Nov 2022 12:24:33 +0200 Subject: [PATCH 59/65] Unit test fixed Signed-off-by: Andrey Balarev --- .../persistence/ConnectionPersistenceActorTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java index b7199c7570..639ba6ba6d 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActorTest.java @@ -244,12 +244,13 @@ public TestActor.AutoPilot run(final ActorRef sender, final Object msg) { } }); final var testProbe = actorSystemResource1.newTestProbe(); + final var clientShardRegion = TestConstants.createClientActorShardRegion( + actorSystemResource1.getActorSystem(), connectionId.toString()); final var connectionActorProps = Props.create(ConnectionPersistenceActor.class, () -> new ConnectionPersistenceActor(connectionId, commandForwarderActor, pubSubMediatorProbe.ref(), - Trilean.TRUE, - ConfigFactory.empty())); + ConfigFactory.empty(), clientShardRegion)); final var underTest = actorSystemResource1.newActor(connectionActorProps, connectionId.toString()); underTest.tell(createConnection(honoConnection), testProbe.ref()); From baecbb19239c0bc40ba102b9a22d58d1789661e0 Mon Sep 17 00:00:00 2001 From: "Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM)" Date: Thu, 1 Dec 2022 14:34:14 +0200 Subject: [PATCH 60/65] Removed empty rows, checked for hono conn. type in getConnectionTypeOrThrow Signed-off-by: Silviya Georgieva-Lyoteva (IOC/PAP-DDM-RM) --- .../model/AbstractConnection.java | 8 +++- .../model/AbstractConnectionBuilder.java | 39 +++++++++---------- .../connectivity/model/HonoConnection.java | 20 ++++------ .../model/ImmutableConnection.java | 11 +++--- .../model/HonoConnectionTest.java | 21 ++++++++++ 5 files changed, 59 insertions(+), 40 deletions(-) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java index b967ee4308..c2d2525b42 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java @@ -41,6 +41,10 @@ import org.eclipse.ditto.json.JsonParseException; import org.eclipse.ditto.json.JsonValue; +/** + * Abstract implementation for common aspects of + * {@link org.eclipse.ditto.connectivity.model.Connection}. + */ abstract class AbstractConnection implements Connection { private final ConnectionId id; @@ -85,7 +89,7 @@ abstract class AbstractConnection implements Connection { abstract ConnectionUri getConnectionUri(@Nullable String builderConnectionUri); - static void fromJson(final JsonObject jsonObject, final AbstractConnectionBuilder builder) { + static void buildFromJson (final JsonObject jsonObject, final AbstractConnectionBuilder builder) { final MappingContext mappingContext = jsonObject.getValue(JsonFields.MAPPING_CONTEXT) .map(ConnectivityModelFactory::mappingContextFromJson) .orElse(null); @@ -152,7 +156,7 @@ private static List getTargets(final JsonObject jsonObject) { .orElse(Collections.emptyList()); } - private static Map getSpecificConfiguration(final JsonObject jsonObject) { + private static Map getSpecificConfiguration(final JsonObject jsonObject) { return jsonObject.getValue(JsonFields.SPECIFIC_CONFIG) .filter(JsonValue::isObject) .map(JsonValue::asObject) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java index 8ae948504d..da91612ded 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java @@ -31,7 +31,6 @@ /** * Abstract implementation for common aspects of * {@link org.eclipse.ditto.connectivity.model.ConnectionBuilder}. - * */ @Immutable @@ -40,29 +39,29 @@ abstract class AbstractConnectionBuilder implements ConnectionBuilder { private static final String MIGRATED_MAPPER_ID = "javascript"; // required but changeable: - @Nullable ConnectionId id; - @Nullable ConnectivityStatus connectionStatus; + @Nullable ConnectionId id; + @Nullable ConnectivityStatus connectionStatus; String uri; // optional: - @Nullable String name = null; - @Nullable Credentials credentials; - @Nullable MappingContext mappingContext = null; - @Nullable String trustedCertificates; - @Nullable ConnectionLifecycle lifecycle = null; - @Nullable SshTunnel sshTunnel = null; + @Nullable String name = null; + @Nullable Credentials credentials; + @Nullable MappingContext mappingContext = null; + @Nullable String trustedCertificates; + @Nullable ConnectionLifecycle lifecycle = null; + @Nullable SshTunnel sshTunnel = null; // optional with default: - Set tags = new LinkedHashSet<>(); - boolean failOverEnabled = true; - boolean validateCertificate = true; - final List sources = new ArrayList<>(); - final List targets = new ArrayList<>(); - int clientCount = 1; - int processorPoolSize = 1; - PayloadMappingDefinition payloadMappingDefinition = + Set tags = new LinkedHashSet<>(); + boolean failOverEnabled = true; + boolean validateCertificate = true; + final List sources = new ArrayList<>(); + final List targets = new ArrayList<>(); + int clientCount = 1; + int processorPoolSize = 1; + PayloadMappingDefinition payloadMappingDefinition = ConnectivityModelFactory.emptyPayloadMappingDefinition(); - final Map specificConfig = new HashMap<>(); - ConnectionType connectionType; + final Map specificConfig = new HashMap<>(); + ConnectionType connectionType; AbstractConnectionBuilder(final ConnectionType connectionType) { this.connectionType = connectionType; @@ -277,7 +276,7 @@ void checkSourceAndTargetAreValid() { /** * If no context is set on connection level each target and source must have its own context. */ - void checkAuthorizationContextsAreValid() { + void checkAuthorizationContextsAreValid() { // if the auth context on connection level is empty, // an auth context is required to be set on each source/target final Set sourcesWithoutAuthContext = sources.stream() diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java index 78494e0ffb..16b60ef4c6 100755 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java @@ -13,18 +13,13 @@ package org.eclipse.ditto.connectivity.model; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; - import java.text.MessageFormat; - import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.NotThreadSafe; - import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonParseException; - - /** * Immutable implementation of {@link org.eclipse.ditto.connectivity.model.AbstractConnection} of type * {@link org.eclipse.ditto.connectivity.model.ConnectionType} HONO. @@ -37,16 +32,16 @@ private HonoConnection(final Builder builder) { } @Override - ConnectionUri getConnectionUri(@Nullable String builderConnectionUri){ + ConnectionUri getConnectionUri(@Nullable String builderConnectionUri) { return ConnectionUri.of(builderConnectionUri); } - static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { final String readConnectionType = jsonObject.getValueOrThrow(JsonFields.CONNECTION_TYPE); - return ConnectionType.forName(readConnectionType) + return ConnectionType.forName(readConnectionType).filter(type -> type == ConnectionType.HONO) .orElseThrow(() -> JsonParseException.newBuilder() - .message(MessageFormat.format("Connection type <{0}> is invalid!", readConnectionType)) + .message(MessageFormat.format("Connection type <{0}> is invalid! Connection type must be of" + + " type <{1}>.", readConnectionType, ConnectionType.HONO)) .build()); } @@ -95,9 +90,9 @@ public static ConnectionBuilder getBuilder(final Connection connection) { */ public static Connection fromJson(final JsonObject jsonObject) { final ConnectionType type = getConnectionTypeOrThrow(jsonObject); - final HonoConnection.Builder builder = new HonoConnection.Builder(type); - fromJson(getJsonObjectWithEmptyUri(jsonObject), builder); - return builder.build(); + final HonoConnection.Builder builder = new HonoConnection.Builder(type); + buildFromJson(getJsonObjectWithEmptyUri(jsonObject), builder); + return builder.build(); } private static JsonObject getJsonObjectWithEmptyUri(final JsonObject jsonObject) { @@ -110,7 +105,6 @@ private static JsonObject getJsonObjectWithEmptyUri(final JsonObject jsonObject) return jsonObject; } - /** * Builder for {@code AbstractConnectionBuilder}. */ diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java index d448e50e1f..fc3df1d96f 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ImmutableConnection.java @@ -19,6 +19,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.NotThreadSafe; + import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonParseException; @@ -29,7 +30,7 @@ final class ImmutableConnection extends AbstractConnection { private ImmutableConnection(final ImmutableConnection.Builder builder) { - super(builder); + super(builder); } /** @@ -83,9 +84,9 @@ public static Connection fromJson(final JsonObject jsonObject) { final ConnectionType type = getConnectionTypeOrThrow(jsonObject); final ImmutableConnection.Builder builder = new Builder(type); - fromJson(jsonObject, builder); + buildFromJson(jsonObject, builder); return builder.build(); - } + } static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { final String readConnectionType = jsonObject.getValueOrThrow(JsonFields.CONNECTION_TYPE); @@ -100,8 +101,8 @@ static ConnectionType getConnectionTypeOrThrow(final JsonObject jsonObject) { */ @NotThreadSafe private static final class Builder extends AbstractConnectionBuilder { - Builder(final ConnectionType connectionType) { - super(connectionType); + Builder(final ConnectionType connectionType) { + super(connectionType); this.connectionType = checkNotNull(connectionType, "connectionType"); } diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java index 6bfb65a206..f199df5fe0 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java @@ -35,6 +35,7 @@ import org.eclipse.ditto.json.JsonCollectors; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonParseException; import org.eclipse.ditto.json.JsonValue; import org.junit.Test; @@ -311,6 +312,26 @@ public void createInstanceWithNullEventTarget() { .withNoCause(); } + @Test + public void createHonoWhitInvalidConnectionType() { + final Connection connection = HonoConnection.getBuilder(ID, ConnectionType.AMQP_10, STATUS, URI_EMPTY) + .sources(SOURCES) + .targets(TARGETS) + .connectionStatus(ConnectivityStatus.OPEN) + .name("connection") + .clientCount(5) + .trustedCertificates("certs") + .processorPoolSize(8) + .id(ID) + .build(); + + assertThatExceptionOfType(JsonParseException.class) + .isThrownBy(() -> HonoConnection.getConnectionTypeOrThrow(connection.toJson())) + .withMessage("Connection type <%s> is invalid! Connection type must be of type <%s>.", + ConnectionType.AMQP_10.getName(), ConnectionType.HONO.getName()) + .withNoCause(); + } + @Test public void createInstanceWithNullSourcesAndEmptyUri() { final ConnectionBuilder builder = HonoConnection.getBuilder(ID, TYPE, STATUS, URI_EMPTY); From 83315d651fd3a74144526256cbb257b54236aee8 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Fri, 2 Dec 2022 17:47:01 +0200 Subject: [PATCH 61/65] Documentation updated regarding Hono connection Signed-off-by: Andrey Balarev --- .../_data/sidebars/ditto_sidebar.yml | 3 + .../main/resources/jsonschema/connection.json | 1 + .../main/resources/openapi/ditto-api-2.yml | 3 +- .../sources/paths/connections/connections.yml | 2 +- .../connectivity-protocol-bindings-hono.md | 207 ++++++++++++++++++ 5 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md diff --git a/documentation/src/main/resources/_data/sidebars/ditto_sidebar.yml b/documentation/src/main/resources/_data/sidebars/ditto_sidebar.yml index b27b135c5b..1b3d92b3ca 100644 --- a/documentation/src/main/resources/_data/sidebars/ditto_sidebar.yml +++ b/documentation/src/main/resources/_data/sidebars/ditto_sidebar.yml @@ -361,6 +361,9 @@ entries: - title: Kafka 2.x protocol binding url: /connectivity-protocol-bindings-kafka2.html output: web + - title: Hono connection binding + url: /connectivity-protocol-bindings-hono.html + output: web - title: Payload mapping url: /connectivity-mapping.html output: web diff --git a/documentation/src/main/resources/jsonschema/connection.json b/documentation/src/main/resources/jsonschema/connection.json index 0ae8d3f590..c0c1be174d 100644 --- a/documentation/src/main/resources/jsonschema/connection.json +++ b/documentation/src/main/resources/jsonschema/connection.json @@ -30,6 +30,7 @@ "mqtt", "mqtt-5", "kafka", + "hono", "http-push" ], "title": "Connection type", diff --git a/documentation/src/main/resources/openapi/ditto-api-2.yml b/documentation/src/main/resources/openapi/ditto-api-2.yml index fd7ab510d8..330256ca8b 100644 --- a/documentation/src/main/resources/openapi/ditto-api-2.yml +++ b/documentation/src/main/resources/openapi/ditto-api-2.yml @@ -6493,7 +6493,7 @@ paths: Creates the connection defined in the JSON body. The ID of the connection will be **generated** by the backend. Any `ID` specified in the request body is therefore prohibited. - Supported connection types are `amqp-091`, `amqp-10`, `mqtt`, `mqtt-5`, `kafka` and `http-push`. + Supported connection types are `amqp-091`, `amqp-10`, `mqtt`, `mqtt-5`, `kafka`, `hono` and `http-push`. security: - DevOpsBasic: [] tags: @@ -8502,6 +8502,7 @@ components: - mqtt - mqtt-5 - kafka + - hono ConnectivityStatus: type: string description: The status of a connection or resource diff --git a/documentation/src/main/resources/openapi/sources/paths/connections/connections.yml b/documentation/src/main/resources/openapi/sources/paths/connections/connections.yml index e1ec5e62e0..dfc2b5372a 100644 --- a/documentation/src/main/resources/openapi/sources/paths/connections/connections.yml +++ b/documentation/src/main/resources/openapi/sources/paths/connections/connections.yml @@ -65,7 +65,7 @@ post: Creates the connection defined in the JSON body. The ID of the connection will be **generated** by the backend. Any `ID` specified in the request body is therefore prohibited. - Supported connection types are `amqp-091`, `amqp-10`, `mqtt`, `mqtt-5`, `kafka` and `http-push`. + Supported connection types are `amqp-091`, `amqp-10`, `mqtt`, `mqtt-5`, `kafka`, `hono` and `http-push`. security: - DevOpsBasic: [ ] tags: diff --git a/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md b/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md new file mode 100644 index 0000000000..69b64ca82b --- /dev/null +++ b/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md @@ -0,0 +1,207 @@ +--- +title: Eclipse Hono binding +keywords: binding, protocol, hono, kafka, kafka2 +tags: [hono, connectivity] +permalink: connectivity-protocol-bindings-hono.html +--- + +Consume messages from Eclipse Hono through Apache Kafka brokers and send messages to +Eclipse Hono the same manner as [Kafka connection](connectivity-protocol-bindings-kafka2.html) does. + +This connection type is created just for convenience - to avoid the need the user to be aware of the specific +header mappings, address formats and Kafka specificConfig, which are required to connect to Eclipse Hono. +These specifics are applied automatically at runtime for the connections of type Hono. + +Hono connection is based on Kafka connection and uses it behind the scenes, so most of the +[Kafka connection documentation](connectivity-protocol-bindings-kafka2.html) is valid for Hono connection too, +but with the following specifics (exceptions): + +## Specific Hono connection configuration + +### Connection URI +In Hono connection definition, property `uri` should not be specified (any specified value will be ignored). +The connection URI and credentials are common for all Hono connections and are derived from the configuration of the connectivity service. +`uri` will be automatically generated, based on values of 3 configuration properties of connectivity service - +`ditto.connectivity.hono.base-uri`, `ditto.connectivity.hono.username` and `ditto.connectivity.hono.password`. +Property `base-uri` must specify protocol, host and port number +(see the [example below](connectivity-protocol-bindings-hono.html#configuration-example)). +In order to connect to Kafka brokers, at runtime `username` and `password` values will be inserted between +protocol identifier and the host name of `base-uri` to form the connection URI like this `tcp://username:password@host.name:port` + +Note: If any of these parameters has to be changed, the service must be restarted to apply the new values. + + +### Source format +#### Source addresses +For a Hono connection source "addresses" are specified as aliases, which are resolved at runtime to Kafka topics to subscribe to. +Valid source addresses (aliases) are `event`, `telemetry` and `command_response`. +Runtime, these are resolved as following: +* `event` -> `hono.event` +* `telemetry` -> `hono.telemetry` +* `command_response` -> `hono.command_response` + +#### Source reply target +Similar to source addresses, the reply target `address` is an alias as well. The single valid value for it is `command`. +It is resolved to Kafka topic/key like this: +* `command` -> `hono.command/` (<thingId> is substituted by thing ID value). + +The needed header mappings for the `replyTarget` are also populated automatically at runtime and there is +no need to specify them in the connection definition. Any of the following specified value will be substituted (i.e. ignored). +Actually the `headerMapping` subsection is not required and could be omitted at all (in the context of `replyTarget`). + +For addresses `telemetry` and `event`, the following header mappings will be automatically applied: +* `device_id`: {%raw%}{{ thing:id }}{%endraw%} +* `subject`: {%raw%}{{ header:subject \| fn:default(topic:action-subject) \| fn:default(topic:criterion) }}{%endraw%}-response +* `correlation-id`: {%raw%}{{ header:correlation-id }}{%endraw%} + +For address `command_response`, the following header mappings will be automatically applied: +* `correlation-id`: {%raw%}{{ header:correlation-id }}{%endraw%} +* `status`: {%raw%}{{ header:status }}{%endraw%} + +Note: Any other header mappings defined manually will be merged with the auto-generated ones. + +The following example shows a valid Hono-connection source: +```json +{ + "addresses": ["event"], + "consumerCount": 1, + "qos": 1, + "authorizationContext": ["ditto:inbound-auth-subject"], + "enforcement": { + "input": "{%raw%}{{ header:device_id }}{%endraw%}", + "filters": ["{%raw%}{{ entity:id }}{%endraw%}"] + }, + "headerMapping": {}, + "payloadMapping": ["Ditto"], + "replyTarget": { + "enabled": true, + "address": "command", + "expectedResponseTypes": ["response", "error", "nack"] + }, + "acknowledgementRequests": { + "includes": [] + }, + "declaredAcks": [] +} +``` +#### Source header mapping + +Hono connection does not need any header mapping for sources. Anyway, the header mappings documented for +[Kafka connection](connectivity-protocol-bindings-kafka2.html) are still available. +See [Source header mapping](connectivity-protocol-bindings-kafka2.html#source-header-mapping) in Kafka protocol bindings +and [Header mapping for connections](connectivity-header-mapping.html). + +### Target format +#### Target address +The target `address` is specified as an alias and the only valid alias is `command`. +It is automatically resolved at runtime to the following Kafka topic/key: +* `command` -> `hono.command/` (<thingId> is substituted by thing ID value). + +#### Target header mapping +The target `headerMapping` section is also populated automatically at runtime and there is +no need to specify it the connection definitionm i.e. could be omitted. +If any of the following keys are specified in the connection will be ignored and automatically substituted as follows: +* `device_id`: {%raw%}{{ thing:id }}{%endraw%} +* `subject`: {%raw%}{{ header:subject \| fn:default(topic:action-subject) }}{%endraw%} +* `response-required`: {%raw%}{{ header:response-required }}{%endraw%} +* `correlation-id`: {%raw%}{{ header:correlation-id }}{%endraw%} + +Note: Any other header mappings defined manually will be merged with the auto-generated ones. + +The following example shows a valid Hono-connection target: +```json +{ + "address": "command", + "topics": [ + "_/_/things/twin/events", + "_/_/things/live/messages" + ], + "authorizationContext": ["ditto:outbound-auth-subject"] +} +``` + +### Specific configuration properties + +The properties needed by Kafka server in section `specificConfig` with the following keys will be automatically added at runtime to the connection. +Any manually specified definition of `bootstrapServers` and `saslMechanism` will be ignored, but `groupId` will not. +* `bootstrapServers` The value will be taken from configuration property `ditto.connectivity.hono.bootstrap-servers` of connectivity service. +It must contain a comma separated list of Kafka bootstrap servers to use for connecting to (in addition to automatically added connection uri). +* `saslMechanism` The value will be taken from configuration property `ditto.connectivity.hono.sasl-mechanism`. +The value must be one of `SaslMechanism` enum values to select the SASL mechanisms to use for authentication at Kafka: + * `PLAIN` + * `SCRAM-SHA-256` + * `SCRAM-SHA-512` +* `groupId`: could be specified by the user, but not required. If omitted, the value of the connection ID will be automatically used. + +Hono connection still allows to manually specify additional properties (like `debugEnabled`), which will be merged with auto-generated ones. +If no additional properties are needed, the whole section `specificConfig` could be omitted. + +### Certificate validation +The connection property `validateCertificates` is also set automatically. The value is taken from `ditto.connectivity.hono.validate-certificates` property. +For more details see [Connection configuration](connectivity-tls-certificates.html). + +## Examples +### Example of Hono connection +```json +{ + "connection": { + "id": "hono-example-connection-123", + "connectionType": "hono", + "connectionStatus": "open", + "failoverEnabled": true, + "sources": [ + { + "addresses": ["event"], + "consumerCount": 1, + "qos": 1, + "authorizationContext": ["ditto:inbound-auth-subject"], + "enforcement": { + "input": "{{ header:device_id }}", + "filters": ["{{ entity:id }}"] + }, + "headerMapping": {}, + "payloadMapping": ["Ditto"], + "replyTarget": { + "enabled": true, + "address": "command", + "expectedResponseTypes": ["response", "error", "nack"] + }, + "acknowledgementRequests": { + "includes": [] + }, + "declaredAcks": [] + } + ], + "targets": [ + { + "address": "command", + "topics": [ + "_/_/things/twin/events", + "_/_/things/live/messages" + ], + "authorizationContext": ["ditto:outbound-auth-subject"] + } + ] + } +} +``` +### Configuration example +Here is an example with all the configurations of connectivity, service which are needed by Hono connections: +``` +ditto { + connection { + hono { + base-uri = "tcp://localhost:9092" + username = "honoUsername" + password = "honoPassword" + sasl-mechanism = "PLAIN" + bootstrap-servers = "localhost:9092" + validate-certificates = false + } + } +} +``` +## Troubleshooting Hono connection configuration +To help the troubleshooting, a separate Piggyback command `retrieveHonoConnection` is implemented. +It is valid only for Hono connections. It returns the "real" Hono connection after all its properties being resolved or auto-generated. +The returned value could be used for inspection, but not for example to create a new Hono connection using it. From 100fe3dd95860981950421b240d2665a6bd279ee Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Fri, 9 Dec 2022 14:56:48 +0200 Subject: [PATCH 62/65] Fixed copyright headers Signed-off-by: Andrey Balarev --- .../eclipse/ditto/connectivity/model/AbstractConnection.java | 2 +- .../ditto/connectivity/model/AbstractConnectionBuilder.java | 2 +- .../org/eclipse/ditto/connectivity/model/ConnectionUri.java | 2 +- .../org/eclipse/ditto/connectivity/model/HonoConnection.java | 5 ++++- .../eclipse/ditto/connectivity/model/ConnectionUriTest.java | 2 +- .../eclipse/ditto/connectivity/model/HonoConnectionTest.java | 2 +- .../service/messaging/hono/HonoValidatorTest.java | 2 +- 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java index c2d2525b42..d984dfe8d7 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java index da91612ded..58363d0ab1 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java index d085603ad8..9b4e176e55 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java index 16b60ef4c6..e2c6f3bcf9 100755 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -13,10 +13,13 @@ package org.eclipse.ditto.connectivity.model; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; + import java.text.MessageFormat; + import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.NotThreadSafe; + import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonParseException; diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java index 60c9da6509..13605c985c 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/ConnectionUriTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java index f199df5fe0..6a2f3c6b1c 100644 --- a/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java +++ b/connectivity/model/src/test/java/org/eclipse/ditto/connectivity/model/HonoConnectionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. diff --git a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java index 7f9a09aaff..185f590395 100644 --- a/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java +++ b/connectivity/service/src/test/java/org/eclipse/ditto/connectivity/service/messaging/hono/HonoValidatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Contributors to the Eclipse Foundation + * Copyright (c) 2022 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. From e4369c9da7193f5c7f19a43f6c98cc081b23c994 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Mon, 12 Dec 2022 10:02:15 +0200 Subject: [PATCH 63/65] Fixed review issues in connectivity-protocol-bindings-hono.md Signed-off-by: Andrey Balarev --- .../connectivity-protocol-bindings-hono.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md b/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md index 69b64ca82b..c6fd325131 100644 --- a/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md +++ b/documentation/src/main/resources/pages/ditto/connectivity-protocol-bindings-hono.md @@ -1,7 +1,7 @@ --- title: Eclipse Hono binding keywords: binding, protocol, hono, kafka, kafka2 -tags: [hono, connectivity] +tags: [connectivity] permalink: connectivity-protocol-bindings-hono.html --- @@ -24,7 +24,7 @@ The connection URI and credentials are common for all Hono connections and are d `uri` will be automatically generated, based on values of 3 configuration properties of connectivity service - `ditto.connectivity.hono.base-uri`, `ditto.connectivity.hono.username` and `ditto.connectivity.hono.password`. Property `base-uri` must specify protocol, host and port number -(see the [example below](connectivity-protocol-bindings-hono.html#configuration-example)). +(see the [example below](#configuration-example)). In order to connect to Kafka brokers, at runtime `username` and `password` values will be inserted between protocol identifier and the host name of `base-uri` to form the connection URI like this `tcp://username:password@host.name:port` @@ -50,13 +50,13 @@ no need to specify them in the connection definition. Any of the following speci Actually the `headerMapping` subsection is not required and could be omitted at all (in the context of `replyTarget`). For addresses `telemetry` and `event`, the following header mappings will be automatically applied: -* `device_id`: {%raw%}{{ thing:id }}{%endraw%} -* `subject`: {%raw%}{{ header:subject \| fn:default(topic:action-subject) \| fn:default(topic:criterion) }}{%endraw%}-response -* `correlation-id`: {%raw%}{{ header:correlation-id }}{%endraw%} +* `device_id`: `{%raw%}{{ thing:id }}{%endraw%}` +* `subject`: `{%raw%}{{ header:subject \| fn:default(topic:action-subject) \| fn:default(topic:criterion) }}{%endraw%}-response` +* `correlation-id`: `{%raw%}{{ header:correlation-id }}{%endraw%}` For address `command_response`, the following header mappings will be automatically applied: -* `correlation-id`: {%raw%}{{ header:correlation-id }}{%endraw%} -* `status`: {%raw%}{{ header:status }}{%endraw%} +* `correlation-id`: `{%raw%}{{ header:correlation-id }}{%endraw%}` +* `status`: `{%raw%}{{ header:status }}{%endraw%}` Note: Any other header mappings defined manually will be merged with the auto-generated ones. @@ -101,10 +101,10 @@ It is automatically resolved at runtime to the following Kafka topic/key: The target `headerMapping` section is also populated automatically at runtime and there is no need to specify it the connection definitionm i.e. could be omitted. If any of the following keys are specified in the connection will be ignored and automatically substituted as follows: -* `device_id`: {%raw%}{{ thing:id }}{%endraw%} -* `subject`: {%raw%}{{ header:subject \| fn:default(topic:action-subject) }}{%endraw%} -* `response-required`: {%raw%}{{ header:response-required }}{%endraw%} -* `correlation-id`: {%raw%}{{ header:correlation-id }}{%endraw%} +* `device_id`: `{%raw%}{{ thing:id }}{%endraw%}` +* `subject`: `{%raw%}{{ header:subject \| fn:default(topic:action-subject) }}{%endraw%}` +* `response-required`: `{%raw%}{{ header:response-required }}{%endraw%}` +* `correlation-id`: `{%raw%}{{ header:correlation-id }}{%endraw%}` Note: Any other header mappings defined manually will be merged with the auto-generated ones. @@ -156,8 +156,8 @@ For more details see [Connection configuration](connectivity-tls-certificates.ht "qos": 1, "authorizationContext": ["ditto:inbound-auth-subject"], "enforcement": { - "input": "{{ header:device_id }}", - "filters": ["{{ entity:id }}"] + "input": "{%raw%}{{ header:device_id }}{%endraw%}", + "filters": ["{%raw%}{{ entity:id }}{%endraw%}"] }, "headerMapping": {}, "payloadMapping": ["Ditto"], From 444408753a4d92f32406077f7d5547a975a2fdb7 Mon Sep 17 00:00:00 2001 From: Andrey Balarev Date: Mon, 12 Dec 2022 18:24:01 +0200 Subject: [PATCH 64/65] Copyright headers Signed-off-by: Andrey Balarev --- .../messaging/persistence/ConnectionPersistenceActor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java index dd5d0d0d8a..240ae92062 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/ConnectionPersistenceActor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 Contributors to the Eclipse Foundation + * Copyright (c) 2017 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. From ff4666258788c68df85629e7b48c5e4194e47273 Mon Sep 17 00:00:00 2001 From: Thomas Jaeckle Date: Mon, 19 Dec 2022 11:54:48 +0100 Subject: [PATCH 65/65] adjust since tags in javadoc to Ditto version 3.2.0 Signed-off-by: Thomas Jaeckle --- .../ditto/connectivity/model/AbstractConnection.java | 5 +++-- .../connectivity/model/AbstractConnectionBuilder.java | 5 +++-- .../eclipse/ditto/connectivity/model/ConnectionType.java | 2 +- .../eclipse/ditto/connectivity/model/ConnectionUri.java | 4 +++- .../eclipse/ditto/connectivity/model/HonoAddressAlias.java | 7 ++++--- .../eclipse/ditto/connectivity/model/HonoConnection.java | 5 +++-- .../ditto/connectivity/model/UserPasswordCredentials.java | 2 +- .../commands/query/RetrieveResolvedHonoConnection.java | 2 +- .../strategies/commands/ConnectionCreatedStrategies.java | 2 -- .../commands/RetrieveResolvedHonoConnectionStrategy.java | 2 +- 10 files changed, 20 insertions(+), 16 deletions(-) diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java index d984dfe8d7..eea09317d5 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnection.java @@ -42,8 +42,9 @@ import org.eclipse.ditto.json.JsonValue; /** - * Abstract implementation for common aspects of - * {@link org.eclipse.ditto.connectivity.model.Connection}. + * Abstract implementation for common aspects of {@link Connection}. + * + * @since 3.2.0 */ abstract class AbstractConnection implements Connection { diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java index 58363d0ab1..5bf00d4797 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/AbstractConnectionBuilder.java @@ -29,8 +29,9 @@ import javax.annotation.concurrent.Immutable; /** - * Abstract implementation for common aspects of - * {@link org.eclipse.ditto.connectivity.model.ConnectionBuilder}. + * Abstract implementation for common aspects of {@link ConnectionBuilder}. + * + * @since 3.2.0 */ @Immutable diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java index 3ee56f53f7..a6d1a29983 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionType.java @@ -54,7 +54,7 @@ public enum ConnectionType implements CharSequence { /** * Indicates a connection to Eclipse Hono. - * @since 3.1.0 + * @since 3.2.0 */ HONO("hono"); diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java index 9b4e176e55..ec75d683f2 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/ConnectionUri.java @@ -22,7 +22,9 @@ import javax.annotation.concurrent.Immutable; /** - * Represents a uri within the Connectivity service. + * Represents an uri within the Connectivity service. + * + * @since 3.2.0 */ @Immutable final class ConnectionUri { diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java index 574e97ea9e..27c3ffe890 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoAddressAlias.java @@ -22,8 +22,9 @@ import javax.annotation.Nullable; /** - * Possible address aliases used by connections of type 'Hono' - * @since 3.1.0 + * Possible address aliases used by connections of type 'Hono'. + * + * @since 3.2.0 */ public enum HonoAddressAlias { @@ -55,7 +56,7 @@ public enum HonoAddressAlias { private final String value; - private HonoAddressAlias(final String value) { + HonoAddressAlias(final String value) { this.value = value; } diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java index e2c6f3bcf9..e92ff37a86 100755 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/HonoConnection.java @@ -24,8 +24,9 @@ import org.eclipse.ditto.json.JsonParseException; /** - * Immutable implementation of {@link org.eclipse.ditto.connectivity.model.AbstractConnection} of type - * {@link org.eclipse.ditto.connectivity.model.ConnectionType} HONO. + * Immutable implementation of {@link AbstractConnection} of {@link ConnectionType} HONO. + * + * @since 3.2.0 */ @Immutable final class HonoConnection extends AbstractConnection { diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java index fb49e72a33..89a7da98ff 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/UserPasswordCredentials.java @@ -115,7 +115,7 @@ public static UserPasswordCredentials newInstance(final String username, final S * * @param jsonObject the jsonObject * @return credentials. - * @since 3.1.0 + * @since 3.2.0 */ public static UserPasswordCredentials newInstance(final JsonObject jsonObject) { return UserPasswordCredentials.fromJson(jsonObject); diff --git a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnection.java b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnection.java index 8d4ba579c8..f4ba9a5367 100644 --- a/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnection.java +++ b/connectivity/model/src/main/java/org/eclipse/ditto/connectivity/model/signals/commands/query/RetrieveResolvedHonoConnection.java @@ -38,7 +38,7 @@ * Command which retrieves a {@link org.eclipse.ditto.connectivity.model.Connection} of type 'hono' * after resolving its aliases and with its additional properties like header mappings and specific config. * - * @since 3.1.0 + * @since 3.2.0 */ @Immutable @JsonParsableCommand(typePrefix = ConnectivityCommand.TYPE_PREFIX, name = RetrieveResolvedHonoConnection.NAME) diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java index be0a69fe28..161f1988d1 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/ConnectionCreatedStrategies.java @@ -15,9 +15,7 @@ import javax.annotation.Nullable; import org.eclipse.ditto.base.model.signals.commands.Command; -import org.eclipse.ditto.connectivity.api.commands.sudo.ConnectivitySudoCommand; import org.eclipse.ditto.connectivity.model.Connection; -import org.eclipse.ditto.connectivity.model.signals.commands.ConnectivityCommand; import org.eclipse.ditto.connectivity.model.signals.commands.exceptions.ConnectionNotAccessibleException; import org.eclipse.ditto.connectivity.model.signals.events.ConnectivityEvent; import org.eclipse.ditto.connectivity.service.messaging.persistence.stages.ConnectionState; diff --git a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveResolvedHonoConnectionStrategy.java b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveResolvedHonoConnectionStrategy.java index 579605b18b..d997d23cb4 100644 --- a/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveResolvedHonoConnectionStrategy.java +++ b/connectivity/service/src/main/java/org/eclipse/ditto/connectivity/service/messaging/persistence/strategies/commands/RetrieveResolvedHonoConnectionStrategy.java @@ -28,7 +28,7 @@ import akka.actor.ActorSystem; /** - * This strategy handles the {@link org.eclipse.ditto.connectivity.model.signals.commands.query.RetrieveResolvedHonoConnection} command. + * This strategy handles the {@link RetrieveResolvedHonoConnection} command. */ final class RetrieveResolvedHonoConnectionStrategy extends AbstractConnectivityCommandStrategy {