diff options
Diffstat (limited to 'message-bus')
92 files changed, 10606 insertions, 0 deletions
diff --git a/message-bus/pom.xml b/message-bus/pom.xml new file mode 100644 index 00000000..54724798 --- /dev/null +++ b/message-bus/pom.xml @@ -0,0 +1,142 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ============LICENSE_START======================================================= + ONAP Policy Engine - Common Modules + ================================================================================ + Copyright (C) 2024 Nordix Foundation. + ================================================================================ + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ============LICENSE_END========================================================= + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.onap.policy.common</groupId> + <artifactId>common-modules</artifactId> + <version>3.0.1-SNAPSHOT</version> + </parent> + + <artifactId>message-bus</artifactId> + + <properties> + <maven.compiler.source>17</maven.compiler.source> + <maven.compiler.target>17</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>capabilities</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>common-parameters</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>gson</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>utils</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + </dependency> + <dependency> + <groupId>com.google.re2j</groupId> + <artifactId>re2j</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>io.opentelemetry</groupId> + <artifactId>opentelemetry-api</artifactId> + </dependency> + <dependency> + <groupId>io.opentelemetry</groupId> + <artifactId>opentelemetry-context</artifactId> + </dependency> + <dependency> + <groupId>io.opentelemetry.instrumentation</groupId> + <artifactId>opentelemetry-kafka-clients-2.6</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.kafka</groupId> + <artifactId>kafka-clients</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>utils-test</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> +</project>
\ No newline at end of file diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/Topic.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/Topic.java new file mode 100644 index 00000000..48dbb715 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/Topic.java @@ -0,0 +1,90 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019 Samsung Electronics Co., Ltd. + * Copyright (C) 2022,2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import java.util.List; +import org.onap.policy.common.capabilities.Lockable; +import org.onap.policy.common.capabilities.Startable; + + +/** + * Essential Topic Data. + */ +public interface Topic extends TopicRegisterable, Startable, Lockable { + + /** + * Underlying Communication infrastructure Types. + */ + enum CommInfrastructure { + /** + * KAFKA Communication Infrastructure. + */ + KAFKA, + /** + * NOOP for internal use only. + */ + NOOP, + /** + * REST Communication Infrastructure. + */ + REST + } + + /** + * Gets the canonical topic name. + * + * @return topic name + */ + String getTopic(); + + /** + * Gets the effective topic that is used in + * the network communication. This name is usually + * the topic name. + * + * @return topic name alias + */ + String getEffectiveTopic(); + + /** + * Gets the communication infrastructure type. + * + * @return CommInfrastructure object + */ + CommInfrastructure getTopicCommInfrastructure(); + + /** + * Return list of servers. + * + * @return bus servers + */ + List<String> getServers(); + + /** + * Get the more recent events in this topic entity. + * + * @return array of most recent events + */ + String[] getRecentEvents(); + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpoint.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpoint.java new file mode 100644 index 00000000..5511a82c --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpoint.java @@ -0,0 +1,238 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Copyright (C) 2022,2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.capabilities.Lockable; +import org.onap.policy.common.capabilities.Startable; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicSink; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicSource; +import org.onap.policy.common.message.bus.event.noop.NoopTopicSink; +import org.onap.policy.common.message.bus.event.noop.NoopTopicSource; +import org.onap.policy.common.parameters.topic.TopicParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; + +/** + * Abstraction to manage the system's Networked Topic Endpoints, sources of all events input into + * the System. + */ +public interface TopicEndpoint extends Startable, Lockable { + + /** + * Add topics configuration (sources and sinks) into a single list. + * + * @param properties topic configuration + * @return topic list + * @throws IllegalArgumentException when invalid arguments are provided + */ + List<Topic> addTopics(Properties properties); + + /** + * Add topics configuration (sources and sinks) into a single list. + * + * @param params parameters to configure topic + * @return topic list + * @throws IllegalArgumentException when invalid arguments are provided + */ + List<Topic> addTopics(TopicParameterGroup params); + + /** + * Add Topic Sources to the communication infrastructure initialized per properties. + * + * @param properties properties for Topic Source construction + * @return a list of generic Topic Sources + * @throws IllegalArgumentException when invalid arguments are provided + */ + List<TopicSource> addTopicSources(Properties properties); + + + /** + * Add Topic Sources to the communication infrastructure initialized per properties. + * + * @param paramList parameters for Topic Source construction + * @return a list of generic Topic Sources + * @throws IllegalArgumentException when invalid arguments are provided + */ + List<TopicSource> addTopicSources(List<TopicParameters> paramList); + + /** + * Add Topic Sinks to the communication infrastructure initialized per properties. + * + * @param properties properties for Topic Sink construction + * @return a list of generic Topic Sinks + * @throws IllegalArgumentException when invalid arguments are provided + */ + List<TopicSink> addTopicSinks(Properties properties); + + /** + * Add Topic Sinks to the communication infrastructure initialized per properties. + * + * @param paramList parameters for Topic Sink construction + * @return a list of generic Topic Sinks + * @throws IllegalArgumentException when invalid arguments are provided + */ + List<TopicSink> addTopicSinks(List<TopicParameters> paramList); + + /** + * Gets all Topic Sources. + * + * @return the Topic Source List + */ + List<TopicSource> getTopicSources(); + + /** + * Get the Topic Sources for the given topic name. + * + * @param topicNames the topic name + * + * @return the Topic Source List + * @throws IllegalStateException if the entity is in an invalid state + * @throws IllegalArgumentException if invalid parameters are present + */ + List<TopicSource> getTopicSources(List<String> topicNames); + + /** + * Gets the Topic Source for the given topic name and underlying communication infrastructure + * type. + * + * @param commType communication infrastructure type + * @param topicName the topic name + * + * @return the Topic Source + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicReaders for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + * @throws UnsupportedOperationException if the operation is not supported. + */ + TopicSource getTopicSource(Topic.CommInfrastructure commType, String topicName); + + /** + * Get the Noop Source for the given topic name. + * + * @param topicName the topic name. + * @return the Noop Source. + */ + NoopTopicSource getNoopTopicSource(String topicName); + + /** + * Get the Kafka Source for the given topic name. + * + * @param topicName the topic name. + * @return the Kafka Source. + */ + KafkaTopicSource getKafkaTopicSource(String topicName); + + /** + * Get the Topic Sinks for the given topic name. + * + * @param topicNames the topic names + * @return the Topic Sink List + */ + List<TopicSink> getTopicSinks(List<String> topicNames); + + /** + * Get the Topic Sinks for the given topic name and all the underlying communication + * infrastructure type. + * + * @param topicName the topic name + * + * @return the Topic Sink List + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicWriters for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + */ + List<TopicSink> getTopicSinks(String topicName); + + /** + * Gets all Topic Sinks. + * + * @return the Topic Sink List + */ + List<TopicSink> getTopicSinks(); + + /** + * Get the Topic Sinks for the given topic name and underlying communication infrastructure type. + * + * @param topicName the topic name + * @param commType communication infrastructure type + * + * @return the Topic Sink List + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicWriters for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + */ + TopicSink getTopicSink(Topic.CommInfrastructure commType, String topicName); + + /** + * Get the no-op Topic Sink for the given topic name. + * + * @param topicName the topic name + * + * @return the Topic Source + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicReaders for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + */ + NoopTopicSink getNoopTopicSink(String topicName); + + /** + * Get the KAFKA Topic Source for the given topic name. + * + * @param topicName the topic name + * + * @return the Topic Source + * @throws IllegalStateException if the entity is in an invalid state, for example multiple + * TopicReaders for a topic name and communication infrastructure + * @throws IllegalArgumentException if invalid parameters are present + */ + KafkaTopicSink getKafkaTopicSink(String topicName); + + /** + * Gets only the KAFKA Topic Sources. + * + * @return the KAFKA Topic Source List + */ + List<KafkaTopicSource> getKafkaTopicSources(); + + /** + * Gets only the NOOP Topic Sources. + * + * @return the NOOP Topic Source List + */ + List<NoopTopicSource> getNoopTopicSources(); + + /** + * Gets only the KAFKA Topic Sinks. + * + * @return the KAFKA Topic Sinks List + */ + List<KafkaTopicSink> getKafkaTopicSinks(); + + /** + * Gets only the NOOP Topic Sinks. + * + * @return the NOOP Topic Sinks List + */ + List<NoopTopicSink> getNoopTopicSinks(); + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointManager.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointManager.java new file mode 100644 index 00000000..40b9c235 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointManager.java @@ -0,0 +1,36 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TopicEndpointManager { + + /** + * Topic endpoint manager. + */ + @Getter + static TopicEndpoint manager = new TopicEndpointProxy(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointProxy.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointProxy.java new file mode 100644 index 00000000..9dbf5418 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointProxy.java @@ -0,0 +1,485 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import lombok.Getter; +import org.onap.policy.common.capabilities.Startable; +import org.onap.policy.common.gson.annotation.GsonJsonIgnore; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicFactories; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicSink; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicSource; +import org.onap.policy.common.message.bus.event.noop.NoopTopicFactories; +import org.onap.policy.common.message.bus.event.noop.NoopTopicSink; +import org.onap.policy.common.message.bus.event.noop.NoopTopicSource; +import org.onap.policy.common.parameters.topic.TopicParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This implementation of the Topic Endpoint Manager, proxies operations to the appropriate + * implementation(s). + */ +@Getter +public class TopicEndpointProxy implements TopicEndpoint { + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(TopicEndpointProxy.class); + + /** + * Is this element locked boolean. + */ + private volatile boolean locked = false; + + /** + * Is this element alive boolean. + */ + private volatile boolean alive = false; + + @Override + public List<Topic> addTopics(Properties properties) { + List<Topic> topics = new ArrayList<>(addTopicSources(properties)); + topics.addAll(addTopicSinks(properties)); + return topics; + } + + @Override + public List<Topic> addTopics(TopicParameterGroup params) { + List<TopicParameters> sinks = + (params.getTopicSinks() != null ? params.getTopicSinks() : Collections.emptyList()); + List<TopicParameters> sources = + (params.getTopicSources() != null ? params.getTopicSources() : Collections.emptyList()); + + List<Topic> topics = new ArrayList<>(sinks.size() + sources.size()); + topics.addAll(addTopicSources(sources)); + topics.addAll(addTopicSinks(sinks)); + return topics; + } + + @Override + public List<TopicSource> addTopicSources(List<TopicParameters> paramList) { + List<TopicSource> sources = new ArrayList<>(paramList.size()); + + for (TopicParameters param : paramList) { + switch (Topic.CommInfrastructure.valueOf(param.getTopicCommInfrastructure().toUpperCase())) { + case KAFKA: + sources.add(KafkaTopicFactories.getSourceFactory().build(param)); + break; + case NOOP: + sources.add(NoopTopicFactories.getSourceFactory().build(param)); + break; + default: + logger.debug("Unknown source type {} for topic: {}", param.getTopicCommInfrastructure(), + param.getTopic()); + break; + } + } + + lockSources(sources); + + return sources; + } + + @Override + public List<TopicSource> addTopicSources(Properties properties) { + + // 1. Create KAFKA Sources + // 2. Create NOOP Sources + + List<TopicSource> sources = new ArrayList<>(); + + sources.addAll(KafkaTopicFactories.getSourceFactory().build(properties)); + sources.addAll(NoopTopicFactories.getSourceFactory().build(properties)); + + lockSources(sources); + + return sources; + } + + private void lockSources(List<TopicSource> sources) { + if (this.isLocked()) { + sources.forEach(TopicSource::lock); + } + } + + @Override + public List<TopicSink> addTopicSinks(List<TopicParameters> paramList) { + List<TopicSink> sinks = new ArrayList<>(paramList.size()); + + for (TopicParameters param : paramList) { + switch (Topic.CommInfrastructure.valueOf(param.getTopicCommInfrastructure().toUpperCase())) { + case KAFKA: + sinks.add(KafkaTopicFactories.getSinkFactory().build(param)); + break; + case NOOP: + sinks.add(NoopTopicFactories.getSinkFactory().build(param)); + break; + default: + logger.debug("Unknown sink type {} for topic: {}", param.getTopicCommInfrastructure(), + param.getTopic()); + break; + } + } + + lockSinks(sinks); + + return sinks; + } + + @Override + public List<TopicSink> addTopicSinks(Properties properties) { + // 1. Create KAFKA Sinks + // 2. Create NOOP Sinks + + final List<TopicSink> sinks = new ArrayList<>(); + + sinks.addAll(KafkaTopicFactories.getSinkFactory().build(properties)); + sinks.addAll(NoopTopicFactories.getSinkFactory().build(properties)); + + lockSinks(sinks); + + return sinks; + } + + private void lockSinks(List<TopicSink> sinks) { + if (this.isLocked()) { + sinks.forEach(TopicSink::lock); + } + } + + @Override + public List<TopicSource> getTopicSources() { + + final List<TopicSource> sources = new ArrayList<>(); + + sources.addAll(KafkaTopicFactories.getSourceFactory().inventory()); + sources.addAll(NoopTopicFactories.getSourceFactory().inventory()); + + return sources; + } + + @Override + public List<TopicSource> getTopicSources(List<String> topicNames) { + + if (topicNames == null) { + throw new IllegalArgumentException("must provide a list of topics"); + } + + final List<TopicSource> sources = new ArrayList<>(); + + topicNames.forEach(topic -> { + try { + sources.add(Objects.requireNonNull(this.getKafkaTopicSource(topic))); + } catch (final Exception e) { + logger.debug("No KAFKA source for topic: {}", topic, e); + } + + try { + sources.add(Objects.requireNonNull(this.getNoopTopicSource(topic))); + } catch (final Exception e) { + logger.debug("No NOOP source for topic: {}", topic, e); + } + }); + + return sources; + } + + @Override + public List<TopicSink> getTopicSinks() { + + final List<TopicSink> sinks = new ArrayList<>(); + + sinks.addAll(KafkaTopicFactories.getSinkFactory().inventory()); + sinks.addAll(NoopTopicFactories.getSinkFactory().inventory()); + + return sinks; + } + + @Override + public List<TopicSink> getTopicSinks(List<String> topicNames) { + + if (topicNames == null) { + throw new IllegalArgumentException("must provide a list of topics"); + } + + final List<TopicSink> sinks = new ArrayList<>(); + for (final String topic : topicNames) { + try { + sinks.add(Objects.requireNonNull(this.getKafkaTopicSink(topic))); + } catch (final Exception e) { + logger.debug("No KAFKA sink for topic: {}", topic, e); + } + + try { + sinks.add(Objects.requireNonNull(this.getNoopTopicSink(topic))); + } catch (final Exception e) { + logger.debug("No NOOP sink for topic: {}", topic, e); + } + } + return sinks; + } + + @Override + public List<TopicSink> getTopicSinks(String topicName) { + if (topicName == null) { + throw paramException(null); + } + + final List<TopicSink> sinks = new ArrayList<>(); + + try { + sinks.add(this.getKafkaTopicSink(topicName)); + } catch (final Exception e) { + logNoSink(topicName, e); + } + + try { + sinks.add(this.getNoopTopicSink(topicName)); + } catch (final Exception e) { + logNoSink(topicName, e); + } + + return sinks; + } + + @GsonJsonIgnore + @Override + public List<KafkaTopicSource> getKafkaTopicSources() { + return KafkaTopicFactories.getSourceFactory().inventory(); + } + + @GsonJsonIgnore + @Override + public List<NoopTopicSource> getNoopTopicSources() { + return NoopTopicFactories.getSourceFactory().inventory(); + } + + @Override + @GsonJsonIgnore + public List<KafkaTopicSink> getKafkaTopicSinks() { + return KafkaTopicFactories.getSinkFactory().inventory(); + } + + @GsonJsonIgnore + @Override + public List<NoopTopicSink> getNoopTopicSinks() { + return NoopTopicFactories.getSinkFactory().inventory(); + } + + @Override + public boolean start() { + + synchronized (this) { + if (this.locked) { + throw new IllegalStateException(this + " is locked"); + } + + if (this.alive) { + return true; + } + + this.alive = true; + } + + final List<Startable> endpoints = this.getEndpoints(); + + var success = true; + for (final Startable endpoint : endpoints) { + try { + success = endpoint.start() && success; + } catch (final Exception e) { + success = false; + logger.error("Problem starting endpoint: {}", endpoint, e); + } + } + + return success; + } + + @Override + public boolean stop() { + + /* + * stop regardless if it is locked, in other words, stop operation has precedence over + * locks. + */ + synchronized (this) { + this.alive = false; + } + + final List<Startable> endpoints = this.getEndpoints(); + + var success = true; + for (final Startable endpoint : endpoints) { + try { + success = endpoint.stop() && success; + } catch (final Exception e) { + success = false; + logger.error("Problem stopping endpoint: {}", endpoint, e); + } + } + + return success; + } + + /** + * Gets the endpoints. + * + * @return list of managed endpoints + */ + @GsonJsonIgnore + protected List<Startable> getEndpoints() { + final List<Startable> endpoints = new ArrayList<>(); + + endpoints.addAll(this.getTopicSources()); + endpoints.addAll(this.getTopicSinks()); + + return endpoints; + } + + @Override + public void shutdown() { + this.stop(); + + KafkaTopicFactories.getSourceFactory().destroy(); + KafkaTopicFactories.getSinkFactory().destroy(); + + NoopTopicFactories.getSinkFactory().destroy(); + NoopTopicFactories.getSourceFactory().destroy(); + + } + + @Override + public boolean lock() { + boolean shouldLock; + + synchronized (this) { + shouldLock = !this.locked; + this.locked = true; + } + + if (shouldLock) { + for (final TopicSource source : this.getTopicSources()) { + source.lock(); + } + + for (final TopicSink sink : this.getTopicSinks()) { + sink.lock(); + } + } + + return true; + } + + @Override + public boolean unlock() { + boolean shouldUnlock; + + synchronized (this) { + shouldUnlock = this.locked; + this.locked = false; + } + + if (shouldUnlock) { + for (final TopicSource source : this.getTopicSources()) { + source.unlock(); + } + + for (final TopicSink sink : this.getTopicSinks()) { + sink.unlock(); + } + } + + return true; + } + + @Override + public TopicSource getTopicSource(Topic.CommInfrastructure commType, String topicName) { + + if (commType == null) { + throw paramException(topicName); + } + + if (topicName == null) { + throw paramException(null); + } + + return switch (commType) { + case KAFKA -> this.getKafkaTopicSource(topicName); + case NOOP -> this.getNoopTopicSource(topicName); + default -> throw new UnsupportedOperationException("Unsupported " + commType.name()); + }; + } + + @Override + public TopicSink getTopicSink(Topic.CommInfrastructure commType, String topicName) { + if (commType == null) { + throw paramException(topicName); + } + + if (topicName == null) { + throw paramException(null); + } + + return switch (commType) { + case KAFKA -> this.getKafkaTopicSink(topicName); + case NOOP -> this.getNoopTopicSink(topicName); + default -> throw new UnsupportedOperationException("Unsupported " + commType.name()); + }; + } + + @Override + public KafkaTopicSource getKafkaTopicSource(String topicName) { + return KafkaTopicFactories.getSourceFactory().get(topicName); + } + + @Override + public NoopTopicSource getNoopTopicSource(String topicName) { + return NoopTopicFactories.getSourceFactory().get(topicName); + } + + @Override + public KafkaTopicSink getKafkaTopicSink(String topicName) { + return KafkaTopicFactories.getSinkFactory().get(topicName); + } + + @Override + public NoopTopicSink getNoopTopicSink(String topicName) { + return NoopTopicFactories.getSinkFactory().get(topicName); + } + + private IllegalArgumentException paramException(String topicName) { + return new IllegalArgumentException( + "Invalid parameter: a communication infrastructure required to fetch " + topicName); + } + + private void logNoSink(String topicName, Exception ex) { + logger.debug("No sink for topic: {}", topicName, ex); + } + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicListener.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicListener.java new file mode 100644 index 00000000..f9896243 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicListener.java @@ -0,0 +1,37 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +/** + * Listener for event messages entering the Policy Engine. + */ +@FunctionalInterface +public interface TopicListener { + + /** + * Notification of a new Event over a given Topic. + * + * @param commType communication infrastructure type + * @param topic topic name + * @param event event message as a string + */ + void onTopicEvent(Topic.CommInfrastructure commType, String topic, String event); + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicRegisterable.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicRegisterable.java new file mode 100644 index 00000000..1d6873e9 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicRegisterable.java @@ -0,0 +1,41 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +/** + * Marks a Topic entity as registerable. + */ +public interface TopicRegisterable { + + /** + * Register for notification of events with this Topic Entity. + * + * @param topicListener the listener of events + */ + void register(TopicListener topicListener); + + /** + * Unregisters for notification of events with this Topic Entity. + * + * @param topicListener the listener of events + */ + void unregister(TopicListener topicListener); + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicSink.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicSink.java new file mode 100644 index 00000000..d5269b9a --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicSink.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +/** + * Marks a given Topic Endpoint as able to send messages over a topic. + */ +public interface TopicSink extends Topic { + + /** + * Sends a string message over this Topic Endpoint. + * + * @param message message to send + * @return true if the send operation succeeded, false otherwise + * @throws IllegalArgumentException an invalid message has been provided + * @throws IllegalStateException the entity is in a state that prevents + * it from sending messages, for example, locked or stopped. + */ + boolean send(String message); + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicSource.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicSource.java new file mode 100644 index 00000000..f0dc3b74 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/TopicSource.java @@ -0,0 +1,36 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +/** + * Marker for a Topic Entity, indicating that the entity is able to read + * over a topic. + */ +public interface TopicSource extends Topic { + + /** + * Pushes an event into the source programmatically. + * + * @param event the event in json format + * @return true if it can be processed correctly, false otherwise + */ + boolean offer(String event); + +}
\ No newline at end of file diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/ApiKeyEnabled.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/ApiKeyEnabled.java new file mode 100644 index 00000000..360a88a2 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/ApiKeyEnabled.java @@ -0,0 +1,39 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +/** + * API. + */ +public interface ApiKeyEnabled { + /** + * Get API key. + * + * @return api key + */ + String getApiKey(); + + /** + * Get API secret. + * + * @return api secret + */ + String getApiSecret(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusConsumer.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusConsumer.java new file mode 100644 index 00000000..925949aa --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusConsumer.java @@ -0,0 +1,278 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2018 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020,2023 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.kafkaclients.v2_6.TracingConsumerInterceptor; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.header.Headers; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wrapper around libraries to consume from message bus. + */ +public interface BusConsumer { + + /** + * fetch messages. + * + * @return list of messages + * @throws IOException when error encountered by underlying libraries + */ + Iterable<String> fetch() throws IOException; + + /** + * close underlying library consumer. + */ + void close(); + + /** + * Consumer that handles fetch() failures by sleeping. + */ + abstract class FetchingBusConsumer implements BusConsumer { + private static final Logger logger = LoggerFactory.getLogger(FetchingBusConsumer.class); + + /** + * Fetch timeout. + */ + protected int fetchTimeout; + + /** + * Time to sleep on a fetch failure. + */ + @Getter + private final int sleepTime; + + /** + * Counted down when {@link #close()} is invoked. + */ + private final CountDownLatch closeCondition = new CountDownLatch(1); + + + /** + * Constructs the object. + * + * @param busTopicParams parameters for the bus topic + */ + protected FetchingBusConsumer(BusTopicParams busTopicParams) { + this.fetchTimeout = busTopicParams.getFetchTimeout(); + + if (this.fetchTimeout <= 0) { + this.sleepTime = DEFAULT_TIMEOUT_MS_FETCH; + } else { + // don't sleep too long, even if fetch timeout is large + this.sleepTime = Math.min(this.fetchTimeout, DEFAULT_TIMEOUT_MS_FETCH); + } + } + + /** + * Causes the thread to sleep; invoked after fetch() fails. If the consumer is closed, + * or the thread is interrupted, then this will return immediately. + */ + protected void sleepAfterFetchFailure() { + try { + logger.info("{}: backoff for {}ms", this, sleepTime); + if (this.closeCondition.await(this.sleepTime, TimeUnit.MILLISECONDS)) { + logger.info("{}: closed while handling fetch error", this); + } + + } catch (InterruptedException e) { + logger.warn("{}: interrupted while handling fetch error", this, e); + Thread.currentThread().interrupt(); + } + } + + @Override + public void close() { + this.closeCondition.countDown(); + } + } + + /** + * Kafka based consumer. + */ + class KafkaConsumerWrapper extends FetchingBusConsumer { + + /** + * logger. + */ + private static final Logger logger = LoggerFactory.getLogger(KafkaConsumerWrapper.class); + + private static final String KEY_DESERIALIZER = "org.apache.kafka.common.serialization.StringDeserializer"; + + /** + * Kafka consumer. + */ + protected KafkaConsumer<String, String> consumer; + protected Properties kafkaProps; + + protected boolean allowTracing; + + /** + * Kafka Consumer Wrapper. + * BusTopicParam - object contains the following parameters + * servers - messaging bus hosts. + * topic - topic + * + * @param busTopicParams - The parameters for the bus topic + */ + public KafkaConsumerWrapper(BusTopicParams busTopicParams) { + super(busTopicParams); + + if (busTopicParams.isTopicInvalid()) { + throw new IllegalArgumentException("No topic for Kafka"); + } + + //Setup Properties for consumer + kafkaProps = new Properties(); + kafkaProps.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, + busTopicParams.getServers().get(0)); + + if (busTopicParams.isAdditionalPropsValid()) { + kafkaProps.putAll(busTopicParams.getAdditionalProps()); + } + + if (kafkaProps.get(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG) == null) { + kafkaProps.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, KEY_DESERIALIZER); + } + if (kafkaProps.get(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG) == null) { + kafkaProps.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, KEY_DESERIALIZER); + } + if (kafkaProps.get(ConsumerConfig.GROUP_ID_CONFIG) == null) { + kafkaProps.setProperty(ConsumerConfig.GROUP_ID_CONFIG, busTopicParams.getConsumerGroup()); + } + if (busTopicParams.isAllowTracing()) { + this.allowTracing = true; + kafkaProps.setProperty(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, + TracingConsumerInterceptor.class.getName()); + } + + consumer = new KafkaConsumer<>(kafkaProps); + //Subscribe to the topic + consumer.subscribe(List.of(busTopicParams.getTopic())); + } + + @Override + public Iterable<String> fetch() { + ConsumerRecords<String, String> records = this.consumer.poll(Duration.ofMillis(fetchTimeout)); + if (records == null || records.count() <= 0) { + return Collections.emptyList(); + } + List<String> messages = new ArrayList<>(records.count()); + try { + if (allowTracing) { + createParentTraceContext(records); + } + + for (TopicPartition partition : records.partitions()) { + List<ConsumerRecord<String, String>> partitionRecords = records.records(partition); + for (ConsumerRecord<String, String> partitionRecord : partitionRecords) { + messages.add(partitionRecord.value()); + } + long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset(); + consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1))); + } + } catch (Exception e) { + logger.error("{}: cannot fetch, throwing exception after sleep...", this); + sleepAfterFetchFailure(); + throw e; + } + return messages; + } + + private void createParentTraceContext(ConsumerRecords<String, String> records) { + TraceParentInfo traceParentInfo = new TraceParentInfo(); + for (ConsumerRecord<String, String> consumerRecord : records) { + + Headers consumerRecordHeaders = consumerRecord.headers(); + traceParentInfo = processTraceParentHeader(consumerRecordHeaders); + } + + SpanContext spanContext = SpanContext.createFromRemoteParent( + traceParentInfo.getTraceId(), traceParentInfo.getSpanId(), + TraceFlags.getSampled(), TraceState.builder().build()); + + Context.current().with(Span.wrap(spanContext)).makeCurrent(); + } + + private TraceParentInfo processTraceParentHeader(Headers headers) { + TraceParentInfo traceParentInfo = new TraceParentInfo(); + if (headers.lastHeader("traceparent") != null) { + traceParentInfo.setParentTraceId(new String(headers.lastHeader( + "traceparent").value(), StandardCharsets.UTF_8)); + + String[] parts = traceParentInfo.getParentTraceId().split("-"); + traceParentInfo.setTraceId(parts[1]); + traceParentInfo.setSpanId(parts[2]); + } + + return traceParentInfo; + } + + @Data + @NoArgsConstructor + private static class TraceParentInfo { + private String parentTraceId; + private String traceId; + private String spanId; + } + + @Override + public void close() { + super.close(); + this.consumer.close(); + logger.info("Kafka Consumer exited {}", this); + } + + @Override + public String toString() { + return "KafkaConsumerWrapper [fetchTimeout=" + fetchTimeout + "]"; + } + } +} + + diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusPublisher.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusPublisher.java new file mode 100644 index 00000000..10c7db2d --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusPublisher.java @@ -0,0 +1,43 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2018 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020,2023 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +public interface BusPublisher { + + String NO_MESSAGE_PROVIDED = "No message provided"; + String LOG_CLOSE = "{}: CLOSE"; + + /** + * sends a message. + * + * @param partitionId id + * @param message the message + * @return true if success, false otherwise + * @throws IllegalArgumentException if no message provided + */ + boolean send(String partitionId, String message); + + /** + * closes the publisher. + */ + void close(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicBase.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicBase.java new file mode 100644 index 00000000..6516945a --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicBase.java @@ -0,0 +1,109 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import lombok.Getter; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Bus Topic Base. + */ +@Getter +public abstract class BusTopicBase extends TopicBase implements ApiKeyEnabled { + + /** + * API Key. + */ + protected String apiKey; + + /** + * API Secret. + */ + protected String apiSecret; + + /** + * Use https. + */ + protected boolean useHttps; + + /** + * Allow tracing. + */ + protected boolean allowTracing; + + /** + * allow self-signed certificates. + */ + protected boolean allowSelfSignedCerts; + + /** + * Instantiates a new Bus Topic Base. + * + * <p>servers list of servers + * topic: the topic name + * apiKey: API Key + * apiSecret: API Secret + * useHttps: does connection use HTTPS? + * allowTracing: Is tracing allowed? + * allowSelfSignedCerts: are self-signed certificates allow + * + * @param busTopicParams holds all our parameters + * @throws IllegalArgumentException if invalid parameters are present + */ + protected BusTopicBase(BusTopicParams busTopicParams) { + super(busTopicParams.getServers(), busTopicParams.getTopic(), busTopicParams.getEffectiveTopic()); + this.apiKey = busTopicParams.getApiKey(); + this.apiSecret = busTopicParams.getApiSecret(); + this.useHttps = busTopicParams.isUseHttps(); + this.allowTracing = busTopicParams.isAllowTracing(); + this.allowSelfSignedCerts = busTopicParams.isAllowSelfSignedCerts(); + } + + protected boolean anyNullOrEmpty(String... args) { + for (String arg : args) { + if (arg == null || arg.isEmpty()) { + return true; + } + } + + return false; + } + + protected boolean allNullOrEmpty(String... args) { + for (String arg : args) { + if (!(arg == null || arg.isEmpty())) { + return false; + } + } + + return true; + } + + + @Override + public String toString() { + return "BusTopicBase [apiKey=" + apiKey + ", apiSecret=" + apiSecret + ", useHttps=" + useHttps + + ", allowSelfSignedCerts=" + allowSelfSignedCerts + ", toString()=" + super.toString() + "]"; + } + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSink.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSink.java new file mode 100644 index 00000000..54b08619 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSink.java @@ -0,0 +1,42 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017, 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import org.onap.policy.common.message.bus.event.TopicSink; + +/** + * Topic Sink over Bus Infrastructure (KAFKA). + */ +public interface BusTopicSink extends ApiKeyEnabled, TopicSink { + + /** + * Sets the partition key for published messages. + * + * @param partitionKey the partition key + */ + void setPartitionKey(String partitionKey); + + /** + * Return the partition key in used by the system to publish messages. + * + * @return the partition key + */ + String getPartitionKey(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSource.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSource.java new file mode 100644 index 00000000..974c02bb --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSource.java @@ -0,0 +1,57 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import org.onap.policy.common.message.bus.event.TopicSource; + +/** + * Generic Topic Source for Bus Communication Infrastructure. + * + */ +public interface BusTopicSource extends ApiKeyEnabled, TopicSource { + + /** + * Gets the consumer group. + * + * @return consumer group + */ + public String getConsumerGroup(); + + /** + * Gets the consumer instance. + * + * @return consumer instance + */ + public String getConsumerInstance(); + + /** + * Gets the fetch timeout. + * + * @return fetch timeout + */ + public int getFetchTimeout(); + + /** + * Gets the fetch limit. + * + * @return fetch limit + */ + public int getFetchLimit(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSink.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSink.java new file mode 100644 index 00000000..6a30f00f --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSink.java @@ -0,0 +1,165 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2018-2019 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. +* ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Transport Agnostic Bus Topic Sink to carry out the core functionality to interact with a sink. + * + */ +public abstract class InlineBusTopicSink extends BusTopicBase implements BusTopicSink { + + /** + * Loggers. + */ + private static final Logger logger = LoggerFactory.getLogger(InlineBusTopicSink.class); + + /** + * The partition key to publish to. + */ + @Getter + @Setter + protected String partitionKey; + + /** + * Message bus publisher. + */ + protected BusPublisher publisher; + + /** + * Constructor for abstract sink. + * @param busTopicParams contains below listed attributes + * servers: servers + * topic: topic + * apiKey: api secret + * apiSecret: api secret + * partitionId: partition id + * useHttps: does connection use HTTPS? + * allowTracing: is tracing allowed? + * allowSelfSignedCerts: are self-signed certificates allow * + * @throws IllegalArgumentException if invalid parameters are passed in + */ + protected InlineBusTopicSink(BusTopicParams busTopicParams) { + + super(busTopicParams); + + if (busTopicParams.isPartitionIdInvalid()) { + this.partitionKey = UUID.randomUUID().toString(); + } else { + this.partitionKey = busTopicParams.getPartitionId(); + } + } + + /** + * Initialize the Bus publisher. + */ + public abstract void init(); + + @Override + public boolean start() { + logger.info("{}: starting", this); + + synchronized (this) { + if (!this.alive) { + if (locked) { + throw new IllegalStateException(this + " is locked."); + } + + this.init(); + this.alive = true; + } + } + + return true; + } + + @Override + public boolean stop() { + + BusPublisher publisherCopy; + synchronized (this) { + this.alive = false; + publisherCopy = this.publisher; + this.publisher = null; + } + + if (publisherCopy != null) { + try { + publisherCopy.close(); + } catch (Exception e) { + logger.warn("{}: cannot stop publisher because of {}", this, e.getMessage(), e); + } + } else { + logger.warn("{}: there is no publisher", this); + return false; + } + + return true; + } + + @Override + public boolean send(String message) { + + if (message == null || message.isEmpty()) { + throw new IllegalArgumentException("Message to send is empty"); + } + + if (!this.alive) { + throw new IllegalStateException(this + " is stopped"); + } + + try { + synchronized (this) { + this.recentEvents.add(message); + } + + NetLoggerUtil.log(EventType.OUT, this.getTopicCommInfrastructure(), this.topic, message); + + publisher.send(this.partitionKey, message); + broadcast(message); + } catch (Exception e) { + logger.warn("{}: cannot send because of {}", this, e.getMessage(), e); + return false; + } + + return true; + } + + @Override + public void shutdown() { + this.stop(); + } + + @Override + public String toString() { + return "InlineBusTopicSink [partitionId=" + partitionKey + ", alive=" + alive + ", publisher=" + publisher + + "]"; + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSource.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSource.java new file mode 100644 index 00000000..912b698c --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSource.java @@ -0,0 +1,288 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2018-2019 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.NO_LIMIT_FETCH; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.NO_TIMEOUT_MS_FETCH; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.UUID; +import lombok.Getter; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.network.NetworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This topic source implementation specializes in reading messages over a bus topic source and + * notifying its listeners. + */ +public abstract class SingleThreadedBusTopicSource extends BusTopicBase + implements Runnable, BusTopicSource { + + /** + * Not to be converted to PolicyLogger. This will contain all in/out traffic and only + * that in a single file in a concise format. + */ + private static final Logger logger = LoggerFactory.getLogger(SingleThreadedBusTopicSource.class); + + /** + * Bus consumer group. + */ + @Getter + protected final String consumerGroup; + + /** + * Bus consumer instance. + */ + @Getter + protected final String consumerInstance; + + /** + * Bus fetch timeout. + */ + @Getter + protected final int fetchTimeout; + + /** + * Bus fetch limit. + */ + @Getter + protected final int fetchLimit; + + /** + * Message Bus Consumer. + */ + protected BusConsumer consumer; + + /** + * Independent thread reading message over my topic. + */ + protected Thread busPollerThread; + + + /** + * Constructor. + * + * @param busTopicParams topic parameters + * + * @throws IllegalArgumentException An invalid parameter passed in + */ + protected SingleThreadedBusTopicSource(BusTopicParams busTopicParams) { + + super(busTopicParams); + + if (busTopicParams.isConsumerGroupInvalid() && busTopicParams.isConsumerInstanceInvalid()) { + this.consumerGroup = UUID.randomUUID().toString(); + this.consumerInstance = NetworkUtil.getHostname(); + + } else if (busTopicParams.isConsumerGroupInvalid()) { + this.consumerGroup = UUID.randomUUID().toString(); + this.consumerInstance = busTopicParams.getConsumerInstance(); + + } else if (busTopicParams.isConsumerInstanceInvalid()) { + this.consumerGroup = busTopicParams.getConsumerGroup(); + this.consumerInstance = UUID.randomUUID().toString(); + + } else { + this.consumerGroup = busTopicParams.getConsumerGroup(); + this.consumerInstance = busTopicParams.getConsumerInstance(); + } + + if (busTopicParams.getFetchTimeout() <= 0) { + this.fetchTimeout = NO_TIMEOUT_MS_FETCH; + } else { + this.fetchTimeout = busTopicParams.getFetchTimeout(); + } + + if (busTopicParams.getFetchLimit() <= 0) { + this.fetchLimit = NO_LIMIT_FETCH; + } else { + this.fetchLimit = busTopicParams.getFetchLimit(); + } + + } + + /** + * Initialize the Bus client. + */ + public abstract void init() throws MalformedURLException; + + @Override + public void register(TopicListener topicListener) { + + super.register(topicListener); + + try { + if (!alive && !locked) { + this.start(); + } else { + logger.info("{}: register: start not attempted", this); + } + } catch (Exception e) { + logger.warn("{}: cannot start after registration of because of: {}", this, topicListener, e); + } + } + + @Override + public void unregister(TopicListener topicListener) { + boolean stop; + synchronized (this) { + super.unregister(topicListener); + stop = this.topicListeners.isEmpty(); + } + + if (stop) { + this.stop(); + } + } + + @Override + public boolean start() { + logger.info("{}: starting", this); + + synchronized (this) { + + if (alive) { + return true; + } + + if (locked) { + throw new IllegalStateException(this + " is locked."); + } + + if (this.busPollerThread == null || !this.busPollerThread.isAlive() || this.consumer == null) { + + try { + this.init(); + this.alive = true; + this.busPollerThread = makePollerThread(); + this.busPollerThread.setName(this.getTopicCommInfrastructure() + "-source-" + this.getTopic()); + busPollerThread.start(); + return true; + } catch (Exception e) { + throw new IllegalStateException(this + ": cannot start", e); + } + } + } + + return false; + } + + /** + * Makes a new thread to be used for polling. + * + * @return a new Thread + */ + protected Thread makePollerThread() { + return new Thread(this); + } + + @Override + public boolean stop() { + logger.info("{}: stopping", this); + + synchronized (this) { + BusConsumer consumerCopy = this.consumer; + + this.alive = false; + this.consumer = null; + + if (consumerCopy != null) { + try { + consumerCopy.close(); + } catch (Exception e) { + logger.warn("{}: stop failed because of {}", this, e.getMessage(), e); + } + } + } + + Thread.yield(); + + return true; + } + + /** + * Run thread method for the Bus Reader. + */ + @Override + public void run() { + while (this.alive) { + try { + fetchAllMessages(); + } catch (IOException | RuntimeException e) { + logger.error("{}: cannot fetch", this, e); + } + } + + logger.info("{}: exiting thread", this); + } + + private void fetchAllMessages() throws IOException { + for (String event : this.consumer.fetch()) { + synchronized (this) { + this.recentEvents.add(event); + } + + NetLoggerUtil.log(EventType.IN, this.getTopicCommInfrastructure(), this.topic, event); + + broadcast(event); + + if (!this.alive) { + return; + } + } + } + + @Override + public boolean offer(String event) { + if (!this.alive) { + throw new IllegalStateException(this + " is not alive."); + } + + synchronized (this) { + this.recentEvents.add(event); + } + + NetLoggerUtil.log(EventType.IN, this.getTopicCommInfrastructure(), this.topic, event); + + return broadcast(event); + } + + @Override + public String toString() { + return "SingleThreadedBusTopicSource [consumerGroup=" + consumerGroup + ", consumerInstance=" + consumerInstance + + ", fetchTimeout=" + fetchTimeout + ", fetchLimit=" + fetchLimit + ", consumer=" + this.consumer + + ", alive=" + alive + ", locked=" + locked + ", uebThread=" + busPollerThread + ", topicListeners=" + + topicListeners.size() + ", toString()=" + super.toString() + "]"; + } + + @Override + public void shutdown() { + this.stop(); + this.topicListeners.clear(); + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBase.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBase.java new file mode 100644 index 00000000..4d1fbc9e --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBase.java @@ -0,0 +1,243 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import org.apache.commons.collections4.queue.CircularFifoQueue; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Getter +public abstract class TopicBase implements Topic { + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(TopicBase.class); + + /** + * List of servers. + */ + protected List<String> servers; + + /** + * Topic. + */ + protected final String topic; + + /** + * Topic Alias. + */ + protected final String effectiveTopic; + + /** + * Event cache. + */ + protected CircularFifoQueue<String> recentEvents = new CircularFifoQueue<>(10); + + /** + * Am I running? reflects invocation of start()/stop() !locked & start() => alive stop() => + * !alive. + */ + protected volatile boolean alive = false; + + /** + * Am I locked? reflects invocation of lock()/unlock() operations locked => !alive (but not in + * the other direction necessarily) locked => !offer, !run, !start, !stop (but this last one is + * obvious since locked => !alive). + */ + protected volatile boolean locked = false; + + /** + * All my subscribers for new message notifications. + */ + @Getter(AccessLevel.NONE) + protected final ArrayList<TopicListener> topicListeners = new ArrayList<>(); + + /** + * Instantiates a new Topic Base. + * + * @param servers list of servers + * @param topic topic name + * + * @throws IllegalArgumentException if invalid parameters are present + */ + protected TopicBase(List<String> servers, String topic) { + this(servers, topic, topic); + } + + /** + * Instantiates a new Topic Base. + * + * @param servers list of servers + * @param topic topic name + * + * @throws IllegalArgumentException if invalid parameters are present + */ + protected TopicBase(List<String> servers, String topic, String effectiveTopic) { + + if (servers == null || servers.isEmpty()) { + throw new IllegalArgumentException("Server(s) must be provided"); + } + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException("A Topic must be provided"); + } + + String effectiveTopicCopy; + if (effectiveTopic == null || effectiveTopic.isEmpty()) { + effectiveTopicCopy = topic; + } else { + effectiveTopicCopy = effectiveTopic; + } + + this.servers = servers; + this.topic = topic.toLowerCase(); + this.effectiveTopic = effectiveTopicCopy.toLowerCase(); + } + + @Override + public void register(TopicListener topicListener) { + + logger.info("{}: registering {}", this, topicListener); + + synchronized (this) { + if (topicListener == null) { + throw new IllegalArgumentException("TopicListener must be provided"); + } + + for (TopicListener listener : this.topicListeners) { + if (listener == topicListener) { + return; + } + } + + this.topicListeners.add(topicListener); + } + } + + @Override + public void unregister(TopicListener topicListener) { + + logger.info("{}: unregistering {}", this, topicListener); + + synchronized (this) { + if (topicListener == null) { + throw new IllegalArgumentException("TopicListener must be provided"); + } + + this.topicListeners.remove(topicListener); + } + } + + /** + * Broadcast event to all listeners. + * + * @param message the event + * @return true if all notifications are performed with no error, false otherwise + */ + protected boolean broadcast(String message) { + List<TopicListener> snapshotListeners = this.snapshotTopicListeners(); + + var success = true; + for (TopicListener topicListener : snapshotListeners) { + try { + topicListener.onTopicEvent(this.getTopicCommInfrastructure(), this.topic, message); + } catch (Exception e) { + logger.warn("{}: notification error @ {} because of {}", this, topicListener, e.getMessage(), e); + success = false; + } + } + return success; + } + + /** + * Take a snapshot of current topic listeners. + * + * @return the topic listeners + */ + protected synchronized List<TopicListener> snapshotTopicListeners() { + @SuppressWarnings("unchecked") + List<TopicListener> listeners = (List<TopicListener>) topicListeners.clone(); + return listeners; + } + + @Override + public boolean lock() { + + logger.info("{}: locking", this); + + synchronized (this) { + if (this.locked) { + return true; + } + + this.locked = true; + } + + return this.stop(); + } + + @Override + public boolean unlock() { + logger.info("{}: unlocking", this); + + synchronized (this) { + if (!this.locked) { + return true; + } + + this.locked = false; + } + + try { + return this.start(); + } catch (Exception e) { + logger.warn("{}: cannot after unlocking because of {}", this, e.getMessage(), e); + return false; + } + } + + @Override + public synchronized String[] getRecentEvents() { + var events = new String[recentEvents.size()]; + return recentEvents.toArray(events); + } + + + @Override + public String toString() { + return "TopicBase [servers=" + servers + + ", topic=" + topic + + ", effectiveTopic=" + effectiveTopic + + ", #recentEvents=" + recentEvents.size() + + ", locked=" + locked + + ", #topicListeners=" + topicListeners.size() + + "]"; + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseFactory.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseFactory.java new file mode 100644 index 00000000..d98de653 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseFactory.java @@ -0,0 +1,87 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Topic Base Factory. + * + * @param <T> Type. + */ +public interface TopicBaseFactory<T extends Topic> { + + /** + * build a TopicBase instance. + * + * @param properties properties. + * @return T instance. + */ + List<T> build(Properties properties); + + /** + * build a TopicBase instance. + * + * @param servers servers. + * @param topic topic. + * @param managed managed. + * @return T instance. + */ + T build(List<String> servers, String topic, boolean managed); + + /** + * Construct an instance of an endpoint. + * + * @param param parameters + * @return an instance of T. + */ + T build(BusTopicParams param); + + /** + * destroy TopicBase instance. + * @param topic topic. + */ + void destroy(String topic); + + /** + * destroy. + */ + void destroy(); + + /** + * get T instance. + * + * @param topic topic. + * @return T instance. + */ + T get(String topic); + + /** + * inventory of T instances. + * + * @return T instance list. + */ + List<T> inventory(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseHashedFactory.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseHashedFactory.java new file mode 100644 index 00000000..70ff04e4 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseHashedFactory.java @@ -0,0 +1,207 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Topic Factory implementation that indexes T instances in a hash table. + */ +public abstract class TopicBaseHashedFactory<T extends Topic> implements TopicBaseFactory<T> { + + protected static final String MISSING_TOPIC_MESSAGE = "A topic must be provided"; + protected static final String MISSING_SERVERS_MESSAGE = "Servers must be provided"; + + /** + * endpoints. + */ + protected final HashMap<String, T> endpoints = new HashMap<>(); + + /** + * get the topic names. + * + * @param properties properties. + * @return list of topic names. + */ + protected abstract List<String> getTopicNames(Properties properties); + + /** + * get the servers that this topic uses. + * + * @param topicName name. + * @param properties properties. + * @return list of servers. + */ + protected abstract List<String> getServers(String topicName, Properties properties); + + /** + * Determines if this topic is managed. + * + * @param topicName name. + * @param properties properties. + * @return if managed. + */ + protected abstract boolean isManaged(String topicName, Properties properties); + + /** + * construct an instance of an endpoint. + * + * @param servers servers, + * @param topic topic. + * @return an instance of T. + */ + public abstract T build(List<String> servers, String topic); + + /** + * {@inheritDoc}. + */ + @Override + public List<T> build(Properties properties) { + List<String> topicNames = getTopicNames(properties); + if (topicNames == null || topicNames.isEmpty()) { + return Collections.emptyList(); + } + + List<T> newEndpoints = new ArrayList<>(); + synchronized (this) { + for (String name : topicNames) { + if (this.endpoints.containsKey(name)) { + newEndpoints.add(this.endpoints.get(name)); + continue; + } + + newEndpoints.add(this.build(getServers(name, properties), name, isManaged(name, properties))); + } + } + return newEndpoints; + } + + /** + * {@inheritDoc}. + */ + @Override + public T build(BusTopicParams param) { + return this.build(param.getServers(), param.getTopic(), param.isManaged()); + } + + /** + * {@inheritDoc}. + */ + @Override + public T build(List<String> servers, String topic, boolean managed) { + if (servers == null || servers.isEmpty()) { + throw new IllegalArgumentException(MISSING_SERVERS_MESSAGE); + } + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC_MESSAGE); + } + + synchronized (this) { + if (this.endpoints.containsKey(topic)) { + return this.endpoints.get(topic); + } + + var endpoint = build(servers, topic); + if (managed) { + this.endpoints.put(topic, endpoint); + } + + return endpoint; + } + } + + /** + * {@inheritDoc}. + */ + @Override + public void destroy(String topic) { + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC_MESSAGE); + } + + T endpoint; + synchronized (this) { + if (!this.endpoints.containsKey(topic)) { + return; + } + + endpoint = this.endpoints.remove(topic); + } + endpoint.shutdown(); + } + + /** + * {@inheritDoc}. + */ + @Override + public void destroy() { + final List<T> snapshotEndpoints = this.inventory(); + for (final T snapshot : snapshotEndpoints) { + snapshot.shutdown(); + } + + synchronized (this) { + this.endpoints.clear(); + } + } + + /** + * {@inheritDoc}. + */ + @Override + public T get(String topic) { + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC_MESSAGE); + } + + synchronized (this) { + if (this.endpoints.containsKey(topic)) { + return this.endpoints.get(topic); + } else { + throw new IllegalStateException(topic + " not found"); + } + } + } + + /** + * {@inheritDoc}. + */ + @Override + public List<T> inventory() { + return new ArrayList<>(this.endpoints.values()); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "TopicBaseHashedFactory[ " + super.toString() + " ]"; + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/BidirectionalTopicClient.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/BidirectionalTopicClient.java new file mode 100644 index 00000000..6ffc188d --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/BidirectionalTopicClient.java @@ -0,0 +1,222 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import lombok.Getter; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicEndpoint; +import org.onap.policy.common.message.bus.event.TopicEndpointManager; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.message.bus.event.TopicSink; +import org.onap.policy.common.message.bus.event.TopicSource; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A "bidirectional" topic, which is a pair of topics, one of which is used to publish + * requests and the other to receive responses. + */ +@Getter +public class BidirectionalTopicClient { + private static final Logger logger = LoggerFactory.getLogger(BidirectionalTopicClient.class); + private static final Coder coder = new StandardCoder(); + + private final String sinkTopic; + private final String sourceTopic; + private final TopicSink sink; + private final TopicSource source; + private final CommInfrastructure sinkTopicCommInfrastructure; + private final CommInfrastructure sourceTopicCommInfrastructure; + + /** + * Used when checking whether a message sent on the sink topic can be received + * on the source topic. When a matching message is received on the incoming topic, + * {@code true} is placed on the queue. If {@link #stopWaiting()} is called or the waiting + * thread is interrupted, then {@code false} is placed on the queue. Whenever a value + * is pulled from the queue, it is immediately placed back on the queue. + */ + private final BlockingDeque<Boolean> checkerQueue = new LinkedBlockingDeque<>(); + + + /** + * Constructs the object. + * + * @param sinkTopic sink topic name + * @param sourceTopic source topic name + * @throws BidirectionalTopicClientException if either topic does not exist + */ + public BidirectionalTopicClient(String sinkTopic, String sourceTopic) throws BidirectionalTopicClientException { + this.sinkTopic = sinkTopic.toLowerCase(); + this.sourceTopic = sourceTopic.toLowerCase(); + + // init sinkClient + List<TopicSink> sinks = getTopicEndpointManager().getTopicSinks(sinkTopic); + if (sinks.isEmpty()) { + throw new BidirectionalTopicClientException("no sinks for topic: " + sinkTopic); + } else if (sinks.size() > 1) { + throw new BidirectionalTopicClientException("too many sinks for topic: " + sinkTopic); + } + + this.sink = sinks.get(0); + + // init source + List<TopicSource> sources = getTopicEndpointManager().getTopicSources(Collections.singletonList(sourceTopic)); + if (sources.isEmpty()) { + throw new BidirectionalTopicClientException("no sources for topic: " + sourceTopic); + } else if (sources.size() > 1) { + throw new BidirectionalTopicClientException("too many sources for topic: " + sourceTopic); + } + + this.source = sources.get(0); + + this.sinkTopicCommInfrastructure = sink.getTopicCommInfrastructure(); + this.sourceTopicCommInfrastructure = source.getTopicCommInfrastructure(); + } + + public boolean send(String message) { + return sink.send(message); + } + + public void register(TopicListener topicListener) { + source.register(topicListener); + } + + public boolean offer(String event) { + return source.offer(event); + } + + public void unregister(TopicListener topicListener) { + source.unregister(topicListener); + } + + /** + * Determines whether the topic is ready (i.e., {@link #awaitReady(Object, long)} has + * previously returned {@code true}). + * + * @return {@code true}, if the topic is ready to send and receive + */ + public boolean isReady() { + return Boolean.TRUE.equals(checkerQueue.peek()); + } + + /** + * Waits for the bidirectional topic to become "ready" by publishing a message on the + * sink topic and awaiting receipt of the message on the source topic. If the message + * is not received within a few seconds, then it tries again. This process is + * continued until the message is received, {@link #stopWaiting()} is called, or this thread + * is interrupted. Once this returns, subsequent calls will return immediately, always + * with the same value. + * + * @param message message to be sent to the sink topic. Note: the equals() method must + * return {@code true} if and only if two messages are the same + * @param waitMs time to wait, in milliseconds, before re-sending the message + * @return {@code true} if the message was received from the source topic, + * {@code false} if this method was stopped or interrupted before receipt of + * the message + * @throws CoderException if the message cannot be encoded + */ + public synchronized <T> boolean awaitReady(T message, long waitMs) throws CoderException { + // see if we already know the answer + if (!checkerQueue.isEmpty()) { + return checkerQueue.peek(); + } + + final String messageText = coder.encode(message); + + // class of message to be decoded + final TopicListener listener = getTopicListener(message); + + source.register(listener); + + // loop until the message is received + try { + Boolean result; + do { + send(messageText); + } while ((result = checkerQueue.poll(waitMs, TimeUnit.MILLISECONDS)) == null); + + // put it back on the queue + checkerQueue.add(result); + + } catch (InterruptedException e) { + logger.error("interrupted waiting for topic sink {} source {}", sink.getTopic(), source.getTopic(), e); + Thread.currentThread().interrupt(); + checkerQueue.add(Boolean.FALSE); + + } finally { + source.unregister(listener); + } + + return checkerQueue.peek(); + } + + private <T> TopicListener getTopicListener(T message) { + @SuppressWarnings("unchecked") + final Class<? extends T> clazz = (Class<? extends T>) message.getClass(); + + // create a listener to detect when a matching message is received + return (infra, topic, msg) -> { + try { + T incoming = decode(msg, clazz); + + if (message.equals(incoming)) { + logger.info("topic {} is ready; found matching message {}", topic, incoming); + checkerQueue.add(Boolean.TRUE); + } + + } catch (CoderException e) { + logger.warn("cannot decode message from topic {}", topic, e); + decodeFailed(); + } + }; + } + + /** + * Stops any listeners that are currently stuck in {@link #awaitReady(Object, long)} by + * adding {@code false} to the queue. + */ + public void stopWaiting() { + checkerQueue.add(Boolean.FALSE); + } + + // these may be overridden by junit tests + + protected TopicEndpoint getTopicEndpointManager() { + return TopicEndpointManager.getManager(); + } + + protected <T> T decode(String msg, Class<? extends T> clazz) throws CoderException { + return coder.decode(msg, clazz); + } + + protected void decodeFailed() { + // already logged - nothing else to do + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/BidirectionalTopicClientException.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/BidirectionalTopicClientException.java new file mode 100644 index 00000000..442c5243 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/BidirectionalTopicClientException.java @@ -0,0 +1,53 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import java.io.Serial; + +/** + * Exception thrown by BidirectionalTopicClient class. + */ +public class BidirectionalTopicClientException extends Exception { + @Serial + private static final long serialVersionUID = 1L; + + public BidirectionalTopicClientException() { + super(); + } + + public BidirectionalTopicClientException(String message) { + super(message); + } + + public BidirectionalTopicClientException(Throwable cause) { + super(cause); + } + + public BidirectionalTopicClientException(String message, Throwable cause) { + super(message, cause); + } + + public BidirectionalTopicClientException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClient.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClient.java new file mode 100644 index 00000000..131bf2d7 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClient.java @@ -0,0 +1,114 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import java.util.List; +import lombok.Getter; +import lombok.NonNull; +import org.onap.policy.common.message.bus.event.TopicEndpointManager; +import org.onap.policy.common.message.bus.event.TopicSink; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Client for sending messages to a Topic using TopicSink. + */ +@Getter +public class TopicSinkClient { + private static final Logger logger = LoggerFactory.getLogger(TopicSinkClient.class); + + /** + * Coder used to encode messages being sent to the topic. + */ + private static final Coder CODER = new StandardCoder(); + + /** + * Where messages are published. + */ + private final TopicSink sink; + + /** + * Constructs the object. + * + * @param topic topic to which messages should be published + * @throws TopicSinkClientException if the topic does not exist + */ + public TopicSinkClient(final String topic) throws TopicSinkClientException { + final List<TopicSink> lst = getTopicSinks(topic.toLowerCase()); + if (lst.isEmpty()) { + throw new TopicSinkClientException("no sinks for topic: " + topic.toLowerCase()); + } + + this.sink = lst.get(0); + } + + /** + * Constructs the client from a sink object. + * + * @param sink topic sink publisher + */ + public TopicSinkClient(@NonNull TopicSink sink) { + this.sink = sink; + } + + + /** + * Gets the canonical topic name. + * + * @return topic name + */ + public String getTopic() { + return this.sink.getTopic(); + } + + /** + * Sends a message to the topic, after encoding the message as json. + * + * @param message message to be encoded and sent + * @return {@code true} if the message was successfully sent/enqueued, {@code false} otherwise + */ + public boolean send(final Object message) { + try { + final String json = CODER.encode(message); + return sink.send(json); + + } catch (RuntimeException | CoderException e) { + logger.warn("send to {} failed because of {}", sink.getTopic(), e.getMessage(), e); + return false; + } + } + + // the remaining methods are wrappers that can be overridden by junit tests + + /** + * Gets the sinks for a given topic. + * + * @param topic the topic of interest + * @return the sinks for the topic + */ + protected List<TopicSink> getTopicSinks(final String topic) { + return TopicEndpointManager.getManager().getTopicSinks(topic); + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientException.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientException.java new file mode 100644 index 00000000..fad5e119 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientException.java @@ -0,0 +1,54 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019, 2023-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import java.io.Serial; + +/** + * Exception thrown by TopicSink client classes. + */ +public class TopicSinkClientException extends Exception { + @Serial + private static final long serialVersionUID = 1L; + + public TopicSinkClientException() { + super(); + } + + public TopicSinkClientException(final String message) { + super(message); + } + + public TopicSinkClientException(final Throwable cause) { + super(cause); + } + + public TopicSinkClientException(final String message, final Throwable cause) { + super(message, cause); + } + + public TopicSinkClientException(final String message, final Throwable cause, final boolean enableSuppression, + final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSinkFactory.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSinkFactory.java new file mode 100644 index 00000000..0497f1f5 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSinkFactory.java @@ -0,0 +1,201 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_KAFKA_SINK_TOPICS; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX; + +import com.google.re2j.Pattern; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.onap.policy.common.message.bus.utils.KafkaPropertyUtils; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.properties.PropertyUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory of KAFKA Reader Topics indexed by topic name. + */ +class IndexedKafkaTopicSinkFactory implements KafkaTopicSinkFactory { + private static final Pattern COMMA_SPACE_PAT = Pattern.compile("\\s*,\\s*"); + private static final String MISSING_TOPIC = "A topic must be provided"; + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(IndexedKafkaTopicSinkFactory.class); + + /** + * KAFKA Topic Name Index. + */ + protected HashMap<String, KafkaTopicSink> kafkaTopicSinks = new HashMap<>(); + + @Override + public KafkaTopicSink build(BusTopicParams busTopicParams) { + + if (busTopicParams.getServers() == null || busTopicParams.getServers().isEmpty()) { + throw new IllegalArgumentException("KAFKA Server(s) must be provided"); + } + + if (StringUtils.isBlank(busTopicParams.getTopic())) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + synchronized (this) { + if (kafkaTopicSinks.containsKey(busTopicParams.getTopic())) { + return kafkaTopicSinks.get(busTopicParams.getTopic()); + } + + KafkaTopicSink kafkaTopicWriter = makeSink(busTopicParams); + if (busTopicParams.isManaged()) { + kafkaTopicSinks.put(busTopicParams.getTopic(), kafkaTopicWriter); + } + + return kafkaTopicWriter; + } + } + + + @Override + public KafkaTopicSink build(List<String> servers, String topic) { + return this.build(BusTopicParams.builder() + .servers(servers) + .topic(topic) + .managed(true) + .useHttps(false) + .build()); + } + + + @Override + public List<KafkaTopicSink> build(Properties properties) { + + String writeTopics = properties.getProperty(PROPERTY_KAFKA_SINK_TOPICS); + if (StringUtils.isBlank(writeTopics)) { + logger.info("{}: no topic for KAFKA Sink", this); + return new ArrayList<>(); + } + + List<KafkaTopicSink> newKafkaTopicSinks = new ArrayList<>(); + synchronized (this) { + for (String topic : COMMA_SPACE_PAT.split(writeTopics)) { + addTopic(newKafkaTopicSinks, topic.toLowerCase(), properties); + } + return newKafkaTopicSinks; + } + } + + private void addTopic(List<KafkaTopicSink> newKafkaTopicSinks, String topic, Properties properties) { + if (this.kafkaTopicSinks.containsKey(topic)) { + newKafkaTopicSinks.add(this.kafkaTopicSinks.get(topic)); + return; + } + + String topicPrefix = PROPERTY_KAFKA_SINK_TOPICS + "." + topic; + + var props = new PropertyUtils(properties, topicPrefix, + (name, value, ex) -> logger.warn("{}: {} {} is in invalid format for topic sink {} ", + this, name, value, topic)); + + String servers = properties.getProperty(topicPrefix + PROPERTY_TOPIC_SERVERS_SUFFIX); + if (StringUtils.isBlank(servers)) { + logger.error("{}: no KAFKA servers configured for sink {}", this, topic); + return; + } + + KafkaTopicSink kafkaTopicWriter = this.build(KafkaPropertyUtils.makeBuilder(props, topic, servers) + .partitionId(props.getString(PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX, null)) + .build()); + newKafkaTopicSinks.add(kafkaTopicWriter); + } + + @Override + public void destroy(String topic) { + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + KafkaTopicSink kafkaTopicWriter; + synchronized (this) { + if (!kafkaTopicSinks.containsKey(topic)) { + return; + } + + kafkaTopicWriter = kafkaTopicSinks.remove(topic); + } + + kafkaTopicWriter.shutdown(); + } + + @Override + public void destroy() { + List<KafkaTopicSink> writers = this.inventory(); + for (KafkaTopicSink writer : writers) { + writer.shutdown(); + } + + synchronized (this) { + this.kafkaTopicSinks.clear(); + } + } + + @Override + public KafkaTopicSink get(String topic) { + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + synchronized (this) { + if (kafkaTopicSinks.containsKey(topic)) { + return kafkaTopicSinks.get(topic); + } else { + throw new IllegalStateException("KafkaTopicSink for " + topic + " not found"); + } + } + } + + @Override + public synchronized List<KafkaTopicSink> inventory() { + return new ArrayList<>(this.kafkaTopicSinks.values()); + } + + /** + * Makes a new sink. + * + * @param busTopicParams parameters to use to configure the sink + * @return a new sink + */ + protected KafkaTopicSink makeSink(BusTopicParams busTopicParams) { + return new InlineKafkaTopicSink(busTopicParams); + } + + + @Override + public String toString() { + return "IndexedKafkaTopicSinkFactory " + kafkaTopicSinks.keySet(); + } + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactory.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactory.java new file mode 100644 index 00000000..1aac89ce --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactory.java @@ -0,0 +1,211 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.DEFAULT_LIMIT_FETCH; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_KAFKA_SOURCE_TOPICS; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SOURCE_CONSUMER_GROUP_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SOURCE_CONSUMER_INSTANCE_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SOURCE_FETCH_LIMIT_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SOURCE_FETCH_TIMEOUT_SUFFIX; + +import com.google.re2j.Pattern; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import org.apache.commons.lang3.StringUtils; +import org.onap.policy.common.message.bus.utils.KafkaPropertyUtils; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.properties.PropertyUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory of KAFKA Source Topics indexed by topic name. + */ +class IndexedKafkaTopicSourceFactory implements KafkaTopicSourceFactory { + private static final Pattern COMMA_SPACE_PAT = Pattern.compile("\\s*,\\s*"); + private static final String MISSING_TOPIC = "A topic must be provided"; + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(IndexedKafkaTopicSourceFactory.class); + + /** + * KAFKA Topic Name Index. + */ + protected HashMap<String, KafkaTopicSource> kafkaTopicSources = new HashMap<>(); + + @Override + public KafkaTopicSource build(BusTopicParams busTopicParams) { + if (busTopicParams.getServers() == null || busTopicParams.getServers().isEmpty()) { + throw new IllegalArgumentException("KAFKA Server(s) must be provided"); + } + + if (busTopicParams.getTopic() == null || busTopicParams.getTopic().isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + synchronized (this) { + if (kafkaTopicSources.containsKey(busTopicParams.getTopic())) { + return kafkaTopicSources.get(busTopicParams.getTopic()); + } + + var kafkaTopicSource = makeSource(busTopicParams); + + kafkaTopicSources.put(busTopicParams.getTopic(), kafkaTopicSource); + + return kafkaTopicSource; + } + } + + @Override + public List<KafkaTopicSource> build(Properties properties) { + + String readTopics = properties.getProperty(PROPERTY_KAFKA_SOURCE_TOPICS); + if (StringUtils.isBlank(readTopics)) { + logger.info("{}: no topic for KAFKA Source", this); + return new ArrayList<>(); + } + + List<KafkaTopicSource> newKafkaTopicSources = new ArrayList<>(); + synchronized (this) { + for (String topic : COMMA_SPACE_PAT.split(readTopics)) { + addTopic(newKafkaTopicSources, topic.toLowerCase(), properties); + } + } + return newKafkaTopicSources; + } + + @Override + public KafkaTopicSource build(List<String> servers, String topic) { + return this.build(BusTopicParams.builder() + .servers(servers) + .topic(topic) + .managed(true) + .fetchTimeout(DEFAULT_TIMEOUT_MS_FETCH) + .fetchLimit(DEFAULT_LIMIT_FETCH) + .useHttps(false).build()); + } + + private void addTopic(List<KafkaTopicSource> newKafkaTopicSources, String topic, Properties properties) { + if (this.kafkaTopicSources.containsKey(topic)) { + newKafkaTopicSources.add(this.kafkaTopicSources.get(topic)); + return; + } + + String topicPrefix = PROPERTY_KAFKA_SOURCE_TOPICS + "." + topic; + + var props = new PropertyUtils(properties, topicPrefix, + (name, value, ex) -> logger.warn("{}: {} {} is in invalid format for topic source {} ", + this, name, value, topic)); + + String servers = properties.getProperty(topicPrefix + PROPERTY_TOPIC_SERVERS_SUFFIX); + if (StringUtils.isBlank(servers)) { + logger.error("{}: no KAFKA servers configured for source {}", this, topic); + return; + } + + var kafkaTopicSource = this.build(KafkaPropertyUtils.makeBuilder(props, topic, servers) + .consumerGroup(props.getString( + PROPERTY_TOPIC_SOURCE_CONSUMER_GROUP_SUFFIX, null)) + .consumerInstance(props.getString( + PROPERTY_TOPIC_SOURCE_CONSUMER_INSTANCE_SUFFIX, null)) + .fetchTimeout(props.getInteger( + PROPERTY_TOPIC_SOURCE_FETCH_TIMEOUT_SUFFIX, + DEFAULT_TIMEOUT_MS_FETCH)) + .fetchLimit(props.getInteger(PROPERTY_TOPIC_SOURCE_FETCH_LIMIT_SUFFIX, + DEFAULT_LIMIT_FETCH)) + .build()); + + newKafkaTopicSources.add(kafkaTopicSource); + } + + /** + * Makes a new source. + * + * @param busTopicParams parameters to use to configure the source + * @return a new source + */ + protected KafkaTopicSource makeSource(BusTopicParams busTopicParams) { + return new SingleThreadedKafkaTopicSource(busTopicParams); + } + + @Override + public void destroy(String topic) { + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + KafkaTopicSource kafkaTopicSource; + + synchronized (this) { + if (!kafkaTopicSources.containsKey(topic)) { + return; + } + + kafkaTopicSource = kafkaTopicSources.remove(topic); + } + + kafkaTopicSource.shutdown(); + } + + @Override + public void destroy() { + List<KafkaTopicSource> readers = this.inventory(); + for (KafkaTopicSource reader : readers) { + reader.shutdown(); + } + + synchronized (this) { + this.kafkaTopicSources.clear(); + } + } + + @Override + public KafkaTopicSource get(String topic) { + + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(MISSING_TOPIC); + } + + synchronized (this) { + if (kafkaTopicSources.containsKey(topic)) { + return kafkaTopicSources.get(topic); + } else { + throw new IllegalStateException("KafkaTopiceSource for " + topic + " not found"); + } + } + } + + @Override + public synchronized List<KafkaTopicSource> inventory() { + return new ArrayList<>(this.kafkaTopicSources.values()); + } + + @Override + public String toString() { + return "IndexedKafkaTopicSourceFactory " + kafkaTopicSources.keySet(); + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSink.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSink.java new file mode 100644 index 00000000..4bdd2b0f --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSink.java @@ -0,0 +1,83 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import java.util.Map; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.event.base.InlineBusTopicSink; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This implementation publishes events for the associated KAFKA topic, inline with the calling + * thread. + */ +public class InlineKafkaTopicSink extends InlineBusTopicSink implements KafkaTopicSink { + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(InlineKafkaTopicSink.class); + + protected Map<String, String> additionalProps; + + /** + * Argument-based KAFKA Topic Writer instantiation. BusTopicParams contains the below + * attributes. + * + * <p>servers list of KAFKA servers available for publishing + * topic the topic to publish to + * partitionId the partition key (optional, autogenerated if not provided) + * useHttps does connection use HTTPS? + * @param busTopicParams contains attributes needed + * @throws IllegalArgumentException if invalid arguments are detected + */ + public InlineKafkaTopicSink(BusTopicParams busTopicParams) { + super(busTopicParams); + this.additionalProps = busTopicParams.getAdditionalProps(); + } + + /** + * Instantiation of internal resources. + */ + @Override + public void init() { + + this.publisher = new KafkaPublisherWrapper(BusTopicParams.builder() + .servers(this.servers) + .topic(this.effectiveTopic) + .useHttps(this.useHttps) + .allowTracing(this.allowTracing) + .additionalProps(this.additionalProps) + .build()); + logger.info("{}: KAFKA SINK created", this); + } + + @Override + public String toString() { + return "InlineKafkaTopicSink [getTopicCommInfrastructure()=" + getTopicCommInfrastructure() + ", toString()=" + + super.toString() + "]"; + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return Topic.CommInfrastructure.KAFKA; + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapper.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapper.java new file mode 100644 index 00000000..86b9e936 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapper.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import io.opentelemetry.instrumentation.kafkaclients.v2_6.TracingProducerInterceptor; +import java.util.Properties; +import java.util.UUID; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.onap.policy.common.message.bus.event.base.BusPublisher; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Kafka based library publisher. + */ +public class KafkaPublisherWrapper implements BusPublisher { + + private static final Logger logger = LoggerFactory.getLogger(KafkaPublisherWrapper.class); + private static final String KEY_SERIALIZER = "org.apache.kafka.common.serialization.StringSerializer"; + + private final String topic; + + /** + * Kafka publisher. + */ + private final Producer<String, String> producer; + protected Properties kafkaProps; + + /** + * Kafka Publisher Wrapper. + * + * @param busTopicParams topic parameters + */ + public KafkaPublisherWrapper(BusTopicParams busTopicParams) { + + if (busTopicParams.isTopicInvalid()) { + throw new IllegalArgumentException("No topic for Kafka"); + } + + this.topic = busTopicParams.getTopic(); + + // Setup Properties for consumer + kafkaProps = new Properties(); + kafkaProps.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, busTopicParams.getServers().get(0)); + if (busTopicParams.isAdditionalPropsValid()) { + kafkaProps.putAll(busTopicParams.getAdditionalProps()); + } + + if (kafkaProps.get(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG) == null) { + kafkaProps.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, KEY_SERIALIZER); + } + + if (kafkaProps.get(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG) == null) { + kafkaProps.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KEY_SERIALIZER); + } + + if (busTopicParams.isAllowTracing()) { + kafkaProps.setProperty(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, + TracingProducerInterceptor.class.getName()); + } + + producer = new KafkaProducer<>(kafkaProps); + } + + @Override + public boolean send(String partitionId, String message) { + if (message == null) { + throw new IllegalArgumentException(NO_MESSAGE_PROVIDED); + } + + try { + // Create the record + ProducerRecord<String, String> producerRecord = + new ProducerRecord<>(topic, UUID.randomUUID().toString(), message); + + this.producer.send(producerRecord); + producer.flush(); + } catch (Exception e) { + logger.warn("{}: SEND of {} cannot be performed because of {}", this, message, e.getMessage(), e); + return false; + } + return true; + } + + @Override + public void close() { + logger.info(LOG_CLOSE, this); + + try { + this.producer.close(); + } catch (Exception e) { + logger.warn("{}: CLOSE FAILED because of {}", this, e.getMessage(), e); + } + } + + @Override + public String toString() { + return "KafkaPublisherWrapper []"; + } + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactories.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactories.java new file mode 100644 index 00000000..c10285fc --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactories.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class KafkaTopicFactories { + + /** + * Factory for instantiation and management of sinks. + */ + @Getter + private static final KafkaTopicSinkFactory sinkFactory = new IndexedKafkaTopicSinkFactory(); + + /** + * Factory for instantiation and management of sources. + */ + @Getter + private static final KafkaTopicSourceFactory sourceFactory = new IndexedKafkaTopicSourceFactory(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSink.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSink.java new file mode 100644 index 00000000..f784a987 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSink.java @@ -0,0 +1,28 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import org.onap.policy.common.message.bus.event.base.BusTopicSink; + +/** + * Topic Writer over KAFKA Infrastructure. + */ +public interface KafkaTopicSink extends BusTopicSink { + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactory.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactory.java new file mode 100644 index 00000000..8feecbe0 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactory.java @@ -0,0 +1,89 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * KAFKA Topic Sink Factory. + */ +public interface KafkaTopicSinkFactory { + + /** + * Instantiates a new KAFKA Topic Writer. + * + * @param busTopicParams parameters object + * @return an KAFKA Topic Sink + */ + KafkaTopicSink build(BusTopicParams busTopicParams); + + /** + * Creates an KAFKA Topic Writer based on properties files. + * + * @param properties Properties containing initialization values + * + * @return an KAFKA Topic Writer + * @throws IllegalArgumentException if invalid parameters are present + */ + List<KafkaTopicSink> build(Properties properties); + + /** + * Instantiates a new KAFKA Topic Writer. + * + * @param servers list of servers + * @param topic topic name + * + * @return an KAFKA Topic Writer + * @throws IllegalArgumentException if invalid parameters are present + */ + KafkaTopicSink build(List<String> servers, String topic); + + /** + * Destroys an KAFKA Topic Writer based on a topic. + * + * @param topic topic name + * @throws IllegalArgumentException if invalid parameters are present + */ + void destroy(String topic); + + /** + * Destroys all KAFKA Topic Writers. + */ + void destroy(); + + /** + * gets an KAFKA Topic Writer based on topic name. + * + * @param topic the topic name + * + * @return an KAFKA Topic Writer with topic name + * @throws IllegalArgumentException if an invalid topic is provided + * @throws IllegalStateException if the KAFKA Topic Reader is an incorrect state + */ + KafkaTopicSink get(String topic); + + /** + * Provides a snapshot of the KAFKA Topic Writers. + * + * @return a list of the KAFKA Topic Writers + */ + List<KafkaTopicSink> inventory(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSource.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSource.java new file mode 100644 index 00000000..bddced7a --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSource.java @@ -0,0 +1,28 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import org.onap.policy.common.message.bus.event.base.BusTopicSource; + +/** + * Kafka Topic Source. + */ +public interface KafkaTopicSource extends BusTopicSource { + +}
\ No newline at end of file diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactory.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactory.java new file mode 100644 index 00000000..06f4412c --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactory.java @@ -0,0 +1,88 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Kafka Topic Source Factory. + */ +public interface KafkaTopicSourceFactory { + + /** + * Creates a Kafka Topic Source based on properties files. + * + * @param properties Properties containing initialization values + * + * @return a Kafka Topic Source + * @throws IllegalArgumentException if invalid parameters are present + */ + List<KafkaTopicSource> build(Properties properties); + + /** + * Instantiates a new Kafka Topic Source. + * + * @param busTopicParams parameters object + * @return a Kafka Topic Source + */ + KafkaTopicSource build(BusTopicParams busTopicParams); + + /** + * Instantiates a new Kafka Topic Source. + * + * @param servers list of servers + * @param topic topic name + * + * @return a Kafka Topic Source + * @throws IllegalArgumentException if invalid parameters are present + */ + KafkaTopicSource build(List<String> servers, String topic); + + /** + * Destroys a Kafka Topic Source based on a topic. + * + * @param topic topic name + * @throws IllegalArgumentException if invalid parameters are present + */ + void destroy(String topic); + + /** + * Destroys all Kafka Topic Sources. + */ + void destroy(); + + /** + * Gets a Kafka Topic Source based on topic name. + * + * @param topic the topic name + * @return a Kafka Topic Source with topic name + * @throws IllegalArgumentException if an invalid topic is provided + * @throws IllegalStateException if the Kafka Topic Source is an incorrect state + */ + KafkaTopicSource get(String topic); + + /** + * Provides a snapshot of the Kafka Topic Sources. + * + * @return a list of the Kafka Topic Sources + */ + List<KafkaTopicSource> inventory(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSource.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSource.java new file mode 100644 index 00000000..5691cb12 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSource.java @@ -0,0 +1,80 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import java.util.Map; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.event.base.BusConsumer; +import org.onap.policy.common.message.bus.event.base.SingleThreadedBusTopicSource; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * This topic source implementation specializes in reading messages over a Kafka Bus topic source and + * notifying its listeners. + */ +public class SingleThreadedKafkaTopicSource extends SingleThreadedBusTopicSource implements KafkaTopicSource { + + protected Map<String, String> additionalProps = null; + + /** + * Constructor. + * + * @param busTopicParams Parameters object containing all the required inputs + * @throws IllegalArgumentException An invalid parameter passed in + */ + public SingleThreadedKafkaTopicSource(BusTopicParams busTopicParams) { + super(busTopicParams); + this.additionalProps = busTopicParams.getAdditionalProps(); + try { + this.init(); + } catch (Exception e) { + throw new IllegalArgumentException("ERROR during init in kafka-source: cannot create topic " + topic, e); + } + } + + /** + * Initialize the client. + */ + @Override + public void init() { + BusTopicParams.TopicParamsBuilder builder = BusTopicParams.builder() + .servers(this.servers) + .topic(this.effectiveTopic) + .fetchTimeout(this.fetchTimeout) + .consumerGroup(this.consumerGroup) + .useHttps(this.useHttps) + .allowTracing(this.allowTracing); + + this.consumer = new BusConsumer.KafkaConsumerWrapper(builder + .additionalProps(this.additionalProps) + .build()); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return Topic.CommInfrastructure.KAFKA; + } + + @Override + public String toString() { + return "SingleThreadedKafkaTopicSource [getTopicCommInfrastructure()=" + getTopicCommInfrastructure() + + ", toString()=" + super.toString() + "]"; + } + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpoint.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpoint.java new file mode 100644 index 00000000..e25aca54 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpoint.java @@ -0,0 +1,141 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019 Samsung Electronics Co., Ltd. + * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import java.util.List; +import org.onap.policy.common.message.bus.event.base.TopicBase; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * No Operation topic endpoint. + */ +public abstract class NoopTopicEndpoint extends TopicBase { + + /** + * Logger. + */ + private static final Logger logger = LoggerFactory.getLogger(NoopTopicEndpoint.class); + + /** + * Constructs the object. + */ + protected NoopTopicEndpoint(List<String> servers, String topic) { + super(servers, topic); + } + + /** + * I/O. + * + * @param type "IN" or "OUT". + * @param message message. + * @return true if successful. + */ + protected boolean io(EventType type, String message) { + + if (message == null || message.isEmpty()) { + throw new IllegalArgumentException("Message is empty"); + } + + if (!this.alive) { + throw new IllegalStateException(this + " is stopped"); + } + + try { + synchronized (this) { + this.recentEvents.add(message); + } + + NetLoggerUtil.log(type, this.getTopicCommInfrastructure(), this.topic, message); + + broadcast(message); + } catch (Exception e) { + logger.warn("{}: cannot send because of {}", this, e.getMessage(), e); + return false; + } + + return true; + } + + /** + * {@inheritDoc}. + */ + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + /** + * {@inheritDoc}. + */ + @Override + public boolean start() { + logger.info("{}: starting", this); + + synchronized (this) { + if (!this.alive) { + if (locked) { + throw new IllegalStateException(this + " is locked."); + } + + this.alive = true; + } + } + + return true; + } + + /** + * {@inheritDoc}. + */ + @Override + public boolean stop() { + logger.info("{}: stopping", this); + + synchronized (this) { + this.alive = false; + } + return true; + } + + /** + * {@inheritDoc}. + */ + @Override + public void shutdown() { + logger.info("{}: shutdown", this); + + this.stop(); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicEndpoint[" + super.toString() + "]"; + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactories.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactories.java new file mode 100644 index 00000000..506cc9f9 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactories.java @@ -0,0 +1,42 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class NoopTopicFactories { + + /** + * Factory for instantiation and management of sinks. + */ + @Getter + private static final NoopTopicSinkFactory sinkFactory = new NoopTopicSinkFactory(); + + /** + * Factory for instantiation and management of sources. + */ + @Getter + private static final NoopTopicSourceFactory sourceFactory = new NoopTopicSourceFactory(); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactory.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactory.java new file mode 100644 index 00000000..e81dfaea --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactory.java @@ -0,0 +1,117 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; + +import com.google.re2j.Pattern; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.base.TopicBaseHashedFactory; + +/** + * Noop Topic Factory. + */ +public abstract class NoopTopicFactory<T extends NoopTopicEndpoint> extends TopicBaseHashedFactory<T> { + private static final Pattern COMMA_SPACE_PAT = Pattern.compile("\\s*,\\s*"); + + /** + * Get Topics Property Name. + * + * @return property name. + */ + protected abstract String getTopicsPropertyName(); + + /** + * {@inheritDoc}. + */ + @Override + protected List<String> getTopicNames(Properties properties) { + String topics = properties.getProperty(getTopicsPropertyName()); + if (topics == null || topics.isEmpty()) { + return new ArrayList<>(); + } + + return Arrays.asList(COMMA_SPACE_PAT.split(topics)); + } + + /** + * {@inheritDoc}. + */ + @Override + protected List<String> getServers(String topicName, Properties properties) { + String servers = + properties.getProperty(getTopicsPropertyName() + "." + topicName + + PROPERTY_TOPIC_SERVERS_SUFFIX); + + if (servers == null || servers.isEmpty()) { + servers = CommInfrastructure.NOOP.toString(); + } + + return new ArrayList<>(Arrays.asList(COMMA_SPACE_PAT.split(servers))); + } + + /** + * {@inheritDoc}. + */ + @Override + protected boolean isManaged(String topicName, Properties properties) { + var managedString = + properties.getProperty(getTopicsPropertyName() + "." + topicName + PROPERTY_MANAGED_SUFFIX); + + var managed = true; + if (managedString != null && !managedString.isEmpty()) { + managed = Boolean.parseBoolean(managedString); + } + + return managed; + } + + /** + * {@inheritDoc}. + */ + @Override + public T build(List<String> serverList, String topic, boolean managed) { + List<String> servers; + if (serverList == null || serverList.isEmpty()) { + servers = Collections.singletonList(CommInfrastructure.NOOP.toString()); + } else { + servers = serverList; + } + + return super.build(servers, topic, managed); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicFactory[ " + super.toString() + " ]"; + } +} + diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSink.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSink.java new file mode 100644 index 00000000..b7d78ff4 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSink.java @@ -0,0 +1,56 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import java.util.List; +import org.onap.policy.common.message.bus.event.TopicSink; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; + +/** + * No Operation Topic Sink. + */ +public class NoopTopicSink extends NoopTopicEndpoint implements TopicSink { + + /** + * Constructs the object. + */ + public NoopTopicSink(List<String> servers, String topic) { + super(servers, topic); + } + + /** + * {@inheritDoc}. + */ + @Override + public boolean send(String message) { + return super.io(EventType.OUT, message); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicSink[" + super.toString() + "]"; + } + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactory.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactory.java new file mode 100644 index 00000000..cdf4a17b --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactory.java @@ -0,0 +1,58 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_NOOP_SINK_TOPICS; + +import java.util.List; + +/** + * Noop Topic Sink Factory. + */ +public class NoopTopicSinkFactory extends NoopTopicFactory<NoopTopicSink> { + + /** + * {@inheritDoc}. + */ + @Override + protected String getTopicsPropertyName() { + return PROPERTY_NOOP_SINK_TOPICS; + } + + /** + * {@inheritDoc}. + */ + @Override + public NoopTopicSink build(List<String> servers, String topic) { + return new NoopTopicSink(servers, topic); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicSinkFactory [" + super.toString() + "]"; + } + +} + diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSource.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSource.java new file mode 100644 index 00000000..6e7d0216 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSource.java @@ -0,0 +1,56 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import java.util.List; +import org.onap.policy.common.message.bus.event.TopicSource; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; + +/** + * No Operation Topic Source. + */ +public class NoopTopicSource extends NoopTopicEndpoint implements TopicSource { + + /** + * Constructs the object. + */ + public NoopTopicSource(List<String> servers, String topic) { + super(servers, topic); + } + + /** + * {@inheritDoc}. + */ + @Override + public boolean offer(String event) { + return super.io(EventType.IN, event); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicSource[" + super.toString() + "]"; + } + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactory.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactory.java new file mode 100644 index 00000000..5e3a365a --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactory.java @@ -0,0 +1,56 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_NOOP_SOURCE_TOPICS; + +import java.util.List; + +/** + * No Operation Topic Source Factory. + */ +public class NoopTopicSourceFactory extends NoopTopicFactory<NoopTopicSource> { + + /** + * {@inheritDoc}. + */ + @Override + protected String getTopicsPropertyName() { + return PROPERTY_NOOP_SOURCE_TOPICS; + } + + /** + * {@inheritDoc}. + */ + @Override + public NoopTopicSource build(List<String> servers, String topic) { + return new NoopTopicSource(servers, topic); + } + + /** + * {@inheritDoc}. + */ + @Override + public String toString() { + return "NoopTopicSourceFactory [" + super.toString() + "]"; + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApi.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApi.java new file mode 100644 index 00000000..6e3b8301 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApi.java @@ -0,0 +1,54 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.features; + +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.utils.services.OrderedService; +import org.slf4j.Logger; + +/** + * Logging Feature API. Provides interception points before and after logging a message. + */ +public interface NetLoggerFeatureApi extends OrderedService { + + /** + * Intercepts a message before it is logged. + * + * @return true if this feature intercepts and takes ownership of the operation + * preventing the invocation of lower priority features. False, otherwise. + */ + default boolean beforeLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + return false; + } + + /** + * Intercepts a message after it is logged. + * + * @return true if this feature intercepts and takes ownership of the operation + * preventing the invocation of lower priority features. False, otherwise. + */ + default boolean afterLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + return false; + } + +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureProviders.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureProviders.java new file mode 100644 index 00000000..4f57ab2a --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureProviders.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.features; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.onap.policy.common.utils.services.OrderedServiceImpl; + +/** + * Providers for network logging feature. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class NetLoggerFeatureProviders { + + /** + * Feature providers implementing this interface. + */ + @Getter + private static final OrderedServiceImpl<NetLoggerFeatureApi> providers = + new OrderedServiceImpl<>(NetLoggerFeatureApi.class); +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/properties/MessageBusProperties.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/properties/MessageBusProperties.java new file mode 100644 index 00000000..9aa529f3 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/properties/MessageBusProperties.java @@ -0,0 +1,78 @@ +/*- + * ============LICENSE_START=============================================== + * Copyright (C) 2024 Nordix Foundation. + * ======================================================================== + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END================================================= + */ + +package org.onap.policy.common.message.bus.properties; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class MessageBusProperties { + + /* Generic property suffixes */ + + public static final String PROPERTY_TOPIC_SERVERS_SUFFIX = ".servers"; + public static final String PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX = ".effectiveTopic"; + + public static final String PROPERTY_TOPIC_SOURCE_CONSUMER_GROUP_SUFFIX = ".consumerGroup"; + public static final String PROPERTY_TOPIC_SOURCE_CONSUMER_INSTANCE_SUFFIX = ".consumerInstance"; + public static final String PROPERTY_TOPIC_SOURCE_FETCH_TIMEOUT_SUFFIX = ".fetchTimeout"; + public static final String PROPERTY_TOPIC_SOURCE_FETCH_LIMIT_SUFFIX = ".fetchLimit"; + public static final String PROPERTY_MANAGED_SUFFIX = ".managed"; + public static final String PROPERTY_ADDITIONAL_PROPS_SUFFIX = ".additionalProps"; + + public static final String PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX = ".partitionKey"; + + public static final String PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX = ".selfSignedCertificates"; + + public static final String PROPERTY_NOOP_SOURCE_TOPICS = "noop.source.topics"; + public static final String PROPERTY_NOOP_SINK_TOPICS = "noop.sink.topics"; + + /* KAFKA Properties */ + + public static final String PROPERTY_KAFKA_SOURCE_TOPICS = "kafka.source.topics"; + public static final String PROPERTY_KAFKA_SINK_TOPICS = "kafka.sink.topics"; + + /* HTTP Server Properties */ + + public static final String PROPERTY_HTTP_HTTPS_SUFFIX = ".https"; + + /* Topic Sink Values */ + + /* Topic Source values */ + + /** + * Default Timeout fetching in milliseconds. + */ + public static final int DEFAULT_TIMEOUT_MS_FETCH = 15000; + + /** + * Default maximum number of messages fetch at the time. + */ + public static final int DEFAULT_LIMIT_FETCH = 100; + + /** + * Definition of No Timeout fetching. + */ + public static final int NO_TIMEOUT_MS_FETCH = -1; + + /** + * Definition of No limit fetching. + */ + public static final int NO_LIMIT_FETCH = -1; +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtils.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtils.java new file mode 100644 index 00000000..cfe62208 --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtils.java @@ -0,0 +1,80 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.utils; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_ADDITIONAL_PROPS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.re2j.Pattern; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.parameters.topic.BusTopicParams.TopicParamsBuilder; +import org.onap.policy.common.utils.properties.PropertyUtils; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class KafkaPropertyUtils { + private static final Pattern COMMA_SPACE_PAT = Pattern.compile("\\s*,\\s*"); + + /** + * Makes a topic builder, configuring it with properties that are common to both + * sources and sinks. + * + * @param props properties to be used to configure the builder + * @param topic topic being configured + * @param servers target servers + * @return a topic builder + */ + public static TopicParamsBuilder makeBuilder(PropertyUtils props, String topic, String servers) { + + final List<String> serverList = new ArrayList<>(Arrays.asList(COMMA_SPACE_PAT.split(servers))); + return BusTopicParams.builder() + .servers(serverList) + .topic(topic) + .effectiveTopic(props.getString(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX, topic)) + .managed(props.getBoolean(PROPERTY_MANAGED_SUFFIX, true)) + .additionalProps(getAdditionalProps(props.getString(PROPERTY_ADDITIONAL_PROPS_SUFFIX, ""))); + } + + private static Map<String, String> getAdditionalProps(String additionalPropsString) { + try { + Map<String, String> additionalProps = new HashMap<>(); + var converted = new ObjectMapper().readValue(additionalPropsString, Map.class); + converted.forEach((k, v) -> { + if (k instanceof String key && v instanceof String value) { + additionalProps.put(key, value); + } + }); + return additionalProps; + } catch (Exception e) { + return Collections.emptyMap(); + } + + } +} diff --git a/message-bus/src/main/java/org/onap/policy/common/message/bus/utils/NetLoggerUtil.java b/message-bus/src/main/java/org/onap/policy/common/message/bus/utils/NetLoggerUtil.java new file mode 100644 index 00000000..b5454e5c --- /dev/null +++ b/message-bus/src/main/java/org/onap/policy/common/message/bus/utils/NetLoggerUtil.java @@ -0,0 +1,134 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.utils; + +import lombok.Getter; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.features.NetLoggerFeatureProviders; +import org.onap.policy.common.utils.services.FeatureApiUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A network logging utility class that allows drools applications code to access the + * network log (or other specified loggers) and logging features. + * + */ +public class NetLoggerUtil { + + /** + * Loggers. + */ + private static final Logger logger = LoggerFactory.getLogger(NetLoggerUtil.class); + @Getter + private static final Logger networkLogger = LoggerFactory.getLogger("network"); + + /** + * Constant for the system line separator. + */ + public static final String SYSTEM_LS = System.lineSeparator(); + + /** + * Specifies if the message is coming in or going out. + */ + public enum EventType { + IN, OUT + } + + /** + * Logs a message to the network logger. + * + * @param type can either be IN or OUT + * @param protocol the protocol used to receive/send the message + * @param topic the topic the message came from or null if the type is REST + * @param message message to be logged + */ + public static void log(EventType type, CommInfrastructure protocol, String topic, String message) { + log(networkLogger, type, protocol, topic, message); + } + + /** + * Logs a message to the specified logger (i.e. a controller logger). + * + * @param eventLogger the logger that will have the message appended + * @param type can either be IN or OUT + * @param protocol the protocol used to receive/send the message + * @param topic the topic the message came from or null if the type is REST + * @param message message to be logged + */ + public static void log(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + if (eventLogger == null) { + logger.debug("the logger is null, defaulting to network logger"); + eventLogger = networkLogger; + } + + if (featureBeforeLog(eventLogger, type, protocol, topic, message)) { + return; + } + + eventLogger.info("[{}|{}|{}]{}{}", type, protocol, topic, SYSTEM_LS, message); + + featureAfterLog(eventLogger, type, protocol, topic, message); + } + + /** + * Executes features that pre-process a message before it is logged. + * + * @param eventLogger the logger that will have the message appended + * @param type can either be IN or OUT + * @param protocol the protocol used to receive/send the message + * @param topic the topic the message came from or null if the type is REST + * @param message message to be logged + * + * @return true if this feature intercepts and takes ownership of the operation + * preventing the invocation of lower priority features. False, otherwise + */ + private static boolean featureBeforeLog(Logger eventLogger, EventType type, CommInfrastructure protocol, + String topic, String message) { + + return FeatureApiUtils.apply(NetLoggerFeatureProviders.getProviders().getList(), + feature -> feature.beforeLog(eventLogger, type, protocol, topic, message), + (feature, ex) -> logger.error("feature {} before-log failure because of {}", + feature.getClass().getName(), ex.getMessage(), ex)); + } + + /** + * Executes features that post-process a message after it is logged. + * + * @param eventLogger the logger that will have the message appended + * @param type can either be IN or OUT + * @param protocol the protocol used to receive/send the message + * @param topic the topic the message came from or null if the type is rest + * @param message message to be logged + * + * @return true if this feature intercepts and takes ownership of the operation + * preventing the invocation of lower priority features. False, otherwise + */ + private static boolean featureAfterLog(Logger eventLogger, EventType type, CommInfrastructure protocol, + String topic, String message) { + + return FeatureApiUtils.apply(NetLoggerFeatureProviders.getProviders().getList(), + feature -> feature.afterLog(eventLogger, type, protocol, topic, message), + (feature, ex) -> logger.error("feature {} after-log failure because of {}", + feature.getClass().getName(), ex.getMessage(), ex)); + } + +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/CommonTestData.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/CommonTestData.java new file mode 100644 index 00000000..ecd2f20c --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/CommonTestData.java @@ -0,0 +1,111 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019, 2024 Nordix Foundation. + * Modifications Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.onap.policy.common.parameters.ParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; + +/** + * Class to hold/create all parameters for test cases. + * + * @author Ajith Sreekumar (ajith.sreekumar@est.tech) + */ +public class CommonTestData { + + public static final String TOPIC_NAME = "policy-pdp-pap"; + public static final String TOPIC_INFRA = "kafka"; + public static final String TOPIC_SERVER = "kafka:9092"; + + public static final List<TopicParameters> TOPIC_PARAMS = + List.of(getTopicParameters(TOPIC_NAME, TOPIC_INFRA, TOPIC_SERVER)); + + protected static final Coder coder = new StandardCoder(); + + /** + * Create topic parameters for test cases. + * + * @param topicName name of topic + * @param topicInfra topicCommInfrastructure + * @param topicServer topic server + * @return topic parameters + */ + public static TopicParameters getTopicParameters(String topicName, String topicInfra, String topicServer) { + final TopicParameters topicParams = new TopicParameters(); + topicParams.setTopic(topicName); + topicParams.setTopicCommInfrastructure(topicInfra); + topicParams.setServers(List.of(topicServer)); + return topicParams; + } + + /** + * Converts the contents of a map to a parameter class. + * + * @param source property map + * @param clazz class of object to be created from the map + * @return a new object represented by the map + */ + public <T extends ParameterGroup> T toObject(final Map<String, Object> source, final Class<T> clazz) { + try { + return coder.decode(coder.encode(source), clazz); + + } catch (final CoderException e) { + throw new RuntimeException("cannot create " + clazz.getName() + " from map", e); + } + } + + /** + * Returns a property map for a TopicParameters map for test cases. + * + * @param isEmpty boolean value to represent that object created should be empty or not + * @return a property map suitable for constructing an object + */ + public Map<String, Object> getTopicParameterGroupMap(final boolean isEmpty) { + final Map<String, Object> map = new TreeMap<>(); + if (!isEmpty) { + map.put("topicSources", TOPIC_PARAMS); + map.put("topicSinks", TOPIC_PARAMS); + } + + return map; + } + + /** + * Gets the standard parameter group as a String. + * + * @param filePath path of the file + * @return the standard parameters + * @throws IOException when file read operation fails + */ + public String getParameterGroupAsString(String filePath) throws IOException { + File file = new File(filePath); + return Files.readString(file.toPath()); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.java new file mode 100644 index 00000000..6f0e38dd --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.java @@ -0,0 +1,400 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicFactories; +import org.onap.policy.common.message.bus.event.kafka.KafkaTopicPropertyBuilder; +import org.onap.policy.common.message.bus.event.noop.NoopTopicFactories; +import org.onap.policy.common.message.bus.event.noop.NoopTopicPropertyBuilder; +import org.onap.policy.common.message.bus.properties.MessageBusProperties; +import org.onap.policy.common.parameters.topic.TopicParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; +import org.onap.policy.common.utils.gson.GsonTestUtils; + +class TopicEndpointProxyTest { + + private static final String NOOP_SOURCE_TOPIC = "noop-source"; + private static final String NOOP_SINK_TOPIC = "noop-sink"; + + private static final String KAFKA_SOURCE_TOPIC = "kafka-source"; + private static final String KAFKA_SINK_TOPIC = "kafka-sink"; + + private final Properties configuration = new Properties(); + private final TopicParameterGroup group = new TopicParameterGroup(); + + /** + * Constructor. + */ + public TopicEndpointProxyTest() { + group.setTopicSinks(new LinkedList<>()); + group.setTopicSources(new LinkedList<>()); + + NoopTopicPropertyBuilder noopSourceBuilder = + new NoopTopicPropertyBuilder(MessageBusProperties.PROPERTY_NOOP_SOURCE_TOPICS) + .makeTopic(NOOP_SOURCE_TOPIC); + configuration.putAll(noopSourceBuilder.build()); + group.getTopicSources().add(noopSourceBuilder.getParams()); + + NoopTopicPropertyBuilder noopSinkBuilder = + new NoopTopicPropertyBuilder(MessageBusProperties.PROPERTY_NOOP_SINK_TOPICS) + .makeTopic(NOOP_SINK_TOPIC); + configuration.putAll(noopSinkBuilder.build()); + group.getTopicSinks().add(noopSinkBuilder.getParams()); + + TopicParameters invalidCommInfraParams = + new NoopTopicPropertyBuilder(MessageBusProperties.PROPERTY_NOOP_SOURCE_TOPICS) + .makeTopic(NOOP_SOURCE_TOPIC).getParams(); + invalidCommInfraParams.setTopicCommInfrastructure(Topic.CommInfrastructure.REST.name()); + group.getTopicSources().add(invalidCommInfraParams); + group.getTopicSinks().add(invalidCommInfraParams); + } + + private <T extends Topic> boolean exists(List<T> topics, String topicName) { + return topics.stream().map(Topic::getTopic).anyMatch(topicName::equals); + } + + private <T extends Topic> boolean allSources(List<T> topics) { + return exists(topics, NOOP_SOURCE_TOPIC); + } + + private <T extends Topic> boolean allSinks(List<T> topics) { + return exists(topics, NOOP_SINK_TOPIC); + } + + private <T extends Topic> boolean anySource(List<T> topics) { + return exists(topics, NOOP_SOURCE_TOPIC); + } + + private <T extends Topic> boolean anySink(List<T> topics) { + return exists(topics, NOOP_SINK_TOPIC); + } + + /** + * Destroys all managed topics. + */ + @AfterEach + public void tearDown() { + NoopTopicFactories.getSinkFactory().destroy(); + NoopTopicFactories.getSourceFactory().destroy(); + KafkaTopicFactories.getSinkFactory().destroy(); + KafkaTopicFactories.getSourceFactory().destroy(); + } + + @Test + void testSerialize() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSources(configuration); + manager.addTopicSinks(configuration); + + assertThatCode(() -> new GsonTestUtils().compareGson(manager, TopicEndpointProxyTest.class)) + .doesNotThrowAnyException(); + } + + @Test + void testAddTopicSourcesListOfTopicParameters() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List<TopicSource> sources = manager.addTopicSources(group.getTopicSources()); + assertSame(1, sources.size()); + + assertTrue(allSources(sources)); + assertFalse(anySink(sources)); + + sources = manager.addTopicSources(group.getTopicSources()); + assertSame(1, sources.size()); + assertTrue(allSources(sources)); + } + + @Test + void testAddTopicSourcesKafka() { + TopicEndpoint manager = new TopicEndpointProxy(); + + KafkaTopicPropertyBuilder kafkaTopicPropertyBuilder = + new KafkaTopicPropertyBuilder(MessageBusProperties.PROPERTY_KAFKA_SOURCE_TOPICS) + .makeTopic(KAFKA_SOURCE_TOPIC); + + configuration.putAll(kafkaTopicPropertyBuilder.build()); + group.getTopicSources().add(kafkaTopicPropertyBuilder.getParams()); + List<TopicSource> sources = manager.addTopicSources(group.getTopicSources()); + assertSame(2, sources.size()); + + configuration.remove(KAFKA_SOURCE_TOPIC); + group.setTopicSources(new LinkedList<>()); + sources = manager.addTopicSources(group.getTopicSources()); + assertSame(0, sources.size()); + } + + @Test + void testAddTopicSourcesProperties() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List<TopicSource> sources = manager.addTopicSources(configuration); + assertSame(1, sources.size()); + + assertTrue(allSources(sources)); + assertFalse(anySink(sources)); + } + + @Test + void testAddTopicSinksListOfTopicParameters() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List<TopicSink> sinks = manager.addTopicSinks(group.getTopicSinks()); + assertSame(1, sinks.size()); + + assertFalse(anySource(sinks)); + assertTrue(allSinks(sinks)); + } + + @Test + void testAddTopicSinksListOfTopicParametersKafka() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List<TopicSink> sinks = manager.addTopicSinks(group.getTopicSinks()); + assertSame(1, sinks.size()); + + KafkaTopicPropertyBuilder kafkaTopicPropertyBuilder = + new KafkaTopicPropertyBuilder(MessageBusProperties.PROPERTY_KAFKA_SINK_TOPICS) + .makeTopic(KAFKA_SINK_TOPIC); + + configuration.putAll(kafkaTopicPropertyBuilder.build()); + group.getTopicSources().add(kafkaTopicPropertyBuilder.getParams()); + sinks = manager.addTopicSinks(group.getTopicSources()); + assertSame(2, sinks.size()); + + configuration.remove(KAFKA_SOURCE_TOPIC); + group.setTopicSources(new LinkedList<>()); + sinks = manager.addTopicSinks(group.getTopicSources()); + assertSame(0, sinks.size()); + } + + @Test + void testAddTopicSinksProperties() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List<TopicSink> sinks = manager.addTopicSinks(configuration); + assertSame(1, sinks.size()); + + assertFalse(anySource(sinks)); + assertTrue(allSinks(sinks)); + } + + @Test + void testAddTopicsProperties() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List<Topic> topics = manager.addTopics(configuration); + assertSame(2, topics.size()); + + assertTrue(allSources(topics)); + assertTrue(allSinks(topics)); + } + + @Test + void testAddTopicsTopicParameterGroup() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List<Topic> topics = manager.addTopics(group); + assertSame(2, topics.size()); + + assertTrue(allSources(topics)); + assertTrue(allSinks(topics)); + } + + @Test + void testAddTopicsTopicParameterGroupNull() { + TopicEndpoint manager = new TopicEndpointProxy(); + + List<Topic> topics = manager.addTopics(new TopicParameterGroup()); + assertEquals(0, topics.size()); + } + + @Test + void testLockSinks_lockSources_locked() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.lock(); + for (Topic topic : manager.addTopics(group)) { + assertTrue(topic.isLocked()); + } + } + + @Test + void testLockSinks_lockSources_unlocked() { + TopicEndpoint manager = new TopicEndpointProxy(); + for (Topic topic : manager.addTopics(group)) { + assertFalse(topic.isLocked()); + } + } + + @Test + void testGetTopicSources() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSources(configuration); + manager.addTopicSinks(configuration); + + List<TopicSource> sources = manager.getTopicSources(); + assertSame(1, sources.size()); + + assertTrue(allSources(sources)); + assertFalse(anySink(sources)); + + assertThatThrownBy(() -> manager.getKafkaTopicSource("testTopic")) + .hasMessageContaining("KafkaTopiceSource for testTopic not found"); + + List<String> topicName = null; + assertThatThrownBy(() -> manager.getTopicSources(topicName)) + .hasMessageContaining("must provide a list of topics"); + } + + @Test + void testGetTopicSinks() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSources(configuration); + manager.addTopicSinks(configuration); + + List<TopicSink> sinks = manager.getTopicSinks(); + assertSame(1, sinks.size()); + + assertFalse(anySource(sinks)); + assertTrue(allSinks(sinks)); + + final List<String> sinks2 = null; + assertThatThrownBy(() -> manager.getTopicSinks(sinks2)).hasMessageContaining("must provide a list of topics"); + + List<String> sinks3 = List.of(NOOP_SINK_TOPIC); + assertThatCode(() -> manager.getTopicSinks(sinks3)).doesNotThrowAnyException(); + + String sinkTest = null; + assertThatThrownBy(() -> manager.getTopicSinks(sinkTest)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid parameter"); + + assertThatThrownBy(() -> manager.getKafkaTopicSink("testTopic")) + .hasMessageContaining("KafkaTopicSink for testTopic not found"); + } + + @Test + void testGetNoopTopicSources() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSources(configuration); + assertSame(1, manager.getNoopTopicSources().size()); + } + + @Test + void testGetNoopTopicSinks() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.addTopicSinks(configuration); + assertSame(1, manager.getNoopTopicSinks().size()); + } + + @Test + void testLifecycle() { + TopicEndpoint manager = new TopicEndpointProxy(); + + assertTrue(manager.start()); + assertTrue(manager.isAlive()); + + assertTrue(manager.stop()); + assertFalse(manager.isAlive()); + + assertTrue(manager.start()); + assertTrue(manager.isAlive()); + + manager.shutdown(); + assertFalse(manager.isAlive()); + } + + @Test + void testLock() { + TopicEndpoint manager = new TopicEndpointProxy(); + + manager.lock(); + assertTrue(manager.isLocked()); + + manager.unlock(); + assertFalse(manager.isLocked()); + } + + @Test + void testGetTopicSource() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.addTopicSources(configuration); + + assertSame(NOOP_SOURCE_TOPIC, manager.getTopicSource(CommInfrastructure.NOOP, NOOP_SOURCE_TOPIC).getTopic()); + + assertThatIllegalStateException() + .isThrownBy(() -> manager.getTopicSource(CommInfrastructure.NOOP, NOOP_SINK_TOPIC)); + } + + @Test + void testGetTopicSink() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.addTopicSinks(configuration); + + assertSame(NOOP_SINK_TOPIC, manager.getTopicSink(CommInfrastructure.NOOP, NOOP_SINK_TOPIC).getTopic()); + + assertThatIllegalStateException() + .isThrownBy(() -> manager.getTopicSink(CommInfrastructure.NOOP, NOOP_SOURCE_TOPIC)); + } + + @Test + void testGetNoopTopicSource() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.addTopicSources(configuration); + + assertSame(NOOP_SOURCE_TOPIC, manager.getNoopTopicSource(NOOP_SOURCE_TOPIC).getTopic()); + + assertThatIllegalArgumentException().isThrownBy(() -> manager.getNoopTopicSource(null)); + assertThatIllegalArgumentException().isThrownBy(() -> manager.getNoopTopicSource("")); + } + + @Test + void testGetNoopTopicSink() { + TopicEndpoint manager = new TopicEndpointProxy(); + manager.addTopicSinks(configuration); + + assertSame(NOOP_SINK_TOPIC, manager.getNoopTopicSink(NOOP_SINK_TOPIC).getTopic()); + + assertThatIllegalArgumentException().isThrownBy(() -> manager.getNoopTopicSink(null)); + assertThatIllegalArgumentException().isThrownBy(() -> manager.getNoopTopicSink("")); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/TopicParameterGroupTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/TopicParameterGroupTest.java new file mode 100644 index 00000000..db28892e --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/TopicParameterGroupTest.java @@ -0,0 +1,147 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019-2024 Nordix Foundation. + * Modifications Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.ValidationResult; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.parameters.topic.TopicParameterGroup; +import org.onap.policy.common.parameters.topic.TopicParameters; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; + +/** + * Class to perform unit test of {@link TopicParameterGroup}. + * + * @author Ajith Sreekumar (ajith.sreekumar@est.tech) + */ +class TopicParameterGroupTest { + private static final CommonTestData testData = new CommonTestData(); + private static final Coder coder = new StandardCoder(); + private final String packageDir = "src/test/resources/org/onap/policy/common/message/bus/parameters/"; + + @Test + void test() throws CoderException { + final TopicParameterGroup topicParameterGroup = + testData.toObject(testData.getTopicParameterGroupMap(false), TopicParameterGroup.class); + final ValidationResult validationResult = topicParameterGroup.validate(); + assertTrue(validationResult.isValid()); + assertEquals(CommonTestData.TOPIC_PARAMS, topicParameterGroup.getTopicSinks()); + assertEquals(CommonTestData.TOPIC_PARAMS, topicParameterGroup.getTopicSources()); + + // these should default to true + assertTrue(new TopicParameters().isManaged()); + assertTrue(coder.decode("{}", TopicParameters.class).isManaged()); + + // but can be overridden + assertFalse(coder.decode("{'managed':false}".replace('\'', '"'), TopicParameters.class).isManaged()); + } + + @Test + void testValidate() { + final TopicParameterGroup topicParameterGroup = + testData.toObject(testData.getTopicParameterGroupMap(false), TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertNull(result.getResult()); + assertTrue(result.isValid()); + } + + @Test + void test_valid() throws Exception { + String json = testData.getParameterGroupAsString( + packageDir + "TopicParameters_valid.json"); + TopicParameterGroup topicParameterGroup = coder.decode(json, TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertNull(result.getResult()); + assertTrue(result.isValid()); + } + + @Test + void test_invalid() throws Exception { + String json = testData.getParameterGroupAsString( + packageDir + "TopicParameters_invalid.json"); + TopicParameterGroup topicParameterGroup = coder.decode(json, TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertFalse(result.isValid()); + assertTrue(result.getResult().contains("INVALID")); + } + + @Test + void test_missing_mandatory_params() throws Exception { + String json = testData.getParameterGroupAsString( + packageDir + "TopicParameters_missing_mandatory.json"); + TopicParameterGroup topicParameterGroup = coder.decode(json, TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertTrue(result.getResult().contains("Mandatory parameters are missing")); + assertFalse(result.isValid()); + } + + @Test + void test_allParams() throws Exception { + String json = testData.getParameterGroupAsString( + packageDir + "TopicParameters_all_params.json"); + TopicParameterGroup topicParameterGroup = coder.decode(json, TopicParameterGroup.class); + final ValidationResult result = topicParameterGroup.validate(); + assertNull(result.getResult()); + assertTrue(result.isValid()); + assertTrue(checkIfAllParamsNotEmpty(topicParameterGroup.getTopicSinks())); + assertTrue(checkIfAllParamsNotEmpty(topicParameterGroup.getTopicSources())); + } + + /** + * Method to check if all parameters in TopicParameters are set. + * Any parameters added to @link TopicParameters or @link BusTopicParams must be added to + * TopicParameters_all_params.json. + * + * @param topicParametersList list of topic parameters + * @return true if all parameters are not empty (if string) or true (if boolean) + * @throws Exception the exception + */ + private boolean checkIfAllParamsNotEmpty(List<TopicParameters> topicParametersList) throws Exception { + for (TopicParameters topicParameters : topicParametersList) { + Field[] fields = BusTopicParams.class.getDeclaredFields(); + for (Field field : fields) { + if (!field.isSynthetic() && !Modifier.isStatic(field.getModifiers())) { + Object parameter = new PropertyDescriptor(field.getName(), TopicParameters.class).getReadMethod() + .invoke(topicParameters); + if ((parameter instanceof String && StringUtils.isBlank(parameter.toString())) + || (parameter instanceof Boolean && !(Boolean) parameter) + || (parameter instanceof Number && ((Number) parameter).longValue() == 0)) { + return false; + } + } + } + } + return true; + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/BusConsumerTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/BusConsumerTest.java new file mode 100644 index 00000000..207023e5 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/BusConsumerTest.java @@ -0,0 +1,282 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.onap.policy.common.message.bus.event.base.BusConsumer.FetchingBusConsumer; +import org.onap.policy.common.message.bus.event.base.BusConsumer.KafkaConsumerWrapper; +import org.onap.policy.common.message.bus.properties.MessageBusProperties; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class BusConsumerTest extends TopicTestBase { + + private static final int SHORT_TIMEOUT_MILLIS = 10; + private static final int LONG_TIMEOUT_MILLIS = 3000; + + @Mock + KafkaConsumer<String, String> mockedKafkaConsumer; + + AutoCloseable closeable; + + @BeforeEach + @Override + public void setUp() { + super.setUp(); + closeable = MockitoAnnotations.openMocks(this); + } + + @AfterEach + public void tearDown() throws Exception { + closeable.close(); + } + + + @Test + void testFetchingBusConsumer() { + // should not be negative + var cons = new FetchingBusConsumerImpl(makeBuilder().fetchTimeout(-1).build()); + assertThat(cons.getSleepTime()).isEqualTo(MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH); + + // should not be zero + cons = new FetchingBusConsumerImpl(makeBuilder().fetchTimeout(0).build()); + assertThat(cons.getSleepTime()).isEqualTo(MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH); + + // should not be too large + cons = new FetchingBusConsumerImpl( + makeBuilder().fetchTimeout(MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH + 100).build()); + assertThat(cons.getSleepTime()).isEqualTo(MessageBusProperties.DEFAULT_TIMEOUT_MS_FETCH); + + // should not be what was specified + cons = new FetchingBusConsumerImpl(makeBuilder().fetchTimeout(100).build()); + assertThat(cons.getSleepTime()).isEqualTo(100); + } + + @Test + void testFetchingBusConsumerSleepAfterFetchFailure() throws InterruptedException { + + var cons = new FetchingBusConsumerImpl(makeBuilder().fetchTimeout(SHORT_TIMEOUT_MILLIS).build()) { + + private CountDownLatch started = new CountDownLatch(1); + + @Override + protected void sleepAfterFetchFailure() { + started.countDown(); + super.sleepAfterFetchFailure(); + } + }; + + // full sleep + long tstart = System.currentTimeMillis(); + cons.sleepAfterFetchFailure(); + assertThat(System.currentTimeMillis() - tstart).isGreaterThanOrEqualTo(SHORT_TIMEOUT_MILLIS); + + // close while sleeping - sleep should halt prematurely + cons.fetchTimeout = LONG_TIMEOUT_MILLIS; + cons.started = new CountDownLatch(1); + Thread thread = new Thread(cons::sleepAfterFetchFailure); + tstart = System.currentTimeMillis(); + thread.start(); + cons.started.await(); + cons.close(); + thread.join(); + assertThat(System.currentTimeMillis() - tstart).isLessThan(LONG_TIMEOUT_MILLIS); + + // interrupt while sleeping - sleep should halt prematurely + cons.fetchTimeout = LONG_TIMEOUT_MILLIS; + cons.started = new CountDownLatch(1); + thread = new Thread(cons::sleepAfterFetchFailure); + tstart = System.currentTimeMillis(); + thread.start(); + cons.started.await(); + thread.interrupt(); + thread.join(); + assertThat(System.currentTimeMillis() - tstart).isLessThan(LONG_TIMEOUT_MILLIS); + } + + @Test + void testKafkaConsumerWrapper() { + // verify that different wrappers can be built + assertThatCode(() -> new KafkaConsumerWrapper(makeKafkaBuilder().build())).doesNotThrowAnyException(); + } + + @Test + void testKafkaConsumerWrapper_InvalidTopic() { + BusTopicParams params = makeBuilder().topic(null).build(); + assertThatThrownBy(() -> new KafkaConsumerWrapper(params)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testKafkaConsumerWrapperFetch() { + + //Setup Properties for consumer + Properties kafkaProps = new Properties(); + kafkaProps.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + kafkaProps.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "test"); + kafkaProps.setProperty("enable.auto.commit", "true"); + kafkaProps.setProperty("auto.commit.interval.ms", "1000"); + kafkaProps.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + kafkaProps.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + kafkaProps.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + kafkaProps.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); + + KafkaConsumerWrapper kafka = new KafkaConsumerWrapper(makeKafkaBuilder().build()); + KafkaConsumer<String, String> consumer = new KafkaConsumer<>(kafkaProps); + kafka.consumer = consumer; + + assertThrows(java.lang.IllegalStateException.class, () -> kafka.fetch().iterator().hasNext()); + consumer.close(); + } + + @Test + void testFetchNoMessages() { + KafkaConsumerWrapper kafkaConsumerWrapper = new KafkaConsumerWrapper(makeKafkaBuilder().build()); + kafkaConsumerWrapper.consumer = mockedKafkaConsumer; + + when(mockedKafkaConsumer.poll(any())).thenReturn(new ConsumerRecords<>(Collections.emptyMap())); + + Iterable<String> result = kafkaConsumerWrapper.fetch(); + + verify(mockedKafkaConsumer).poll(any()); + + assertNotNull(result); + + assertFalse(result.iterator().hasNext()); + + mockedKafkaConsumer.close(); + } + + @Test + void testFetchWithMessages() { + // Setup + KafkaConsumerWrapper kafkaConsumerWrapper = new KafkaConsumerWrapper(makeKafkaBuilder().build()); + kafkaConsumerWrapper.consumer = mockedKafkaConsumer; + + ConsumerRecord<String, String> customerRecord = + new ConsumerRecord<>("my-effective-topic", 0, 0, "key", "value"); + Map<TopicPartition, List<ConsumerRecord<String, String>>> recordsMap = new HashMap<>(); + recordsMap.put(new TopicPartition("my-effective-topic", 0), Collections.singletonList(customerRecord)); + ConsumerRecords<String, String> consumerRecords = new ConsumerRecords<>(recordsMap); + + when(mockedKafkaConsumer.poll(any())).thenReturn(consumerRecords); + + Iterable<String> result = kafkaConsumerWrapper.fetch(); + + verify(mockedKafkaConsumer, times(1)).poll(any()); + + verify(mockedKafkaConsumer, times(1)).commitSync(any(Map.class)); + + assertNotNull(result); + + assertTrue(result.iterator().hasNext()); + + assertEquals("value", result.iterator().next()); + + mockedKafkaConsumer.close(); + } + + @Test + void testFetchWithMessagesAndTraceParent() { + // Setup + KafkaConsumerWrapper kafkaConsumerWrapper = new KafkaConsumerWrapper(makeKafkaBuilder().build()); + kafkaConsumerWrapper.consumer = mockedKafkaConsumer; + + ConsumerRecord<String, String> customerRecord = + new ConsumerRecord<>("my-effective-topic", 0, 0, "key", "value"); + customerRecord.headers().add( + "traceparent", + "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".getBytes(StandardCharsets.UTF_8) + ); + + Map<TopicPartition, List<ConsumerRecord<String, String>>> recordsMap = new HashMap<>(); + recordsMap.put(new TopicPartition("my-effective-topic", 0), Collections.singletonList(customerRecord)); + ConsumerRecords<String, String> consumerRecords = new ConsumerRecords<>(recordsMap); + + when(mockedKafkaConsumer.poll(any())).thenReturn(consumerRecords); + + Iterable<String> result = kafkaConsumerWrapper.fetch(); + + verify(mockedKafkaConsumer, times(1)).poll(any()); + + verify(mockedKafkaConsumer, times(1)).commitSync(any(Map.class)); + + assertNotNull(result); + + assertTrue(result.iterator().hasNext()); + + assertEquals("value", result.iterator().next()); + + mockedKafkaConsumer.close(); + } + + + @Test + void testKafkaConsumerWrapperClose() { + assertThatCode(() -> new KafkaConsumerWrapper(makeKafkaBuilder().build()).close()).doesNotThrowAnyException(); + } + + @Test + void testKafkaConsumerWrapperToString() { + assertNotNull(new KafkaConsumerWrapper(makeKafkaBuilder().build()) {}.toString()); + } + + private static class FetchingBusConsumerImpl extends FetchingBusConsumer { + + protected FetchingBusConsumerImpl(BusTopicParams busTopicParams) { + super(busTopicParams); + } + + @Override + public Iterable<String> fetch() { + return null; + } + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.java new file mode 100644 index 00000000..343a56a8 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.java @@ -0,0 +1,139 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.gson.GsonTestUtils; + +class BusTopicBaseTest extends TopicTestBase { + + private BusTopicBaseImpl base; + + /** + * Initializes the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + base = new BusTopicBaseImpl(builder.build()); + } + + @Test + void testToString() { + assertNotNull(base.toString()); + } + + @Test + void testSerialize() { + assertThatCode(() -> new GsonTestUtils().compareGson(base, BusTopicBaseTest.class)).doesNotThrowAnyException(); + } + + @Test + void testGetApiKey() { + assertEquals(MY_API_KEY, base.getApiKey()); + } + + @Test + void testGetApiSecret() { + assertEquals(MY_API_SECRET, base.getApiSecret()); + } + + @Test + void testIsUseHttps() { + assertTrue(base.isUseHttps()); + assertFalse(new BusTopicBaseImpl(builder.useHttps(false).build()).isUseHttps()); + } + + @Test + void testIsAllowSelfSignedCerts() { + assertTrue(base.isAllowSelfSignedCerts()); + assertFalse(new BusTopicBaseImpl(builder.allowSelfSignedCerts(false).build()).isAllowSelfSignedCerts()); + } + + @Test + void testTopic() { + assertEquals(MY_TOPIC, base.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, base.getEffectiveTopic()); + assertNotEquals(base.getTopic(), base.getEffectiveTopic()); + } + + @Test + void testAnyNullOrEmpty() { + assertFalse(base.anyNullOrEmpty()); + assertFalse(base.anyNullOrEmpty("any-none-null", "any-none-null-B")); + + assertTrue(base.anyNullOrEmpty(null, "any-first-null")); + assertTrue(base.anyNullOrEmpty("any-middle-null", null, "any-middle-null-B")); + assertTrue(base.anyNullOrEmpty("any-last-null", null)); + assertTrue(base.anyNullOrEmpty("any-empty", "")); + } + + @Test + void testAllNullOrEmpty() { + assertTrue(base.allNullOrEmpty()); + assertTrue(base.allNullOrEmpty("")); + assertTrue(base.allNullOrEmpty(null, "")); + + assertFalse(base.allNullOrEmpty("all-ok-only-one")); + assertFalse(base.allNullOrEmpty("all-ok-one", "all-ok-two")); + assertFalse(base.allNullOrEmpty("all-ok-null", null)); + assertFalse(base.allNullOrEmpty("", "all-ok-empty")); + assertFalse(base.allNullOrEmpty("", "all-one-ok", null)); + } + + private static class BusTopicBaseImpl extends BusTopicBase { + + public BusTopicBaseImpl(BusTopicParams busTopicParams) { + super(busTopicParams); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + + @Override + public void shutdown() { + // do nothing + } + + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicFactoryTestBase.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicFactoryTestBase.java new file mode 100644 index 00000000..bd531114 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicFactoryTestBase.java @@ -0,0 +1,238 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_HTTP_HTTPS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; + +import java.util.List; +import java.util.Properties; +import java.util.function.Predicate; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +/** + * Base class for Topic Factory tests that use BusTopicParams. + * + * @param <T> type of topic managed by the factory + */ +public abstract class BusTopicFactoryTestBase<T extends Topic> extends TopicFactoryTestBase<T> { + + /** + * Builds a topic. + * + * @param params the parameters used to configure the topic + * @return a new topic + */ + protected abstract T buildTopic(BusTopicParams params); + + /** + * Builds a topic. + * + * @param servers list of servers + * @param topic the topic name + * @return a new topic + */ + protected abstract T buildTopic(List<String> servers, String topic); + + /** + * Gets the parameters used to build the most recent topic. + * + * @return the most recent topic's parameters + */ + protected abstract BusTopicParams getLastParams(); + + /** + * Tests building a topic using BusTopicParams. + */ + public void testBuildBusTopicParams() { + initFactory(); + + // two unmanaged topics + T item = buildTopic(makeBuilder().managed(false).effectiveTopic(null).build()); + T item2 = buildTopic(makeBuilder().managed(false).topic(TOPIC2).build()); + assertNotNull(item); + assertNotNull(item2); + assertEquals(item.getTopic(), item.getEffectiveTopic()); + assertNotEquals(item2.getTopic(), item2.getEffectiveTopic()); + assertNotSame(item, item2); + + // duplicate topics, but since they aren't managed, they should be different + T item3 = buildTopic(makeBuilder().managed(false).build()); + T item4 = buildTopic(makeBuilder().managed(false).effectiveTopic(TOPIC2).build()); + assertNotNull(item3); + assertNotNull(item4); + assertEquals(MY_TOPIC, item4.getTopic()); + assertEquals(TOPIC2, item4.getEffectiveTopic()); + assertNotSame(item, item3); + assertNotSame(item, item4); + assertNotSame(item3, item4); + + // two managed topics + T item5 = buildTopic(makeBuilder().build()); + T item6 = buildTopic(makeBuilder().topic(TOPIC2).build()); + assertNotNull(item5); + assertNotNull(item6); + + // re-build same managed topics - should get exact same objects + assertSame(item5, buildTopic(makeBuilder().topic(MY_TOPIC).build())); + assertSame(item6, buildTopic(makeBuilder().topic(TOPIC2).build())); + } + + /** + * Tests exception cases when building a topic using BusTopicParams. + */ + public void testBuildBusTopicParams_Ex() { + // null topic + assertThatIllegalArgumentException().isThrownBy(() -> buildTopic(makeBuilder().topic(null).build())); + + // empty topic + assertThatIllegalArgumentException().isThrownBy(() -> buildTopic(makeBuilder().topic("").build())); + } + + /** + * Tests building a topic using a list of servers and a topic. + */ + public void testBuildListOfStringString() { + initFactory(); + + T item1 = buildTopic(servers, MY_TOPIC); + assertNotNull(item1); + + // check parameters that were used + BusTopicParams params = getLastParams(); + assertEquals(servers, params.getServers()); + assertEquals(MY_TOPIC, params.getTopic()); + assertTrue(params.isManaged()); + assertFalse(params.isUseHttps()); + + T item2 = buildTopic(servers, TOPIC2); + assertNotNull(item2); + assertNotSame(item1, item2); + + // duplicate - should be the same, as these topics are managed + T item3 = buildTopic(servers, TOPIC2); + assertSame(item2, item3); + } + + /** + * Tests building a topic using Properties. Verifies parameters specific to Bus + * topics. + */ + public void testBuildProperties() { + initFactory(); + + List<T> topics = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()); + assertEquals(1, topics.size()); + assertEquals(MY_TOPIC, topics.get(0).getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, topics.get(0).getEffectiveTopic()); + + BusTopicParams params = getLastParams(); + assertTrue(params.isManaged()); + assertTrue(params.isUseHttps()); + assertTrue(params.isAllowSelfSignedCerts()); + assertEquals(MY_API_KEY, params.getApiKey()); + assertEquals(MY_API_SECRET, params.getApiSecret()); + assertEquals(List.of(SERVER), params.getServers()); + assertEquals(MY_TOPIC, params.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, params.getEffectiveTopic()); + + List<T> topics2 = buildTopics(makePropBuilder().makeTopic(TOPIC3) + .removeTopicProperty(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX).build()); + assertEquals(1, topics2.size()); + assertEquals(TOPIC3, topics2.get(0).getTopic()); + assertEquals(topics2.get(0).getTopic(), topics2.get(0).getEffectiveTopic()); + } + + @Override + void testBuildProperties_Variations() { + super.testBuildProperties_Variations(); + + // check boolean properties that default to true + checkDefault(PROPERTY_MANAGED_SUFFIX, BusTopicParams::isManaged); + + // check boolean properties that default to false + checkDefault(PROPERTY_HTTP_HTTPS_SUFFIX, params -> !params.isUseHttps()); + checkDefault(PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX, params -> !params.isAllowSelfSignedCerts()); + } + + /** + * Verifies that a parameter has the correct default, if the original builder property + * is not provided. + * + * @param builderName name of the builder property + * @param validate function to test the validity of the property + * @param values the values to which the property should be set, defaults to + * {@code null} and "" + */ + protected void checkDefault(String builderName, Predicate<BusTopicParams> validate, Object... values) { + Object[] values2 = (values.length > 0 ? values : new Object[] {null, ""}); + + for (Object value : values2) { + // always start with a fresh factory + initFactory(); + + TopicPropertyBuilder builder = makePropBuilder().makeTopic(MY_TOPIC); + + if (value == null) { + builder.removeTopicProperty(builderName); + + } else { + builder.setTopicProperty(builderName, value.toString()); + } + + assertEquals(1, buildTopics(builder.build()).size(), "size for default " + value); + assertTrue(validate.test(getLastParams()), "default for " + value); + } + } + + /** + * Verifies that an "additional" property does not exist, if the original builder + * property is not provided. + * + * @param builderName name of the builder property + * @param addName name of the "additional" property + */ + public void expectNullAddProp(String builderName, String addName) { + + // remove the property + initFactory(); + Properties props = makePropBuilder().makeTopic(MY_TOPIC).removeTopicProperty(builderName).build(); + assertEquals(1, buildTopics(props).size()); + assertFalse(getLastParams().getAdditionalProps().containsKey(addName)); + + + // repeat, this time using an empty string instead of null + initFactory(); + props = makePropBuilder().makeTopic(MY_TOPIC).setTopicProperty(builderName, "").build(); + assertEquals(1, buildTopics(props).size()); + assertFalse(getLastParams().getAdditionalProps().containsKey(addName)); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.java new file mode 100644 index 00000000..820fc2c3 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.java @@ -0,0 +1,230 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.gson.GsonTestUtils; + +class InlineBusTopicSinkTest extends TopicTestBase { + + private InlineBusTopicSinkImpl sink; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + sink = new InlineBusTopicSinkImpl(makeBuilder().build()); + } + + @AfterEach + public void tearDown() { + sink.shutdown(); + } + + @Test + void testSerialize() { + assertThatCode(() -> new GsonTestUtils().compareGson(sink, InlineBusTopicSinkTest.class)) + .doesNotThrowAnyException(); + } + + @Test + void testInlineBusTopicSinkImpl() { + // verify that different wrappers can be built + sink = new InlineBusTopicSinkImpl(makeBuilder().build()); + assertEquals(MY_PARTITION, sink.getPartitionKey()); + + sink = new InlineBusTopicSinkImpl(makeBuilder().partitionId(null).build()); + assertNotNull(sink.getPartitionKey()); + } + + @Test + void testStart() { + assertTrue(sink.start()); + assertEquals(1, sink.initCount); + + // re-start, init() should not be invoked again + assertTrue(sink.start()); + assertEquals(1, sink.initCount); + } + + @Test + void testStart_Locked() { + sink.lock(); + assertThatThrownBy(() -> sink.start()).isInstanceOf(IllegalStateException.class); + } + + @Test + void testStop() { + BusPublisher pub = mock(BusPublisher.class); + sink.publisher = pub; + + assertTrue(sink.stop()); + verify(pub).close(); + + // stop again, shouldn't not invoke close() again + assertFalse(sink.stop()); + verify(pub).close(); + + // publisher throws exception + sink = new InlineBusTopicSinkImpl(makeBuilder().build()); + sink.publisher = pub; + doThrow(new RuntimeException(EXPECTED)).when(pub).close(); + assertTrue(sink.stop()); + } + + @Test + void testSend() { + sink.start(); + BusPublisher pub = mock(BusPublisher.class); + sink.publisher = pub; + + TopicListener listener = mock(TopicListener.class); + sink.register(listener); + + assertTrue(sink.send(MY_MESSAGE)); + + verify(pub).send(MY_PARTITION, MY_MESSAGE); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + assertEquals(List.of(MY_MESSAGE), Arrays.asList(sink.getRecentEvents())); + + // arrange for send to throw an exception + when(pub.send(anyString(), anyString())).thenThrow(new RuntimeException(EXPECTED)); + + assertFalse(sink.send(MY_MESSAGE)); + + // no more event deliveries + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + } + + @Test + void testSend_NullMessage() { + sink.start(); + sink.publisher = mock(BusPublisher.class); + + assertThatThrownBy(() -> sink.send(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testSend_EmptyMessage() { + sink.start(); + sink.publisher = mock(BusPublisher.class); + + assertThatThrownBy(() -> sink.send("")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testSend_NotStarted() { + sink.publisher = mock(BusPublisher.class); + assertThatThrownBy(() -> sink.send(MY_MESSAGE)).isInstanceOf(IllegalStateException.class); + } + + @Test + void testSetPartitionKey_getPartitionKey() { + assertEquals(MY_PARTITION, sink.getPartitionKey()); + + sink.setPartitionKey("part-B"); + assertEquals("part-B", sink.getPartitionKey()); + } + + @Test + void testShutdown() { + BusPublisher pub = mock(BusPublisher.class); + sink.publisher = pub; + + sink.shutdown(); + verify(pub).close(); + } + + @Test + void testAnyNullOrEmpty() { + assertFalse(sink.anyNullOrEmpty()); + assertFalse(sink.anyNullOrEmpty("any-none-null", "any-none-null-B")); + + assertTrue(sink.anyNullOrEmpty(null, "any-first-null")); + assertTrue(sink.anyNullOrEmpty("any-middle-null", null, "any-middle-null-B")); + assertTrue(sink.anyNullOrEmpty("any-last-null", null)); + assertTrue(sink.anyNullOrEmpty("any-empty", "")); + } + + @Test + void testAllNullOrEmpty() { + assertTrue(sink.allNullOrEmpty()); + assertTrue(sink.allNullOrEmpty("")); + assertTrue(sink.allNullOrEmpty(null, "")); + + assertFalse(sink.allNullOrEmpty("all-ok-only-one")); + assertFalse(sink.allNullOrEmpty("all-ok-one", "all-ok-two")); + assertFalse(sink.allNullOrEmpty("all-ok-null", null)); + assertFalse(sink.allNullOrEmpty("", "all-ok-empty")); + assertFalse(sink.allNullOrEmpty("", "all-one-ok", null)); + } + + @Test + void testToString() { + assertTrue(sink.toString().startsWith("InlineBusTopicSink [")); + } + + /** + * Implementation of InlineBusTopicSink that tracks the number of times that init() is + * invoked. + */ + private static class InlineBusTopicSinkImpl extends InlineBusTopicSink { + + private int initCount = 0; + + public InlineBusTopicSinkImpl(BusTopicParams busTopicParams) { + super(busTopicParams); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public void init() { + ++initCount; + } + + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.java new file mode 100644 index 00000000..8ad8e8fd --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.java @@ -0,0 +1,375 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.utils.gson.GsonTestUtils; +import org.onap.policy.common.utils.network.NetworkUtil; + +class SingleThreadedBusTopicSourceTest extends TopicTestBase { + private Thread thread; + private BusConsumer cons; + private TopicListener listener; + private SingleThreadedBusTopicSourceImpl source; + + /** + * Creates the object to be tested, as well as various mocks. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + thread = mock(Thread.class); + cons = mock(BusConsumer.class); + listener = mock(TopicListener.class); + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().build()); + } + + @AfterEach + public void tearDown() { + source.shutdown(); + } + + @Test + void testSerialize() { + assertThatCode(() -> new GsonTestUtils().compareGson(source, SingleThreadedBusTopicSourceTest.class)) + .doesNotThrowAnyException(); + } + + @Test + void testRegister() { + source.register(listener); + assertEquals(1, source.initCount); + source.offer(MY_MESSAGE); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + + // register another - should not re-init + TopicListener listener2 = mock(TopicListener.class); + source.register(listener2); + assertEquals(1, source.initCount); + source.offer(MY_MESSAGE + "z"); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE + "z"); + verify(listener2).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE + "z"); + + // re-register - should not re-init + source.register(listener); + assertEquals(1, source.initCount); + source.offer(MY_MESSAGE2); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE2); + + // lock & register - should not init + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().build()); + source.lock(); + source.register(listener); + assertEquals(0, source.initCount); + + // exception during init + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().build()); + source.initEx = true; + source.register(listener); + } + + @Test + void testUnregister() { + TopicListener listener2 = mock(TopicListener.class); + source.register(listener); + source.register(listener2); + + // unregister first listener - should NOT invoke close + source.unregister(listener); + verify(cons, never()).close(); + assertEquals(Arrays.asList(listener2), source.snapshotTopicListeners()); + + // unregister same listener - should not invoke close + source.unregister(listener); + verify(cons, never()).close(); + assertEquals(Arrays.asList(listener2), source.snapshotTopicListeners()); + + // unregister second listener - SHOULD invoke close + source.unregister(listener2); + verify(cons).close(); + assertTrue(source.snapshotTopicListeners().isEmpty()); + + // unregister same listener - should not invoke close again + source.unregister(listener2); + verify(cons).close(); + assertTrue(source.snapshotTopicListeners().isEmpty()); + } + + @Test + void testToString() { + assertTrue(source.toString().startsWith("SingleThreadedBusTopicSource [")); + } + + @Test + void testMakePollerThread() { + SingleThreadedBusTopicSource source2 = new SingleThreadedBusTopicSource(makeBuilder().build()) { + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public void init() throws MalformedURLException { + // do nothing + } + }; + + assertNotNull(source2.makePollerThread()); + } + + @Test + void testSingleThreadedBusTopicSource() { + // Note: if the value contains "-", it's probably a UUID + + // verify that different wrappers can be built + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().build()); + assertThat(source.getConsumerGroup()).isEqualTo(MY_CONS_GROUP); + assertThat(source.getConsumerInstance()).isEqualTo(MY_CONS_INST); + + // group is null => group is UUID, instance is as provided + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().consumerGroup(null).build()); + assertThat(source.getConsumerGroup()).contains("-").isNotEqualTo(NetworkUtil.getHostname()); + assertThat(source.getConsumerInstance()).isEqualTo(MY_CONS_INST); + + // instance is null => group is as provided, instance is UUID + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().consumerInstance(null).build()); + assertThat(source.getConsumerGroup()).isEqualTo(MY_CONS_GROUP); + assertThat(source.getConsumerInstance()).contains("-").isNotEqualTo(NetworkUtil.getHostname()); + + // group & instance are null => group is UUID, instance is hostname + source = new SingleThreadedBusTopicSourceImpl(makeBuilder().consumerGroup(null).consumerInstance(null).build()); + assertThat(source.getConsumerGroup()).contains("-").isNotEqualTo(NetworkUtil.getHostname()); + assertThat(source.getConsumerInstance()).isEqualTo(NetworkUtil.getHostname()); + + assertThatCode(() -> new SingleThreadedBusTopicSourceImpl( + makeBuilder().fetchLimit(-1).fetchTimeout(-1).build())).doesNotThrowAnyException(); + } + + @Test + void testStart() { + source.start(); + assertTrue(source.isAlive()); + assertEquals(1, source.initCount); + verify(thread).start(); + + // attempt to start again - nothing should be invoked again + source.start(); + assertTrue(source.isAlive()); + assertEquals(1, source.initCount); + verify(thread).start(); + + // stop & re-start + source.stop(); + source.start(); + assertTrue(source.isAlive()); + assertEquals(2, source.initCount); + verify(thread, times(2)).start(); + } + + @Test + void testStart_Locked() { + source.lock(); + assertThatThrownBy(() -> source.start()).isInstanceOf(IllegalStateException.class); + } + + @Test + void testStart_InitEx() { + assertThatThrownBy(() -> { + source.initEx = true; + + source.start(); + }).isInstanceOf(IllegalStateException.class); + } + + @Test + void testStop() { + source.start(); + source.stop(); + verify(cons).close(); + + // stop it again - not re-closed + source.stop(); + verify(cons).close(); + + // start & stop again, but with an exception + doThrow(new RuntimeException(EXPECTED)).when(cons).close(); + source.start(); + source.stop(); + } + + @Test + void testRun() throws Exception { + source.register(listener); + + /* + * Die in the middle of fetching messages. Also, throw an exception during the + * first fetch attempt. + */ + when(cons.fetch()).thenAnswer(new Answer<Iterable<String>>() { + int count = 0; + + @Override + public Iterable<String> answer(InvocationOnMock invocation) throws Throwable { + if (++count > 1) { + source.alive = false; + return Arrays.asList(MY_MESSAGE, MY_MESSAGE2); + + } else { + throw new IOException(EXPECTED); + } + } + }); + source.alive = true; + source.run(); + assertEquals(Arrays.asList(MY_MESSAGE), Arrays.asList(source.getRecentEvents())); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + verify(listener, never()).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE2); + + /* + * Die AFTER fetching messages. + */ + final String msga = "message-A"; + final String msgb = "message-B"; + when(cons.fetch()).thenAnswer(new Answer<Iterable<String>>() { + int count = 0; + + @Override + public Iterable<String> answer(InvocationOnMock invocation) throws Throwable { + if (++count > 1) { + source.alive = false; + return Collections.emptyList(); + + } else { + return Arrays.asList(msga, msgb); + } + } + }); + source.alive = true; + source.run(); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msga); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msgb); + + assertEquals(Arrays.asList(MY_MESSAGE, msga, msgb), Arrays.asList(source.getRecentEvents())); + } + + @Test + void testOffer() { + source.register(listener); + source.offer(MY_MESSAGE); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + assertEquals(Arrays.asList(MY_MESSAGE), Arrays.asList(source.getRecentEvents())); + } + + @Test + void testOffer_NotStarted() { + assertThatThrownBy(() -> source.offer(MY_MESSAGE)).isInstanceOf(IllegalStateException.class); + } + + @Test + void testGetConsumerGroup() { + assertEquals(MY_CONS_GROUP, source.getConsumerGroup()); + } + + @Test + void testGetConsumerInstance() { + assertEquals(MY_CONS_INST, source.getConsumerInstance()); + } + + @Test + void testShutdown() { + source.register(listener); + + source.shutdown(); + verify(cons).close(); + assertTrue(source.snapshotTopicListeners().isEmpty()); + } + + @Test + void testGetFetchTimeout() { + assertEquals(MY_FETCH_TIMEOUT, source.getFetchTimeout()); + } + + @Test + void testGetFetchLimit() { + assertEquals(MY_FETCH_LIMIT, source.getFetchLimit()); + } + + /** + * Implementation of SingleThreadedBusTopicSource that counts the number of times + * init() is invoked. + */ + private class SingleThreadedBusTopicSourceImpl extends SingleThreadedBusTopicSource { + + private int initCount = 0; + private boolean initEx = false; + + public SingleThreadedBusTopicSourceImpl(BusTopicParams busTopicParams) { + super(busTopicParams); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public void init() throws MalformedURLException { + ++initCount; + + if (initEx) { + throw new MalformedURLException(EXPECTED); + } + + consumer = cons; + } + + @Override + protected Thread makePollerThread() { + return thread; + } + + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicBaseTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicBaseTest.java new file mode 100644 index 00000000..5ecde258 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicBaseTest.java @@ -0,0 +1,355 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.utils.gson.GsonTestUtils; + +class TopicBaseTest extends TopicTestBase { + + private TopicBaseImpl base; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + base = new TopicBaseImpl(servers, MY_TOPIC); + } + + @Test + void testTopicBase_NullServers() { + assertThatThrownBy(() -> new TopicBaseImpl(null, MY_TOPIC)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testTopicBase_EmptyServers() { + List<String> testList = Collections.emptyList(); + assertThatThrownBy(() -> new TopicBaseImpl(testList, MY_TOPIC)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testTopicBase_NullTopic() { + assertThatThrownBy(() -> new TopicBaseImpl(servers, null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testTopicBase_EmptyTopic() { + assertThatThrownBy(() -> new TopicBaseImpl(servers, "")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testTopicBase_EffectiveTopic() { + TopicBase baseEf = new TopicBaseImpl(servers, MY_TOPIC, MY_EFFECTIVE_TOPIC); + assertEquals(MY_TOPIC, baseEf.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, baseEf.getEffectiveTopic()); + } + + @Test + void testTopicBase_NullEffectiveTopic() { + TopicBase baseEf = new TopicBaseImpl(servers, MY_TOPIC, null); + assertEquals(MY_TOPIC, baseEf.getTopic()); + assertEquals(MY_TOPIC, baseEf.getEffectiveTopic()); + } + + @Test + void testTopicBase_EmptyEffectiveTopic() { + TopicBase baseEf = new TopicBaseImpl(servers, MY_TOPIC, ""); + assertEquals(MY_TOPIC, baseEf.getTopic()); + assertEquals(MY_TOPIC, baseEf.getEffectiveTopic()); + } + + @Test + void testSerialize() { + assertThatCode(() -> new GsonTestUtils().compareGson(base, TopicBaseTest.class)).doesNotThrowAnyException(); + } + + @Test + void testRegister() { + TopicListener listener = mock(TopicListener.class); + base.register(listener); + assertEquals(List.of(listener), base.snapshotTopicListeners()); + + // re-register - list should be unchanged + base.register(listener); + assertEquals(List.of(listener), base.snapshotTopicListeners()); + + // register a new listener + TopicListener listener2 = mock(TopicListener.class); + base.register(listener2); + assertEquals(List.of(listener, listener2), base.snapshotTopicListeners()); + } + + @Test + void testRegister_NullListener() { + assertThatThrownBy(() -> base.register(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testUnregister() { + // register two listeners + TopicListener listener = mock(TopicListener.class); + TopicListener listener2 = mock(TopicListener.class); + base.register(listener); + base.register(listener2); + + // unregister one + base.unregister(listener); + assertEquals(List.of(listener2), base.snapshotTopicListeners()); + + // unregister the other + base.unregister(listener2); + assertTrue(base.snapshotTopicListeners().isEmpty()); + + // unregister again + base.unregister(listener2); + assertTrue(base.snapshotTopicListeners().isEmpty()); + } + + @Test + void testUnregister_NullListener() { + base.register(mock(TopicListener.class)); + assertThatThrownBy(() -> base.unregister(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testBroadcast() { + // register two listeners + TopicListener listener = mock(TopicListener.class); + TopicListener listener2 = mock(TopicListener.class); + base.register(listener); + base.register(listener2); + + // broadcast a message + final String msg1 = "message-A"; + base.broadcast(msg1); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msg1); + verify(listener2).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msg1); + + // broadcast another message, with an exception + final String msg2 = "message-B"; + doThrow(new RuntimeException(EXPECTED)).when(listener).onTopicEvent(any(), any(), any()); + base.broadcast(msg2); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msg2); + verify(listener2).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, msg2); + } + + @Test + void testLock_testUnlock() { + assertFalse(base.isLocked()); + assertTrue(base.lock()); + assertEquals(0, base.startCount); + assertEquals(1, base.stopCount); + + // lock again - should not stop again + assertTrue(base.isLocked()); + assertTrue(base.lock()); + assertEquals(0, base.startCount); + assertEquals(1, base.stopCount); + + assertTrue(base.isLocked()); + assertTrue(base.unlock()); + assertEquals(1, base.startCount); + assertEquals(1, base.stopCount); + + // unlock again - should not start again + assertFalse(base.isLocked()); + assertTrue(base.unlock()); + assertEquals(1, base.startCount); + assertEquals(1, base.stopCount); + } + + /** + * Tests lock/unlock when the stop/start methods return false. + */ + @Test + void testLock_testUnlock_FalseReturns() { + + // lock, but stop returns false + base.stopReturn = false; + assertFalse(base.lock()); + assertTrue(base.isLocked()); + assertTrue(base.lock()); + + // unlock, but start returns false + base.startReturn = false; + assertFalse(base.unlock()); + assertFalse(base.isLocked()); + assertTrue(base.unlock()); + } + + /** + * Tests lock/unlock when the start method throws an exception. + */ + @Test + void testLock_testUnlock_Exception() { + + // lock & re-lock, but start throws an exception + base.startEx = true; + assertTrue(base.lock()); + assertFalse(base.unlock()); + assertFalse(base.isLocked()); + assertTrue(base.unlock()); + } + + @Test + void testIsLocked() { + assertFalse(base.isLocked()); + base.lock(); + assertTrue(base.isLocked()); + base.unlock(); + assertFalse(base.isLocked()); + } + + @Test + void testGetTopic() { + assertEquals(MY_TOPIC, base.getTopic()); + } + + @Test + void testGetEffectiveTopic() { + assertEquals(MY_TOPIC, base.getTopic()); + assertEquals(MY_TOPIC, base.getEffectiveTopic()); + } + + @Test + void testIsAlive() { + assertFalse(base.isAlive()); + base.start(); + assertTrue(base.isAlive()); + base.stop(); + assertFalse(base.isAlive()); + } + + @Test + void testGetServers() { + assertEquals(servers, base.getServers()); + } + + @Test + void testGetRecentEvents() { + assertEquals(0, base.getRecentEvents().length); + + base.addEvent("recent-A"); + base.addEvent("recent-B"); + + String[] recent = base.getRecentEvents(); + assertEquals(2, recent.length); + assertEquals("recent-A", recent[0]); + assertEquals("recent-B", recent[1]); + } + + @Test + void testToString() { + assertNotNull(base.toString()); + } + + /** + * Implementation of TopicBase. + */ + private static class TopicBaseImpl extends TopicBase { + private int startCount = 0; + private int stopCount = 0; + private boolean startReturn = true; + private boolean stopReturn = true; + private boolean startEx = false; + + /** + * Constructor. + * + * @param servers list of servers + * @param topic topic name + */ + public TopicBaseImpl(List<String> servers, String topic) { + super(servers, topic); + } + + /** + * Constructor. + * + * @param servers list of servers + * @param topic topic name + * @param effectiveTopic effective topic name for network communication + */ + public TopicBaseImpl(List<String> servers, String topic, String effectiveTopic) { + super(servers, topic, effectiveTopic); + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + return CommInfrastructure.NOOP; + } + + @Override + public boolean start() { + ++startCount; + + if (startEx) { + throw new RuntimeException(EXPECTED); + } + + alive = true; + return startReturn; + } + + @Override + public boolean stop() { + ++stopCount; + alive = false; + return stopReturn; + } + + @Override + public void shutdown() { + // do nothing + } + + /** + * Adds an event to the list of recent events. + * + * @param event event to be added + */ + public void addEvent(String event) { + recentEvents.add(event); + } + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicFactoryTestBase.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicFactoryTestBase.java new file mode 100644 index 00000000..8444b482 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicFactoryTestBase.java @@ -0,0 +1,225 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.common.message.bus.event.Topic; + +/** + * Base class for XxxTopicFactory tests. + * + * @param <T> type of topic managed by the factory + */ +public abstract class TopicFactoryTestBase<T extends Topic> extends TopicTestBase { + + public static final String SERVER = "my-server"; + public static final String TOPIC2 = "my-topic-2"; + public static final String TOPIC3 = "my-topic-3"; + + /** + * Initializes a new factory. + */ + protected abstract void initFactory(); + + /** + * Makes a property builder. + * + * @return a new property builder + */ + protected abstract TopicPropertyBuilder makePropBuilder(); + + /** + * Builds a set of topics. + * + * @param properties the properties used to configure the topics + * @return a list of new topics + */ + protected abstract List<T> buildTopics(Properties properties); + + /** + * Destroys the factory. + */ + protected abstract void destroyFactory(); + + /** + * Destroys a topic within the factory. + * + * @param topic the topic to destroy + */ + protected abstract void destroyTopic(String topic); + + /** + * Gets the list of topics from the factory. + * + * @return the topic inventory + */ + protected abstract List<T> getInventory(); + + /** + * Gets a topic from the factory. + * + * @param topic the topic name + * @return the topic + */ + protected abstract T getTopic(String topic); + + + /** + * Tests building a topic using varied Properties. + */ + void testBuildProperties_Variations() { + initFactory(); + + // null topic list + assertTrue(buildTopics(makePropBuilder().build()).isEmpty()); + + // empty topic list + assertTrue(buildTopics(makePropBuilder().addTopic("").build()).isEmpty()); + + // null servers + assertTrue(buildTopics(makePropBuilder().makeTopic(MY_TOPIC).removeTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX) + .build()).isEmpty()); + + // empty servers + assertTrue(buildTopics(makePropBuilder().makeTopic(MY_TOPIC).setTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX, "") + .build()).isEmpty()); + } + + /** + * Tests building multiple topics using Properties. + */ + public void testBuildProperties_Multiple() { + initFactory(); + + // make two fully-defined topics, and add two duplicate topic names to the list + TopicPropertyBuilder builder = + makePropBuilder().makeTopic(MY_TOPIC).makeTopic(TOPIC2).addTopic(MY_TOPIC).addTopic(MY_TOPIC); + + List<T> lst = buildTopics(builder.build()); + assertEquals(4, lst.size()); + + int index = 0; + T item = lst.get(index++); + assertNotSame(item, lst.get(index++)); + assertSame(item, lst.get(index++)); + assertSame(item, lst.get(index++)); + } + + /** + * Tests destroy(topic), get(topic), and inventory() methods. + */ + public void testDestroyString_testGet_testInventory() { + initFactory(); + + List<T> lst = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).makeTopic(TOPIC2).build()); + + int index = 0; + T item1 = lst.get(index++); + T item2 = lst.get(index++); + + assertEquals(2, getInventory().size()); + assertTrue(getInventory().contains(item1)); + assertTrue(getInventory().contains(item2)); + + item1.start(); + item2.start(); + + assertEquals(item1, getTopic(MY_TOPIC)); + assertEquals(item2, getTopic(TOPIC2)); + + destroyTopic(MY_TOPIC); + assertFalse(item1.isAlive()); + assertTrue(item2.isAlive()); + assertEquals(item2, getTopic(TOPIC2)); + assertEquals(1, getInventory().size()); + assertTrue(getInventory().contains(item2)); + + // repeat + destroyTopic(MY_TOPIC); + assertFalse(item1.isAlive()); + assertTrue(item2.isAlive()); + + // with other topic + destroyTopic(TOPIC2); + assertFalse(item1.isAlive()); + assertFalse(item2.isAlive()); + assertEquals(0, getInventory().size()); + } + + /** + * Tests exception cases with destroy(topic). + */ + public void testDestroyString_Ex() { + // null topic + assertThatIllegalArgumentException().as("null topic").isThrownBy(() -> destroyTopic(null)); + + // empty topic + assertThatIllegalArgumentException().as("empty topic").isThrownBy(() -> destroyTopic("")); + } + + /** + * Tests the destroy() method. + */ + public void testDestroy() { + initFactory(); + + List<T> lst = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).makeTopic(TOPIC2).build()); + + int index = 0; + T item1 = lst.get(index++); + T item2 = lst.get(index++); + + item1.start(); + item2.start(); + + destroyFactory(); + + assertFalse(item1.isAlive()); + assertFalse(item2.isAlive()); + assertEquals(0, getInventory().size()); + } + + /** + * Tests exception cases with get(topic). + */ + public void testGet_Ex() { + // null topic + assertThatIllegalArgumentException().as("null topic").isThrownBy(() -> getTopic(null)); + + // empty topic + assertThatIllegalArgumentException().as("empty topic").isThrownBy(() -> getTopic("")); + + // unknown topic + initFactory(); + buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()); + + assertThatIllegalStateException().as("unknown topic").isThrownBy(() -> getTopic(TOPIC2)); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicPropertyBuilder.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicPropertyBuilder.java new file mode 100644 index 00000000..29c5306b --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicPropertyBuilder.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * ONAP Policy Engine - Common Modules + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.Properties; + +/** + * Builder of properties used when configuring topics. + */ +public abstract class TopicPropertyBuilder { + private final Properties properties = new Properties(); + private final String prefix; + private String topicPrefix; + + /** + * Constructs the object. + * + * @param prefix the prefix for the properties to be built + */ + public TopicPropertyBuilder(String prefix) { + this.prefix = prefix; + } + + /** + * Constructs the properties from the builder. + * + * @return a copy of the properties + */ + public Properties build() { + Properties props = new Properties(); + props.putAll(properties); + + return props; + } + + /** + * Adds a topic to the list of topics, configuring all of its properties with default + * values. + * + * @param topic the topic to be added + * @return this builder + */ + public abstract TopicPropertyBuilder makeTopic(String topic); + + /** + * Adds a topic to the list of topics. Also sets the current topic so that subsequent + * invocations of property methods will manipulate the topic's properties. + * + * @param topic the topic to be added + * @return this builder + */ + public TopicPropertyBuilder addTopic(String topic) { + // add topic to the list of topics + String topicList = properties.getProperty(prefix); + if (topicList == null || topicList.isEmpty()) { + topicList = topic; + } else { + topicList += "," + topic; + } + + properties.setProperty(prefix, topicList); + + setTopic(topic); + + return this; + } + + /** + * Sets the topic for which subsequent properties will be managed. + * + * @param topic the topic + * @return this builder + */ + public TopicPropertyBuilder setTopic(String topic) { + this.topicPrefix = prefix + "." + topic; + return this; + } + + /** + * Sets a topic's property. + * + * @param name name of the property + * @param value value to which the property should be set + * @return this builder + */ + public TopicPropertyBuilder setTopicProperty(String name, Object value) { + properties.setProperty(topicPrefix + name, value.toString()); + return this; + } + + /** + * Removes a topic's property. + * + * @param name name of the property + * @return this builder + */ + public TopicPropertyBuilder removeTopicProperty(String name) { + properties.remove(topicPrefix + name); + return this; + } +} + diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicTestBase.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicTestBase.java new file mode 100644 index 00000000..8d5c3535 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/base/TopicTestBase.java @@ -0,0 +1,159 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.base; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.onap.policy.common.parameters.topic.BusTopicParams; +import org.onap.policy.common.parameters.topic.BusTopicParams.TopicParamsBuilder; + +/** + * Base class for Topic Test classes. + */ +public class TopicTestBase { + + public static final String MY_AFT_ENV = "my-aft-env"; + public static final String MY_API_KEY = "my-api-key"; + public static final String MY_API_SECRET = "my-api-secret"; + public static final String MY_BASE_PATH = "my-base"; + public static final String MY_CLIENT_NAME = "my-client"; + public static final String MY_CONS_GROUP = "my-cons-group"; + public static final String MY_CONS_INST = "my-cons-inst"; + public static final String MY_ENV = "my-env"; + public static final int MY_FETCH_LIMIT = 100; + public static final int MY_FETCH_TIMEOUT = 101; + public static final String MY_HOST = "my-host"; + public static final String MY_LAT = "my-lat"; + public static final String MY_LONG = "my-long"; + public static final String MY_PARTNER = "my-partner"; + public static final String MY_PASS = "my-pass"; + public static final int MY_PORT = 102; + public static final String MY_TOPIC = "my-topic"; + public static final String MY_EFFECTIVE_TOPIC = "my-effective-topic"; + public static final String MY_USERNAME = "my-user"; + + public static final String MY_MESSAGE = "my-message"; + public static final String MY_PARTITION = "my-partition"; + public static final String MY_MESSAGE2 = "my-message-2"; + + public static final String MY_SERIALIZER = "org.apache.kafka.common.serialization.StringSerializer"; + public static final int KAFKA_PORT = 9092; + + /** + * Message used within exceptions that are expected. + */ + public static final String EXPECTED = "expected exception"; + + /** + * Additional properties to be added to the parameter builder. + */ + protected Map<String, String> addProps; + + /** + * Servers to be added to the parameter builder. + */ + protected List<String> servers; + + /** + * Servers to be added to the parameter builder. + */ + protected List<String> kafkaServers; + + /** + * Parameter builder used to build topic parameters. + */ + protected TopicParamsBuilder builder; + + /** + * Initializes {@link #addProps}, {@link #servers}, and {@link #builder}. + */ + public void setUp() { + addProps = new TreeMap<>(); + addProps.put("my-key-A", "my-value-A"); + addProps.put("my-key-B", "my-value-B"); + + servers = Arrays.asList("svra", "svrb"); + kafkaServers = Arrays.asList("localhost:9092", "10.1.2.3:9092"); + + builder = makeBuilder(); + } + + /** + * Makes a fully populated parameter builder. + * + * @return a new parameter builder + */ + public TopicParamsBuilder makeBuilder() { + return makeBuilder(addProps, servers); + } + + /** + * Makes a fully populated parameter builder. + * + * @param addProps additional properties to be added to the builder + * @param servers servers to be added to the builder + * @return a new parameter builder + */ + public TopicParamsBuilder makeBuilder(Map<String, String> addProps, List<String> servers) { + + return BusTopicParams.builder().additionalProps(addProps).aftEnvironment(MY_AFT_ENV).allowSelfSignedCerts(true) + .apiKey(MY_API_KEY).apiSecret(MY_API_SECRET).basePath(MY_BASE_PATH).clientName(MY_CLIENT_NAME) + .consumerGroup(MY_CONS_GROUP).consumerInstance(MY_CONS_INST).environment(MY_ENV) + .fetchLimit(MY_FETCH_LIMIT).fetchTimeout(MY_FETCH_TIMEOUT).hostname(MY_HOST).latitude(MY_LAT) + .longitude(MY_LONG).managed(true).partitionId(MY_PARTITION).partner(MY_PARTNER) + .password(MY_PASS).port(MY_PORT).servers(servers).topic(MY_TOPIC) + .effectiveTopic(MY_EFFECTIVE_TOPIC).useHttps(true).allowTracing(true).userName(MY_USERNAME) + .serializationProvider(MY_SERIALIZER); + } + + /** + * Makes a fully populated parameter builder. + * + * @return a new parameter builder + */ + public TopicParamsBuilder makeKafkaBuilder() { + addProps.clear(); + String jaas = "org.apache.kafka.common.security.plain.PlainLoginModule " + + "required username=abc password=abc serviceName=kafka;"; + addProps.put("sasl.jaas.config", jaas); + addProps.put("sasl.mechanism", "SCRAM-SHA-512"); + addProps.put("security.protocol", "SASL_PLAINTEXT"); + + return makeKafkaBuilder(addProps, kafkaServers); + } + + /** + * Makes a fully populated parameter builder. + * + * @param addProps additional properties to be added to the builder + * @param servers servers to be added to the builder + * @return a new parameter builder + */ + public TopicParamsBuilder makeKafkaBuilder(Map<String, String> addProps, List<String> servers) { + + return BusTopicParams.builder().additionalProps(addProps).basePath(MY_BASE_PATH).clientName(MY_CLIENT_NAME) + .consumerGroup(MY_CONS_GROUP).consumerInstance(MY_CONS_INST).environment(MY_ENV) + .hostname(MY_HOST).partitionId(MY_PARTITION).partner(MY_PARTNER).fetchTimeout(MY_FETCH_TIMEOUT) + .port(KAFKA_PORT).servers(servers).topic(MY_TOPIC) + .effectiveTopic(MY_EFFECTIVE_TOPIC).useHttps(false).allowTracing(true); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/client/BidirectionalTopicClientTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/client/BidirectionalTopicClientTest.java new file mode 100644 index 00000000..ba33ff9a --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/client/BidirectionalTopicClientTest.java @@ -0,0 +1,454 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Properties; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicEndpoint; +import org.onap.policy.common.message.bus.event.TopicEndpointManager; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.message.bus.event.TopicSink; +import org.onap.policy.common.message.bus.event.TopicSource; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; + +@ExtendWith(MockitoExtension.class) +class BidirectionalTopicClientTest { + private static final Coder coder = new StandardCoder(); + private static final long MAX_WAIT_MS = 5000; + private static final long SHORT_WAIT_MS = 1; + private static final String SINK_TOPIC = "my-sink-topic"; + private static final String SOURCE_TOPIC = "my-source-topic"; + private static final String MY_TEXT = "my-text"; + + private static final CommInfrastructure SINK_INFRA = CommInfrastructure.NOOP; + private static final CommInfrastructure SOURCE_INFRA = CommInfrastructure.NOOP; + + @Mock + private TopicSink sink; + @Mock + private TopicSource source; + @Mock + private TopicEndpoint endpoint; + @Mock + private TopicListener listener; + + private MyMessage theMessage; + + private BidirectionalTopicClient client; + private Context context; + + /** + * Configures the endpoints. + */ + @BeforeAll + public static void setUpBeforeClass() { + Properties props = new Properties(); + props.setProperty("noop.sink.topics", SINK_TOPIC); + props.setProperty("noop.source.topics", SOURCE_TOPIC); + + // clear all topics and then configure one sink and one source + TopicEndpointManager.getManager().shutdown(); + TopicEndpointManager.getManager().addTopicSinks(props); + TopicEndpointManager.getManager().addTopicSources(props); + } + + @AfterAll + public static void tearDownAfterClass() { + // clear all topics after the tests + TopicEndpointManager.getManager().shutdown(); + } + + /** + * Creates mocks and an initial client object. + */ + @BeforeEach + public void setUp() throws Exception { + lenient().when(sink.send(anyString())).thenReturn(true); + lenient().when(sink.getTopicCommInfrastructure()).thenReturn(SINK_INFRA); + + lenient().when(source.offer(anyString())).thenReturn(true); + lenient().when(source.getTopicCommInfrastructure()).thenReturn(SOURCE_INFRA); + + lenient().when(endpoint.getTopicSinks(anyString())).thenReturn(Arrays.asList()); + lenient().when(endpoint.getTopicSinks(SINK_TOPIC)).thenReturn(Arrays.asList(sink)); + + lenient().when(endpoint.getTopicSources(any())).thenReturn(Arrays.asList()); + lenient().when(endpoint.getTopicSources(Arrays.asList(SOURCE_TOPIC))).thenReturn(Arrays.asList(source)); + + theMessage = new MyMessage(MY_TEXT); + + client = new BidirectionalTopicClient2(SINK_TOPIC, SOURCE_TOPIC); + + context = new Context(); + } + + @AfterEach + public void tearDown() { + context.stop(); + } + + @Test + void testBidirectionalTopicClient_testGetters() { + assertSame(sink, client.getSink()); + assertSame(source, client.getSource()); + assertEquals(SINK_TOPIC, client.getSinkTopic()); + assertEquals(SOURCE_TOPIC, client.getSourceTopic()); + assertEquals(SINK_INFRA, client.getSinkTopicCommInfrastructure()); + assertEquals(SOURCE_INFRA, client.getSourceTopicCommInfrastructure()); + } + + /** + * Tests the constructor when the sink or source cannot be found. + */ + @Test + void testBidirectionalTopicClientExceptions() { + assertThatThrownBy(() -> new BidirectionalTopicClient2("unknown-sink", SOURCE_TOPIC)) + .isInstanceOf(BidirectionalTopicClientException.class) + .hasMessage("no sinks for topic: unknown-sink"); + + assertThatThrownBy(() -> new BidirectionalTopicClient2(SINK_TOPIC, "unknown-source")) + .isInstanceOf(BidirectionalTopicClientException.class) + .hasMessage("no sources for topic: unknown-source"); + + // too many sources + when(endpoint.getTopicSources(Arrays.asList(SOURCE_TOPIC))).thenReturn(Arrays.asList(source, source)); + + assertThatThrownBy(() -> new BidirectionalTopicClient2(SINK_TOPIC, SOURCE_TOPIC)) + .isInstanceOf(BidirectionalTopicClientException.class) + .hasMessage("too many sources for topic: my-source-topic"); + } + + /** + * Tests the "delegate" methods. + */ + @Test + void testDelegates() { + assertTrue(client.send("hello")); + verify(sink).send("hello"); + + assertTrue(client.offer("incoming")); + verify(source).offer("incoming"); + + client.register(listener); + verify(source).register(listener); + + client.unregister(listener); + verify(source).unregister(listener); + } + + @Test + void testGetTopicEndpointManager() throws BidirectionalTopicClientException { + // use a real manager + client = new BidirectionalTopicClient(SINK_TOPIC, SOURCE_TOPIC); + assertNotNull(client.getTopicEndpointManager()); + + assertNotNull(client.getSink()); + assertNotNull(client.getSource()); + + assertNotSame(sink, client.getSink()); + assertNotSame(source, client.getSource()); + } + + @Test + void testAwaitReceipt() throws Exception { + context.start(theMessage); + assertThat(context.awaitSend(1)).isTrue(); + + verify(source).register(any()); + verify(sink, atLeast(1)).send(any()); + assertThat(context.checker.isReady()).isFalse(); + + inject(theMessage); + + verifyReceipt(); + } + + @Test + void testAwaitReceipt_AlreadyDone() throws Exception { + context.start(theMessage); + assertThat(context.awaitSend(1)).isTrue(); + + inject(theMessage); + + verifyReceipt(); + + // calling again should result in "true" again, without injecting message + context.start(theMessage); + verifyReceipt(); + } + + @Test + void testAwaitReceipt_MessageDoesNotMatch() throws Exception { + context.start(theMessage); + assertThat(context.awaitSend(1)).isTrue(); + + // non-matching message + inject("{}"); + + // wait for a few more calls to "send" and then inject a matching message + assertThat(context.awaitSend(3)).isTrue(); + inject(theMessage); + + verifyReceipt(); + } + + @Test + void testAwaitReceipt_DecodeFails() throws Exception { + context.start(theMessage); + assertThat(context.awaitSend(1)).isTrue(); + + // force a failure and inject the message + context.forceDecodeFailure = true; + inject(theMessage); + + assertThat(context.awaitDecodeFailure()).isTrue(); + + // no more failures + context.forceDecodeFailure = false; + inject(theMessage); + + verifyReceipt(); + } + + @Test + void testAwaitReceipt_Interrupted() throws InterruptedException { + context.start(theMessage); + assertThat(context.awaitSend(1)).isTrue(); + + context.interrupt(); + + verifyNoReceipt(); + } + + @Test + void testAwaitReceipt_MultipleLoops() throws Exception { + context.start(theMessage); + + // wait for multiple "send" calls + assertThat(context.awaitSend(3)).isTrue(); + + inject(theMessage); + + verifyReceipt(); + } + + @Test + void testStop() throws InterruptedException { + context.start(theMessage); + assertThat(context.awaitSend(1)).isTrue(); + + context.stop(); + + verifyNoReceipt(); + } + + /** + * Verifies that awaitReceipt() returns {@code true}. + * + * @throws InterruptedException if interrupted while waiting for the thread to + * terminate + */ + private void verifyReceipt() throws InterruptedException { + assertThat(context.join()).isTrue(); + assertThat(context.result).isTrue(); + assertThat(context.exception).isNull(); + assertThat(context.checker.isReady()).isTrue(); + + verify(source).unregister(any()); + } + + /** + * Verifies that awaitReceipt() returns {@code false}. + * + * @throws InterruptedException if interrupted while waiting for the thread to + * terminate + */ + private void verifyNoReceipt() throws InterruptedException { + assertThat(context.join()).isTrue(); + assertThat(context.result).isFalse(); + assertThat(context.exception).isNull(); + assertThat(context.checker.isReady()).isFalse(); + + verify(source).unregister(any()); + } + + /** + * Injects a message into the source topic. + * + * @param message message to be injected + * @throws CoderException if the message cannot be encoded + */ + private void inject(MyMessage message) throws CoderException { + inject(coder.encode(message)); + } + + /** + * Injects a message into the source topic. + * + * @param message message to be injected + */ + private void inject(String message) { + ArgumentCaptor<TopicListener> cap = ArgumentCaptor.forClass(TopicListener.class); + verify(source).register(cap.capture()); + + cap.getValue().onTopicEvent(SOURCE_INFRA, SOURCE_TOPIC, message); + } + + + /** + * BidirectionalTopicClient with some overrides. + */ + private class BidirectionalTopicClient2 extends BidirectionalTopicClient { + + public BidirectionalTopicClient2(String sinkTopic, String sourceTopic) + throws BidirectionalTopicClientException { + super(sinkTopic, sourceTopic); + } + + @Override + protected TopicEndpoint getTopicEndpointManager() { + return endpoint; + } + } + + private class Context { + private Thread thread; + private boolean result; + private Exception exception; + private boolean forceDecodeFailure; + + // released every time the checker publishes a message + private final Semaphore sendSem = new Semaphore(0); + + // released every time a message-decode fails + private final Semaphore decodeFailedSem = new Semaphore(0); + + private final BidirectionalTopicClient2 checker; + + public Context() throws BidirectionalTopicClientException { + + checker = new BidirectionalTopicClient2(SINK_TOPIC, SOURCE_TOPIC) { + + @Override + public boolean send(String messageText) { + boolean result = super.send(messageText); + sendSem.release(); + return result; + } + + @Override + protected <T> T decode(String msg, Class<? extends T> clazz) throws CoderException { + if (forceDecodeFailure) { + throw new CoderException("expected exception"); + } + + return super.decode(msg, clazz); + } + + @Override + protected void decodeFailed() { + super.decodeFailed(); + decodeFailedSem.release(); + } + }; + } + + /** + * Starts the thread. + * + * @param message message to be sent to the sink topic + */ + public void start(MyMessage message) { + thread = new Thread() { + @Override + public void run() { + try { + result = checker.awaitReady(message, SHORT_WAIT_MS); + } catch (Exception e) { + exception = e; + } + } + }; + thread.setDaemon(true); + thread.start(); + } + + public void stop() { + checker.stopWaiting(); + } + + public boolean join() throws InterruptedException { + thread.join(MAX_WAIT_MS); + return !thread.isAlive(); + } + + public void interrupt() { + thread.interrupt(); + } + + public boolean awaitSend(int npermits) throws InterruptedException { + return sendSem.tryAcquire(npermits, MAX_WAIT_MS, TimeUnit.MILLISECONDS); + } + + public boolean awaitDecodeFailure() throws InterruptedException { + return decodeFailedSem.tryAcquire(MAX_WAIT_MS, TimeUnit.MILLISECONDS); + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class MyMessage { + private String text; + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/client/TopicClientExceptionTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/client/TopicClientExceptionTest.java new file mode 100644 index 00000000..1ea2f1bc --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/client/TopicClientExceptionTest.java @@ -0,0 +1,36 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.onap.policy.common.utils.test.ExceptionsTester; + +class TopicClientExceptionTest { + + @Test + void test() { + assertEquals(5, new ExceptionsTester().test(TopicSinkClientException.class)); + assertEquals(5, new ExceptionsTester().test(BidirectionalTopicClientException.class)); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientTest.java new file mode 100644 index 00000000..67b15ec9 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientTest.java @@ -0,0 +1,147 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP PAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2019, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.client; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.TopicEndpointManager; +import org.onap.policy.common.message.bus.event.TopicSink; + +class TopicSinkClientTest { + private static final String TOPIC = "my-topic"; + + private TopicSinkClient client; + private TopicSink sink; + private List<TopicSink> sinks; + + /** + * Creates mocks and an initial client object. + * + * @throws Exception if an error occurs + */ + @BeforeEach + public void setUp() throws Exception { + sink = mock(TopicSink.class); + when(sink.send(anyString())).thenReturn(true); + + sinks = Arrays.asList(sink, null); + + client = new TopicSinkClient2(TOPIC); + + Properties props = new Properties(); + props.setProperty("noop.sink.topics", TOPIC); + + // clear all topics and then configure one topic + TopicEndpointManager.getManager().shutdown(); + TopicEndpointManager.getManager().addTopicSinks(props); + } + + @AfterAll + public static void tearDown() { + // clear all topics after the tests + TopicEndpointManager.getManager().shutdown(); + } + + /** + * Uses a real NO-OP topic sink. + */ + @Test + void testGetTopicSinks() throws Exception { + + sink = TopicEndpointManager.getManager().getNoopTopicSink(TOPIC); + assertNotNull(sink); + + final AtomicReference<String> evref = new AtomicReference<>(null); + + sink.register((infra, topic, event) -> evref.set(event)); + sink.start(); + + client = new TopicSinkClient(TOPIC); + client.send(100); + + assertEquals("100", evref.get()); + } + + @Test + void testTopicSinkClient() { + // unknown topic -> should throw exception + sinks = new LinkedList<>(); + assertThatThrownBy(() -> new TopicSinkClient2(TOPIC)).isInstanceOf(TopicSinkClientException.class) + .hasMessage("no sinks for topic: my-topic"); + } + + @Test + void testTopicSinkClient_GetTopic() throws TopicSinkClientException { + assertEquals(TOPIC, new TopicSinkClient(TopicEndpointManager.getManager().getNoopTopicSink(TOPIC)).getTopic()); + assertEquals(TOPIC, new TopicSinkClient(TOPIC).getTopic()); + + assertThatThrownBy(() -> new TopicSinkClient((TopicSink) null)) + .hasMessageContaining("sink is marked non-null but is null"); + assertThatThrownBy(() -> new TopicSinkClient("blah")).isInstanceOf(TopicSinkClientException.class) + .hasMessage("no sinks for topic: blah"); + } + + @Test + void testSend() { + client.send(Arrays.asList("abc", "def")); + verify(sink).send("['abc','def']".replace('\'', '"')); + + // sink send fails + when(sink.send(anyString())).thenReturn(false); + assertFalse(client.send("ghi")); + + // sink send throws an exception + final RuntimeException ex = new RuntimeException("expected exception"); + when(sink.send(anyString())).thenThrow(ex); + assertFalse(client.send("jkl")); + } + + /** + * TopicSinkClient with some overrides. + */ + private class TopicSinkClient2 extends TopicSinkClient { + + public TopicSinkClient2(final String topic) throws TopicSinkClientException { + super(topic); + } + + @Override + protected List<TopicSink> getTopicSinks(final String topic) { + return sinks; + } + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactoryTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactoryTest.java new file mode 100644 index 00000000..a901b07b --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactoryTest.java @@ -0,0 +1,63 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class IndexedKafkaTopicSourceFactoryTest { + + private IndexedKafkaTopicSourceFactory factory; + + @Test + void testBuild() { + factory = new IndexedKafkaTopicSourceFactory(); + BusTopicParams params = new BusTopicParams(); + + // set servers to null + params.setServers(null); + assertThatThrownBy(() -> factory.build(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("KAFKA Server(s) must be provided"); + + // set servers to empty + params.setServers(List.of()); + assertThatThrownBy(() -> factory.build(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("KAFKA Server(s) must be provided"); + + List<String> servers = List.of("kafka:9092", "kafka:29092"); + params.setServers(servers); + + // set topic to null + params.setTopic(null); + assertThatThrownBy(() -> factory.build(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("A topic must be provided"); + + // set topic to empty + params.setTopic(""); + assertThatThrownBy(() -> factory.build(params)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("A topic must be provided"); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSinkTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSinkTest.java new file mode 100644 index 00000000..c8d6e21e --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSinkTest.java @@ -0,0 +1,68 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.base.TopicTestBase; + +class InlineKafkaTopicSinkTest extends TopicTestBase { + private InlineKafkaTopicSink sink; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + sink = new InlineKafkaTopicSink(makeKafkaBuilder().build()); + } + + @AfterEach + public void tearDown() { + sink.shutdown(); + } + + @Test + void testToString() { + assertTrue(sink.toString().startsWith("InlineKafkaTopicSink [")); + } + + @Test + void testInit() { + // nothing null + sink = new InlineKafkaTopicSink(makeKafkaBuilder().build()); + sink.init(); + assertThatCode(() -> sink.shutdown()).doesNotThrowAnyException(); + } + + @Test + void testGetTopicCommInfrastructure() { + assertEquals(CommInfrastructure.KAFKA, sink.getTopicCommInfrastructure()); + } + +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapperTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapperTest.java new file mode 100644 index 00000000..8e13af23 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapperTest.java @@ -0,0 +1,98 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Properties; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class KafkaPublisherWrapperTest { + + private KafkaPublisherWrapper kafkaPublisherWrapper; + private Producer<String, String> mockProducer; + private BusTopicParams mockBusTopicParams; + + @BeforeEach + void setUp() { + mockProducer = mock(KafkaProducer.class); + mockBusTopicParams = mock(BusTopicParams.class); + + when(mockBusTopicParams.getTopic()).thenReturn("testTopic"); + when(mockBusTopicParams.getServers()).thenReturn(Collections.singletonList("localhost:9092")); + when(mockBusTopicParams.isTopicInvalid()).thenReturn(false); + when(mockBusTopicParams.isAdditionalPropsValid()).thenReturn(false); + when(mockBusTopicParams.isAllowTracing()).thenReturn(false); + + kafkaPublisherWrapper = new KafkaPublisherWrapper(mockBusTopicParams) { + private Producer<String, String> createProducer(Properties props) { // NOSONAR instance creation + return mockProducer; + } + }; + } + + @Test + void testConstructor() { + verify(mockBusTopicParams).getTopic(); + verify(mockBusTopicParams).getServers(); + verify(mockBusTopicParams).isTopicInvalid(); + verify(mockBusTopicParams).isAdditionalPropsValid(); + verify(mockBusTopicParams).isAllowTracing(); + } + + @Test + void testSendSuccess() { + when(mockProducer.send(ArgumentMatchers.any(ProducerRecord.class))).thenReturn(null); + assertTrue(kafkaPublisherWrapper.send("partitionId", "testMessage")); + } + + @Test + void testSendNullMessage() { + IllegalArgumentException thrown = assertThrows( + IllegalArgumentException.class, + () -> kafkaPublisherWrapper.send("partitionId", null), + "Expected send() to throw, but it didn't" + ); + assertEquals("No message provided", thrown.getMessage()); + } + + @Test + void testSendFailure() { + when(mockProducer.send(ArgumentMatchers.any(ProducerRecord.class))).thenThrow(RuntimeException.class); + assertTrue(kafkaPublisherWrapper.send("partitionId", "testMessage")); + } + + @Test + void testClose() { + assertThatCode(kafkaPublisherWrapper::close).doesNotThrowAnyException(); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactoryTestBase.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactoryTestBase.java new file mode 100644 index 00000000..1085ee90 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactoryTestBase.java @@ -0,0 +1,47 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import java.util.Collections; +import org.onap.policy.common.message.bus.event.Topic; +import org.onap.policy.common.message.bus.event.base.BusTopicFactoryTestBase; + +/** + * Base class for KafkaTopicXxxFactory tests. + * + * @param <T> type of topic managed by the factory + */ +public abstract class KafkaTopicFactoryTestBase<T extends Topic> extends BusTopicFactoryTestBase<T> { + + @Override + public void testBuildBusTopicParams_Ex() { + + super.testBuildBusTopicParams_Ex(); + + // null servers + assertThatIllegalArgumentException().as("null servers") + .isThrownBy(() -> buildTopic(makeBuilder().servers(null).build())); + + // empty servers + assertThatIllegalArgumentException().as("empty servers") + .isThrownBy(() -> buildTopic(makeBuilder().servers(Collections.emptyList()).build())); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicPropertyBuilder.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicPropertyBuilder.java new file mode 100644 index 00000000..6a1be7a0 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicPropertyBuilder.java @@ -0,0 +1,94 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.onap.policy.common.message.bus.event.base.TopicTestBase.MY_EFFECTIVE_TOPIC; +import static org.onap.policy.common.message.bus.event.base.TopicTestBase.MY_PARTITION; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_HTTP_HTTPS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.parameters.topic.TopicParameters; + +@Getter +public class KafkaTopicPropertyBuilder extends TopicPropertyBuilder { + + public static final String SERVER = "localhost:9092"; + public static final String TOPIC2 = "my-topic-2"; + public static final String ADDITIONAL_PROPS = "{\"security.protocol\": \"SASL_PLAINTEXT\"," + + "\"sasl.mechanism\": \"SCRAM-SHA-512\",\"sasl.jaas.config\": " + + "\"org.apache.kafka.common.security.plain.PlainLoginModule " + + "required username=abc password=abc serviceName=kafka;\"}"; + + private final TopicParameters params = new TopicParameters(); + + /** + * Constructs the object. + * + * @param prefix the prefix for the properties to be built + */ + public KafkaTopicPropertyBuilder(String prefix) { + super(prefix); + } + + /** + * Adds a topic and configures it's properties with default values. + * + * @param topic the topic to be added + * @return this builder + */ + public KafkaTopicPropertyBuilder makeTopic(String topic) { + addTopic(topic); + + setTopicProperty(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX, MY_EFFECTIVE_TOPIC); + setTopicProperty(PROPERTY_MANAGED_SUFFIX, "true"); + setTopicProperty(PROPERTY_HTTP_HTTPS_SUFFIX, "true"); + setTopicProperty(PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX, MY_PARTITION); + setTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX, SERVER); + setTopicProperty(".additionalProps", ADDITIONAL_PROPS); + + params.setTopicCommInfrastructure("kafka"); + params.setTopic(topic); + params.setEffectiveTopic(MY_EFFECTIVE_TOPIC); + params.setManaged(true); + params.setUseHttps(true); + params.setPartitionId(MY_PARTITION); + params.setServers(List.of(SERVER)); + params.setAdditionalProps(getAdditionalProps()); + + return this; + } + + private Map<String, String> getAdditionalProps() { + try { + return new ObjectMapper().readValue(ADDITIONAL_PROPS, Map.class); + } catch (JsonProcessingException e) { + return Collections.emptyMap(); + } + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactoryTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactoryTest.java new file mode 100644 index 00000000..62210361 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactoryTest.java @@ -0,0 +1,200 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_KAFKA_SINK_TOPICS; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class KafkaTopicSinkFactoryTest extends KafkaTopicFactoryTestBase<KafkaTopicSink> { + + private SinkFactory factory; + public static final String KAFKA_SERVER = "localhost:9092"; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + factory = new SinkFactory(); + } + + @AfterEach + public void tearDown() { + factory.destroy(); + } + + @Test + @Override + public void testBuildBusTopicParams() { + super.testBuildBusTopicParams(); + super.testBuildBusTopicParams_Ex(); + } + + @Test + @Override + public void testBuildListOfStringString() { + super.testBuildListOfStringString(); + + // check parameters that were used + BusTopicParams params = getLastParams(); + assertFalse(params.isAllowSelfSignedCerts()); + } + + @Test + @Override + public void testBuildProperties() { + List<KafkaTopicSink> topics = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()); + assertEquals(1, topics.size()); + assertEquals(MY_TOPIC, topics.get(0).getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, topics.get(0).getEffectiveTopic()); + + BusTopicParams params = getLastParams(); + assertTrue(params.isManaged()); + assertFalse(params.isUseHttps()); + assertEquals(List.of(KAFKA_SERVER), params.getServers()); + assertEquals(MY_TOPIC, params.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, params.getEffectiveTopic()); + assertEquals(MY_PARTITION, params.getPartitionId()); + assertNotNull(params.getAdditionalProps()); + + List<KafkaTopicSink> topics2 = buildTopics(makePropBuilder().makeTopic(TOPIC3) + .removeTopicProperty(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX).build()); + assertEquals(1, topics2.size()); + assertEquals(TOPIC3, topics2.get(0).getTopic()); + assertEquals(topics2.get(0).getTopic(), topics2.get(0).getEffectiveTopic()); + + initFactory(); + + assertEquals(1, buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()).size()); + } + + @Test + void testBuildFromProperties() { + Properties props = makePropBuilder().makeTopic(MY_TOPIC).build(); + var listTopic = factory.build(props); + assertNotNull(listTopic); + } + + @Test + @Override + public void testDestroyString_testGet_testInventory() { + super.testDestroyString_testGet_testInventory(); + super.testDestroyString_Ex(); + } + + @Test + @Override + public void testDestroy() { + super.testDestroy(); + } + + @Test + void testGet() { + super.testGet_Ex(); + } + + @Test + void testToString() { + assertTrue(factory.toString().startsWith("IndexedKafkaTopicSinkFactory [")); + } + + @Override + protected void initFactory() { + if (factory != null) { + factory.destroy(); + } + + factory = new SinkFactory(); + } + + @Override + protected List<KafkaTopicSink> buildTopics(Properties properties) { + return factory.build(properties); + } + + @Override + protected KafkaTopicSink buildTopic(BusTopicParams params) { + return factory.build(params); + } + + @Override + protected KafkaTopicSink buildTopic(List<String> servers, String topic) { + return factory.build(servers, topic); + } + + @Override + protected void destroyFactory() { + factory.destroy(); + } + + @Override + protected void destroyTopic(String topic) { + factory.destroy(topic); + } + + @Override + protected List<KafkaTopicSink> getInventory() { + return factory.inventory(); + } + + @Override + protected KafkaTopicSink getTopic(String topic) { + return factory.get(topic); + } + + @Override + protected BusTopicParams getLastParams() { + return factory.params.getLast(); + } + + @Override + protected TopicPropertyBuilder makePropBuilder() { + return new KafkaTopicPropertyBuilder(PROPERTY_KAFKA_SINK_TOPICS); + } + + /** + * Factory that records the parameters of all the sinks it creates. + */ + private static class SinkFactory extends IndexedKafkaTopicSinkFactory { + private Deque<BusTopicParams> params = new LinkedList<>(); + + @Override + protected KafkaTopicSink makeSink(BusTopicParams busTopicParams) { + params.add(busTopicParams); + return super.makeSink(busTopicParams); + } + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkTest.java new file mode 100644 index 00000000..8818a27f --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkTest.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START======================================================= + * ONAP Policy Engine - Common Modules + * ================================================================================ + * Copyright (C) 2022, 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +class KafkaTopicSinkTest { + + @Test + void test() { + assertNotNull(KafkaTopicFactories.getSinkFactory()); + } + +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactoryTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactoryTest.java new file mode 100644 index 00000000..c91e548b --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactoryTest.java @@ -0,0 +1,168 @@ +/* + * ============LICENSE_START======================================================= + * ONAP Policy Engine - Common Modules + * ================================================================================ + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_KAFKA_SOURCE_TOPICS; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +class KafkaTopicSourceFactoryTest extends KafkaTopicFactoryTestBase<KafkaTopicSource> { + + private SourceFactory factory; + + public static final String KAFKA_SERVER = "localhost:9092"; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + factory = new SourceFactory(); + } + + @AfterEach + public void tearDown() { + factory.destroy(); + } + + @Test + @Override + public void testBuildProperties() { + + initFactory(); + + List<KafkaTopicSource> topics = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()); + assertEquals(1, topics.size()); + assertEquals(MY_TOPIC, topics.get(0).getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, topics.get(0).getEffectiveTopic()); + + BusTopicParams params = getLastParams(); + assertTrue(params.isManaged()); + assertFalse(params.isUseHttps()); + assertEquals(List.of(KAFKA_SERVER), params.getServers()); + assertEquals(MY_TOPIC, params.getTopic()); + assertEquals(MY_EFFECTIVE_TOPIC, params.getEffectiveTopic()); + } + + @Test + @Override + public void testDestroyString_testGet_testInventory() { + super.testDestroyString_testGet_testInventory(); + super.testDestroyString_Ex(); + } + + @Test + @Override + public void testDestroy() { + super.testDestroy(); + } + + @Test + void testGet() { + super.testGet_Ex(); + } + + @Test + void testToString() { + assertTrue(factory.toString().startsWith("IndexedKafkaTopicSourceFactory [")); + } + + @Override + protected void initFactory() { + if (factory != null) { + factory.destroy(); + } + + factory = new SourceFactory(); + } + + @Override + protected List<KafkaTopicSource> buildTopics(Properties properties) { + return factory.build(properties); + } + + @Override + protected KafkaTopicSource buildTopic(BusTopicParams params) { + return factory.build(params); + } + + @Override + protected KafkaTopicSource buildTopic(List<String> servers, String topic) { + return factory.build(servers, topic); + } + + @Override + protected void destroyFactory() { + factory.destroy(); + } + + @Override + protected void destroyTopic(String topic) { + factory.destroy(topic); + } + + @Override + protected List<KafkaTopicSource> getInventory() { + return factory.inventory(); + } + + @Override + protected KafkaTopicSource getTopic(String topic) { + return factory.get(topic); + } + + @Override + protected BusTopicParams getLastParams() { + return factory.params.getLast(); + } + + @Override + protected TopicPropertyBuilder makePropBuilder() { + return new KafkaTopicPropertyBuilder(PROPERTY_KAFKA_SOURCE_TOPICS); + } + + /** + * Factory that records the parameters of all the sources it creates. + */ + private static class SourceFactory extends IndexedKafkaTopicSourceFactory { + private final Deque<BusTopicParams> params = new LinkedList<>(); + + @Override + protected KafkaTopicSource makeSource(BusTopicParams busTopicParams) { + params.add(busTopicParams); + return super.makeSource(busTopicParams); + } + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceTest.java new file mode 100644 index 00000000..5572bc01 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceTest.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START======================================================= + * ONAP Policy Engine - Common Modules + * ================================================================================ + * Copyright (C) 2022-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +class KafkaTopicSourceTest { + + @Test + void verifyKafkaTopicFactoriesNotNull() { + assertNotNull(KafkaTopicFactories.getSourceFactory()); + } + +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSourceTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSourceTest.java new file mode 100644 index 00000000..0c732ba7 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSourceTest.java @@ -0,0 +1,61 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.kafka; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.base.TopicTestBase; + +class SingleThreadedKafkaTopicSourceTest extends TopicTestBase { + private SingleThreadedKafkaTopicSource source; + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + + source = new SingleThreadedKafkaTopicSource(makeKafkaBuilder().build()); + } + + @AfterEach + public void tearDown() { + source.shutdown(); + } + + @Test + void testToString() { + assertTrue(source.toString().startsWith("SingleThreadedKafkaTopicSource [")); + source.shutdown(); + } + + @Test + void testGetTopicCommInfrastructure() { + assertEquals(CommInfrastructure.KAFKA, source.getTopicCommInfrastructure()); + } + +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpointTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpointTest.java new file mode 100644 index 00000000..3ebec83c --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpointTest.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.TopicListener; +import org.onap.policy.common.message.bus.event.base.TopicTestBase; + +public abstract class NoopTopicEndpointTest<F extends NoopTopicFactory<T>, T extends NoopTopicEndpoint> + extends TopicTestBase { + + protected final F factory; + protected T endpoint; + + public NoopTopicEndpointTest(F factory) { + this.factory = factory; + } + + protected abstract boolean io(String message); + + @BeforeEach + @Override + public void setUp() { + super.setUp(); + this.endpoint = this.factory.build(servers, MY_TOPIC); + } + + @Test + void testIo() { + TopicListener listener = mock(TopicListener.class); + this.endpoint.register(listener); + this.endpoint.start(); + + assertTrue(io(MY_MESSAGE)); + assertSame(MY_MESSAGE, this.endpoint.getRecentEvents()[0]); + assertEquals(Collections.singletonList(MY_MESSAGE), Arrays.asList(this.endpoint.getRecentEvents())); + verify(listener).onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MY_MESSAGE); + + this.endpoint.unregister(listener); + } + + @Test + void testIoNullMessage() { + assertThatThrownBy(() -> io(null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testIoEmptyMessage() { + assertThatThrownBy(() -> io("")).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testOfferNotStarted() { + assertThatThrownBy(() -> io(MY_MESSAGE)).isInstanceOf(IllegalStateException.class); + } + + @Test + void testGetTopicCommInfrastructure() { + assertEquals(CommInfrastructure.NOOP, this.endpoint.getTopicCommInfrastructure()); + } + + @Test + void testStart_testStop_testShutdown() { + this.endpoint.start(); + assertTrue(this.endpoint.isAlive()); + + // start again + this.endpoint.start(); + assertTrue(this.endpoint.isAlive()); + + // stop + this.endpoint.stop(); + assertFalse(this.endpoint.isAlive()); + + // re-start again + this.endpoint.start(); + assertTrue(this.endpoint.isAlive()); + + // shutdown + this.endpoint.shutdown(); + assertFalse(this.endpoint.isAlive()); + } + + @Test + void testStart_Locked() { + this.endpoint.lock(); + assertThatThrownBy(() -> this.endpoint.start()).isInstanceOf(IllegalStateException.class); + } + +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactoryTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactoryTest.java new file mode 100644 index 00000000..4dcba86e --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactoryTest.java @@ -0,0 +1,257 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.event.base.TopicFactoryTestBase; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.message.bus.event.base.TopicTestBase; +import org.onap.policy.common.parameters.topic.BusTopicParams; + +public abstract class NoopTopicFactoryTest<F extends NoopTopicFactory<T>, T extends NoopTopicEndpoint> + extends TopicFactoryTestBase<T> { + + private static final List<String> NOOP_SERVERS = List.of(CommInfrastructure.NOOP.toString()); + private F factory = null; + + protected abstract F buildFactory(); + + /** + * Creates the object to be tested. + */ + @BeforeEach + @Override + public void setUp() { + super.setUp(); + initFactory(); + } + + @AfterEach + void tearDown() { + factory.destroy(); + } + + @Test + void testBuildBusTopicParams() { + initFactory(); + + T item1 = buildTopic(makeParams(servers)); + assertNotNull(item1); + + assertEquals(servers, item1.getServers()); + assertEquals(MY_TOPIC, item1.getTopic()); + } + + @Test + void testBuildListOfStringStringBoolean() { + initFactory(); + + T item1 = buildTopic(servers, MY_TOPIC, true); + assertNotNull(item1); + + assertEquals(servers, item1.getServers()); + assertEquals(MY_TOPIC, item1.getTopic()); + + // managed topic - should not build a new one + assertEquals(item1, buildTopic(servers, MY_TOPIC, true)); + + T item2 = buildTopic(servers, TOPIC2, true); + assertNotNull(item2); + assertNotSame(item1, item2); + + // duplicate - should be the same, as these topics are managed + List<String> randomServers = new ArrayList<>(); + randomServers.add(RandomStringUtils.randomAlphanumeric(8)); + T item3 = buildTopic(randomServers, TOPIC2, true); + assertSame(item2, item3); + + T item4 = buildTopic(Collections.emptyList(), TOPIC2, true); + assertSame(item3, item4); + + // null server list + initFactory(); + assertEquals(NOOP_SERVERS, buildTopic(null, MY_TOPIC, true).getServers()); + + // empty server list + initFactory(); + assertEquals(NOOP_SERVERS, buildTopic(Collections.emptyList(), MY_TOPIC, true).getServers()); + + // unmanaged topic + initFactory(); + item1 = buildTopic(servers, MY_TOPIC, false); + assertNotSame(item1, buildTopic(servers, MY_TOPIC, false)); + } + + @Test + void testBuildListOfStringStringBoolean_NullTopic() { + assertThatThrownBy(() -> buildTopic(servers, null, true)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testBuildListOfStringStringBoolean_EmptyTopic() { + assertThatThrownBy(() -> buildTopic(servers, "", true)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testBuildProperties() { + // managed topic + initFactory(); + assertEquals(1, buildTopics(makePropBuilder().makeTopic(MY_TOPIC).build()).size()); + assertNotNull(factory.get(MY_TOPIC)); + + // unmanaged topic - get() will throw an exception + initFactory(); + assertEquals(1, buildTopics(makePropBuilder().makeTopic(MY_TOPIC) + .setTopicProperty(PROPERTY_MANAGED_SUFFIX, "false").build()).size()); + assertThatIllegalStateException().isThrownBy(() -> factory.get(MY_TOPIC)); + + // managed undefined - default to true + initFactory(); + assertEquals(1, buildTopics( + makePropBuilder().makeTopic(MY_TOPIC).removeTopicProperty(PROPERTY_MANAGED_SUFFIX).build()) + .size()); + assertNotNull(factory.get(MY_TOPIC)); + + // managed empty - default to true + initFactory(); + assertEquals(1, buildTopics( + makePropBuilder().makeTopic(MY_TOPIC).setTopicProperty(PROPERTY_MANAGED_SUFFIX, "").build()) + .size()); + assertNotNull(factory.get(MY_TOPIC)); + + initFactory(); + + // null topic list + assertTrue(buildTopics(makePropBuilder().build()).isEmpty()); + + // empty topic list + assertTrue(buildTopics(makePropBuilder().addTopic("").build()).isEmpty()); + + // null server list + initFactory(); + T endpoint = buildTopics(makePropBuilder().makeTopic(MY_TOPIC) + .removeTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX).build()).get(0); + assertEquals(NOOP_SERVERS, endpoint.getServers()); + + // empty server list + initFactory(); + endpoint = buildTopics(makePropBuilder().makeTopic(MY_TOPIC).setTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX, "") + .build()).get(0); + assertEquals(NOOP_SERVERS, endpoint.getServers()); + + // test other options + super.testBuildProperties_Multiple(); + } + + @Test + @Override + public void testDestroyString_testGet_testInventory() { + super.testDestroyString_testGet_testInventory(); + super.testDestroyString_Ex(); + } + + @Test + @Override + public void testDestroy() { + super.testDestroy(); + } + + @Test + void testGet() { + super.testGet_Ex(); + } + + @Override + protected void initFactory() { + if (factory != null) { + factory.destroy(); + } + + factory = buildFactory(); + } + + @Override + protected List<T> buildTopics(Properties properties) { + return factory.build(properties); + } + + protected T buildTopic(BusTopicParams param) { + return factory.build(param); + } + + protected T buildTopic(List<String> servers, String topic, boolean managed) { + return factory.build(servers, topic, managed); + } + + @Override + protected void destroyFactory() { + factory.destroy(); + } + + @Override + protected void destroyTopic(String topic) { + factory.destroy(topic); + } + + @Override + protected List<T> getInventory() { + return factory.inventory(); + } + + @Override + protected T getTopic(String topic) { + return factory.get(topic); + } + + @Override + protected TopicPropertyBuilder makePropBuilder() { + return new NoopTopicPropertyBuilder(factory.getTopicsPropertyName()); + } + + private BusTopicParams makeParams(List<String> servers) { + BusTopicParams params = new BusTopicParams(); + + params.setServers(servers); + params.setTopic(TopicTestBase.MY_TOPIC); + params.setManaged(true); + + return params; + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicPropertyBuilder.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicPropertyBuilder.java new file mode 100644 index 00000000..57b78435 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicPropertyBuilder.java @@ -0,0 +1,77 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.onap.policy.common.message.bus.event.base.TopicTestBase.MY_EFFECTIVE_TOPIC; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_HTTP_HTTPS_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_MANAGED_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX; +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_TOPIC_SERVERS_SUFFIX; + +import java.util.List; +import lombok.Getter; +import org.onap.policy.common.message.bus.event.base.TopicPropertyBuilder; +import org.onap.policy.common.parameters.topic.TopicParameters; + +@Getter +public class NoopTopicPropertyBuilder extends TopicPropertyBuilder { + + public static final String SERVER = "my-server"; + + private final TopicParameters params = new TopicParameters(); + + /** + * Constructs the object. + * + * @param prefix the prefix for the properties to be built + */ + public NoopTopicPropertyBuilder(String prefix) { + super(prefix); + } + + /** + * Adds a topic and configures it's properties with default values. + * + * @param topic the topic to be added + * @return this builder + */ + public NoopTopicPropertyBuilder makeTopic(String topic) { + addTopic(topic); + + setTopicProperty(PROPERTY_TOPIC_EFFECTIVE_TOPIC_SUFFIX, MY_EFFECTIVE_TOPIC); + setTopicProperty(PROPERTY_MANAGED_SUFFIX, "true"); + setTopicProperty(PROPERTY_HTTP_HTTPS_SUFFIX, "true"); + setTopicProperty(PROPERTY_ALLOW_SELF_SIGNED_CERTIFICATES_SUFFIX, "true"); + setTopicProperty(PROPERTY_TOPIC_SERVERS_SUFFIX, SERVER); + + params.setTopicCommInfrastructure("noop"); + params.setTopic(topic); + params.setEffectiveTopic(MY_EFFECTIVE_TOPIC); + params.setManaged(true); + params.setUseHttps(true); + params.setAllowSelfSignedCerts(true); + params.setServers(List.of(SERVER)); + + return this; + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactoryTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactoryTest.java new file mode 100644 index 00000000..ecd4df1e --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactoryTest.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class NoopTopicSinkFactoryTest extends NoopTopicFactoryTest<NoopTopicSinkFactory, NoopTopicSink> { + + @Override + protected NoopTopicSinkFactory buildFactory() { + return new NoopTopicSinkFactory(); + } + + @Test + void testToString() { + assertTrue(new NoopTopicSinkFactory().toString().startsWith("NoopTopicSinkFactory [")); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkTest.java new file mode 100644 index 00000000..ecba379f --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkTest.java @@ -0,0 +1,58 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; + +class NoopTopicSinkTest extends NoopTopicEndpointTest<NoopTopicSinkFactory, NoopTopicSink> { + + public NoopTopicSinkTest() { + super(new NoopTopicSinkFactory()); + } + + @Override + protected boolean io(String message) { + return endpoint.send(message); + } + + @Test + void testToString() { + assertThat(endpoint.toString()).startsWith("NoopTopicSink"); + } + + @Test + void testSend() { + NoopTopicSink sink = new NoopTopicSink(servers, MY_TOPIC) { + @Override + protected boolean broadcast(String message) { + throw new RuntimeException(EXPECTED); + } + + }; + + sink.start(); + assertFalse(sink.send(MY_MESSAGE)); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactoryTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactoryTest.java new file mode 100644 index 00000000..c9038068 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactoryTest.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class NoopTopicSourceFactoryTest extends NoopTopicFactoryTest<NoopTopicSourceFactory, NoopTopicSource> { + + @Override + protected NoopTopicSourceFactory buildFactory() { + return new NoopTopicSourceFactory(); + } + + @Test + void testToString() { + assertTrue(new NoopTopicSourceFactory().toString().startsWith("NoopTopicSourceFactory [")); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceTest.java new file mode 100644 index 00000000..51ff109b --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceTest.java @@ -0,0 +1,56 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.event.noop; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class NoopTopicSourceTest extends NoopTopicEndpointTest<NoopTopicSourceFactory, NoopTopicSource> { + + public NoopTopicSourceTest() { + super(new NoopTopicSourceFactory()); + } + + @Override + protected boolean io(String message) { + return this.endpoint.offer(message); + } + + @Test + void testToString() { + assertTrue(this.endpoint.toString().startsWith("NoopTopicSource")); + } + + @Test + void testOffer() { + NoopTopicSource source = new NoopTopicSource(servers, MY_TOPIC) { + @Override + protected boolean broadcast(String message) { + throw new RuntimeException(EXPECTED); + } + + }; + + source.start(); + assertFalse(source.offer(MY_MESSAGE)); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApiTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApiTest.java new file mode 100644 index 00000000..776c5d57 --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApiTest.java @@ -0,0 +1,86 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.features; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.slf4j.Logger; + +@ExtendWith(MockitoExtension.class) +class NetLoggerFeatureApiTest { + + @Mock + private Logger mockLogger; + + @Mock + private EventType mockEventType; + + @Mock + private CommInfrastructure mockCommInfrastructure; + + private NetLoggerFeatureApi featureApi; + + @BeforeEach + public void setUp() { + featureApi = new NetLoggerFeatureApi() { + @Override + public boolean beforeLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + return NetLoggerFeatureApi.super.beforeLog(eventLogger, type, protocol, topic, message); + } + + @Override + public boolean afterLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + return NetLoggerFeatureApi.super.afterLog(eventLogger, type, protocol, topic, message); + } + + @Override + public int getSequenceNumber() { + return 0; + } + + @Override + public String getName() { + return NetLoggerFeatureApi.super.getName(); + } + }; + } + + @Test + void testBeforeLogDefaultBehavior() { + boolean result = featureApi.beforeLog(mockLogger, mockEventType, mockCommInfrastructure, + "testTopic", "testMessage"); + assertFalse(result, "Expected beforeLog to return false by default"); + } + + @Test + void testAfterLogDefaultBehavior() { + boolean result = featureApi.afterLog(mockLogger, mockEventType, mockCommInfrastructure, + "testTopic", "testMessage"); + assertFalse(result, "Expected afterLog to return false by default"); + } +} diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtilsTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtilsTest.java new file mode 100644 index 00000000..55f6a69f --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtilsTest.java @@ -0,0 +1,54 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.utils; + +import static org.onap.policy.common.message.bus.properties.MessageBusProperties.PROPERTY_ADDITIONAL_PROPS_SUFFIX; + +import java.util.Properties; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.utils.properties.PropertyUtils; + +class KafkaPropertyUtilsTest { + + @Test + void test() { + var properties = new Properties(); + properties.setProperty("mytopic" + PROPERTY_ADDITIONAL_PROPS_SUFFIX, "{444-"); + PropertyUtils props = new PropertyUtils(properties, "mytopic", null); + + var build = KafkaPropertyUtils.makeBuilder(props, "mytopic", "servers").build(); + Assertions.assertTrue(build.getAdditionalProps().isEmpty()); + + properties.setProperty("mytopic" + PROPERTY_ADDITIONAL_PROPS_SUFFIX, + "{\"security.protocol\": \"SASL_PLAINTEXT\"}"); + build = KafkaPropertyUtils.makeBuilder(props, "mytopic", "servers").build(); + Assertions.assertTrue(build.getAdditionalProps().containsKey("security.protocol")); + + properties.setProperty("mytopic" + PROPERTY_ADDITIONAL_PROPS_SUFFIX, + "{\"security.protocol\": false }"); + build = KafkaPropertyUtils.makeBuilder(props, "mytopic", "servers").build(); + Assertions.assertTrue(build.getAdditionalProps().isEmpty()); + + properties.setProperty("mytopic" + PROPERTY_ADDITIONAL_PROPS_SUFFIX, ""); + build = KafkaPropertyUtils.makeBuilder(props, "mytopic", "servers").build(); + Assertions.assertTrue(build.getAdditionalProps().isEmpty()); + } + +}
\ No newline at end of file diff --git a/message-bus/src/test/java/org/onap/policy/common/message/bus/utils/NetLoggerUtilTest.java b/message-bus/src/test/java/org/onap/policy/common/message/bus/utils/NetLoggerUtilTest.java new file mode 100644 index 00000000..df621f5b --- /dev/null +++ b/message-bus/src/test/java/org/onap/policy/common/message/bus/utils/NetLoggerUtilTest.java @@ -0,0 +1,269 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.message.bus.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure; +import org.onap.policy.common.message.bus.features.NetLoggerFeatureApi; +import org.onap.policy.common.message.bus.features.NetLoggerFeatureProviders; +import org.onap.policy.common.message.bus.utils.NetLoggerUtil.EventType; +import org.slf4j.Logger; + +/** + * Test class for network log utilities such as logging and feature invocation. + */ +class NetLoggerUtilTest { + + private static final String TEST_TOPIC = "test-topic"; + private static final String MESSAGE = "hello world!"; + /** + * Test feature used for junits. + */ + private static NetLoggerFeature netLoggerFeature; + + /** + * Obtains the test implementation of NetLoggerFeatureApi. + */ + @BeforeAll + public static void setUp() { + netLoggerFeature = (NetLoggerFeature) NetLoggerFeatureProviders.getProviders().getList().get(0); + } + + /** + * Clears events list and resets return/exceptions flags before invoking every unit test. + */ + @BeforeEach + public void reset() { + TestAppender.clear(); + netLoggerFeature.setReturnValue(false, false); + netLoggerFeature.setExceptions(false, false); + } + + /** + * Tests obtaining the network logger instance. + */ + @Test + void getNetworkLoggerTest() { + assertEquals("network", NetLoggerUtil.getNetworkLogger().getName()); + } + + /** + * Tests logging a message to the network logger and invoking features before/after logging. + */ + @Test + void logTest() { + NetLoggerUtil.log(EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(3, TestAppender.events.size()); + } + + /** + * Tests that the network logger is used to log messages if a logger is not passed in. + */ + @Test + void logDefaultTest() { + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(3, TestAppender.events.size()); + assertEquals("network", TestAppender.events.get(0).getLoggerName()); + } + + /** + * Tests a NetLoggerFeature that replaces base implementation before logging. + */ + @Test + void beforeLogReturnTrueTest() { + netLoggerFeature.setReturnValue(true, false); + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(1, TestAppender.events.size()); + } + + /** + * Tests a NetLoggerFeature that post processes a logged message. + */ + @Test + void afterLogReturnTrueTest() { + netLoggerFeature.setReturnValue(false, true); + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(3, TestAppender.events.size()); + } + + /** + * Tests throwing an exception in the before hook. + */ + @Test + void beforeLogExceptionTest() { + netLoggerFeature.setExceptions(true, false); + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(2, TestAppender.events.size()); + } + + /** + * Tests throwing an exception in the after hook. + */ + @Test + void afterLogExceptionTest() { + netLoggerFeature.setExceptions(false, true); + NetLoggerUtil.log(null, EventType.IN, CommInfrastructure.NOOP, TEST_TOPIC, MESSAGE); + assertEquals(2, TestAppender.events.size()); + } + + /** + * A custom list appender to track messages being logged to the network logger. + * NOTE: Check src/test/resources/logback-test.xml for network logger configurations. + */ + public static class TestAppender extends AppenderBase<ILoggingEvent> { + + /** + * List of logged events. + */ + private static final List<ILoggingEvent> events = new ArrayList<>(); + + /** + * Called after every unit test to clear list of events. + */ + public static void clear() { + events.clear(); + } + + /** + * Appends each event to the event list. + */ + @Override + protected void append(ILoggingEvent event) { + events.add(event); + } + + } + + /** + * Test implementation of NetLoggerFeatureApi to be used by junits. + */ + public static class NetLoggerFeature implements NetLoggerFeatureApi { + + /** + * Used for setting the return values of before/after hooks. + */ + private boolean beforeReturn = false; + private boolean afterReturn = false; + + /** + * Used for throwing an exception in the before/after hooks. + */ + private boolean beforeException = false; + private boolean afterException = false; + + + /** + * Gets sequence number. + */ + @Override + public int getSequenceNumber() { + return 0; + } + + /** + * Get beforeLog return value. + */ + public boolean getBeforeReturn() { + return this.beforeReturn; + } + + /** + * Get afterLog return value. + */ + public boolean getAfterReturn() { + return this.afterReturn; + } + + /** + * Sets the return value for the before/after hooks. + * + * @param beforeVal beforeLog() return value + * @param afterVal afterLog() return value + */ + public void setReturnValue(boolean beforeVal, boolean afterVal) { + this.beforeReturn = beforeVal; + this.afterReturn = afterVal; + } + + /** + * Gets beforeException boolean. + */ + public boolean getBeforeException() { + return this.beforeException; + } + + /** + * Gets afterException boolean. + */ + public boolean getAfterException() { + return this.afterException; + } + + /** + * Sets before/after flags to determine if the feature should throw an exception. + */ + public void setExceptions(boolean beforeException, boolean afterException) { + this.beforeException = beforeException; + this.afterException = afterException; + } + + /** + * Simple beforeLog message. + */ + @Override + public boolean beforeLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + + if (beforeException) { + throw new RuntimeException("beforeLog exception"); + } + + eventLogger.info("before feature test"); + + return this.beforeReturn; + } + + /** + * Simple afterLog message. + */ + @Override + public boolean afterLog(Logger eventLogger, EventType type, CommInfrastructure protocol, String topic, + String message) { + + if (afterException) { + throw new RuntimeException("afterLog exception"); + } + + eventLogger.info("after feature test"); + + return this.afterReturn; + } + + } + +} diff --git a/message-bus/src/test/resources/META-INF/services/org.onap.policy.common.message.bus.features.NetLoggerFeatureApi b/message-bus/src/test/resources/META-INF/services/org.onap.policy.common.message.bus.features.NetLoggerFeatureApi new file mode 100644 index 00000000..7889cb8f --- /dev/null +++ b/message-bus/src/test/resources/META-INF/services/org.onap.policy.common.message.bus.features.NetLoggerFeatureApi @@ -0,0 +1 @@ +org.onap.policy.common.message.bus.utils.NetLoggerUtilTest$NetLoggerFeature
\ No newline at end of file diff --git a/message-bus/src/test/resources/logback-test.xml b/message-bus/src/test/resources/logback-test.xml new file mode 100644 index 00000000..ddf4f8bb --- /dev/null +++ b/message-bus/src/test/resources/logback-test.xml @@ -0,0 +1,42 @@ +<!-- + ============LICENSE_START======================================================= + ONAP + ================================================================================ + Copyright (C) 2024 Nordix Foundation. + ================================================================================ + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ============LICENSE_END========================================================= + --> +<configuration> + + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> + <Pattern> + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%M\(%line\) - %msg%n + </Pattern> + </encoder> + </appender> + + <appender name="testAppender" class="org.onap.policy.common.message.bus.utils.NetLoggerUtilTest$TestAppender"/> + + <logger name="org.onap.policy.drools.http.server.test" level="INFO"/> + + <logger name="network" level="INFO"> + <appender-ref ref="testAppender"/> + </logger> + + <root level="WARN"> + <appender-ref ref="STDOUT"/> + </root> + +</configuration>
\ No newline at end of file diff --git a/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.json b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.json new file mode 100644 index 00000000..5b2e712a --- /dev/null +++ b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.json @@ -0,0 +1,30 @@ +{ + "locked": false, + "alive": false, + "topicSources": [ + { + "servers": [ + "my-server" + ], + "topic": "noop-source", + "effectiveTopic": "noop-source", + "recentEvents": [], + "alive": false, + "locked": false, + "topicCommInfrastructure": "NOOP" + } + ], + "topicSinks": [ + { + "servers": [ + "my-server" + ], + "topic": "noop-sink", + "effectiveTopic": "noop-sink", + "recentEvents": [], + "alive": false, + "locked": false, + "topicCommInfrastructure": "NOOP" + } + ] +} diff --git a/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.json b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.json new file mode 100644 index 00000000..462278a4 --- /dev/null +++ b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.json @@ -0,0 +1,14 @@ +{ + "servers" : [ "svra", "svrb" ], + "topic" : "my-topic", + "effectiveTopic" : "my-effective-topic", + "recentEvents" : [ ], + "alive" : false, + "locked" : false, + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "topicCommInfrastructure" : "NOOP" +} diff --git a/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.json b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.json new file mode 100644 index 00000000..1f2fb55f --- /dev/null +++ b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.json @@ -0,0 +1,15 @@ +{ + "servers" : [ "svra", "svrb" ], + "topic" : "my-topic", + "effectiveTopic" : "my-effective-topic", + "recentEvents" : [ ], + "alive" : false, + "locked" : false, + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "topicCommInfrastructure" : "NOOP", + "partitionKey" : "my-partition" +} diff --git a/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.json b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.json new file mode 100644 index 00000000..305620c8 --- /dev/null +++ b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.json @@ -0,0 +1,18 @@ +{ + "servers" : [ "svra", "svrb" ], + "topic" : "my-topic", + "effectiveTopic" : "my-effective-topic", + "recentEvents" : [ ], + "alive" : false, + "locked" : false, + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "consumerGroup" : "my-cons-group", + "consumerInstance" : "my-cons-inst", + "fetchTimeout" : 101, + "fetchLimit" : 100, + "topicCommInfrastructure" : "NOOP" +} diff --git a/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/TopicBaseTest.json b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/TopicBaseTest.json new file mode 100644 index 00000000..b72b4efd --- /dev/null +++ b/message-bus/src/test/resources/org/onap/policy/common/message/bus/event/base/TopicBaseTest.json @@ -0,0 +1,9 @@ +{ + "servers" : [ "svra", "svrb" ], + "topic" : "my-topic", + "effectiveTopic" : "my-topic", + "recentEvents" : [ ], + "alive" : false, + "locked" : false, + "topicCommInfrastructure" : "NOOP" +} diff --git a/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_all_params.json b/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_all_params.json new file mode 100644 index 00000000..89e464dd --- /dev/null +++ b/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_all_params.json @@ -0,0 +1,62 @@ +{ + "topicSources" : [ { + "topic" : "policy-pdp-pap1", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka", + "effectiveTopic" : "my-effective-topic", + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "port": 123, + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "consumerGroup" : "consumer group", + "consumerInstance" : "consumer instance", + "fetchTimeout" : 15000, + "fetchLimit" : 100, + "userName": "username", + "password": "password", + "managed": true, + "environment": "environment1", + "aftEnvironment": "aftEnvironment1", + "partner": "partner1", + "latitude": "1234", + "longitude": "1234", + "partitionId": "partition_id", + "additionalProps": {"xyz":"xyz"}, + "clientName": "clientName1", + "hostname": "hostname1", + "basePath": "basePath1", + "serializationProvider": "serializationProvider1" + }], + "topicSinks" : [ { + "topic" : "policy-pdp-pap1", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka", + "effectiveTopic" : "my-effective-topic", + "apiKey" : "my-api-key", + "apiSecret" : "my-api-secret", + "port": 123, + "useHttps" : true, + "allowTracing": true, + "allowSelfSignedCerts" : true, + "consumerGroup" : "consumer group", + "consumerInstance" : "consumer instance", + "fetchTimeout" : 15000, + "fetchLimit" : 100, + "userName": "username", + "password": "password", + "managed": true, + "environment": "environment1", + "aftEnvironment": "aftEnvironment1", + "partner": "partner1", + "latitude": "1234", + "longitude": "1234", + "partitionId": "partition_id", + "additionalProps": {"xyz":"xyz"}, + "clientName": "clientName1", + "hostname": "hostname1", + "basePath": "basePath1", + "serializationProvider": "serializationProvider1" + }] +}
\ No newline at end of file diff --git a/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_invalid.json b/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_invalid.json new file mode 100644 index 00000000..775b4886 --- /dev/null +++ b/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_invalid.json @@ -0,0 +1,6 @@ +{ + "topicSources" : [{ + "topic" : "ueb-source", + "servers" : ["my-server"] + }] +}
\ No newline at end of file diff --git a/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_missing_mandatory.json b/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_missing_mandatory.json new file mode 100644 index 00000000..216c11ec --- /dev/null +++ b/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_missing_mandatory.json @@ -0,0 +1,12 @@ +{ + "topicSources" : [ { + "topic" : "policy-pdp-pap1", + "servers" : [], + "topicCommInfrastructure" : "kafka" + }], + "topicSinks" : [ { + "topic" : "policy-pdp-pap2", + "servers" : [ "kafka1, kafka2" ], + "topicCommInfrastructure" : "kafka" + }] +}
\ No newline at end of file diff --git a/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_valid.json b/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_valid.json new file mode 100644 index 00000000..2603bfdc --- /dev/null +++ b/message-bus/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_valid.json @@ -0,0 +1,30 @@ +{ + "topicSources" : [ { + "topic" : "ueb-source", + "servers" : [ "my-server" ], + "topicCommInfrastructure" : "ueb" + },{ + "topic" : "policy-pdp-pap1", + "servers" : [ "kafka1, kafka2" ], + "topicCommInfrastructure" : "kafka" + },{ + "topic" : "policy-pdp-pap2", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka" + }], + "topicSinks" : [ { + "topic" : "ueb-sink", + "servers" : [ "my-server" ], + "topicCommInfrastructure" : "ueb" + },{ + "topic" : "policy-pdp-pap2", + "servers" : [ "kafka1, kafka2" ], + "topicCommInfrastructure" : "kafka" + },{ + "topic" : "policy-pdp-pap3", + "servers" : [ "kafka2, kafka3" ], + "topicCommInfrastructure" : "kafka", + "effectiveTopic":"effectiveTopic1", + "allowSelfSignedCerts":true + }] +}
\ No newline at end of file |