Decouple policy-common from clamp 36/142636/2
authordanielhanrahan <daniel.hanrahan@est.tech>
Wed, 3 Dec 2025 02:35:13 +0000 (02:35 +0000)
committerdanielhanrahan <daniel.hanrahan@est.tech>
Thu, 4 Dec 2025 18:53:24 +0000 (18:53 +0000)
- Copy used files from policy/common repo into clamp repo
- Copy unit tests of those files
- Update POMs to use the new internal policy-common module

Issue-ID: POLICY-5494
Signed-off-by: danielhanrahan <daniel.hanrahan@est.tech>
Change-Id: Ia58a1c56b72518adc09a19a81968f8091142f042

278 files changed:
common/pom.xml
models/pom.xml
participant/participant-intermediary/pom.xml
policy-common/pom.xml [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/capabilities/Lockable.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/capabilities/Startable.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/JsonListener.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/ScoListener.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/DoubleConverter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/InstantTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/JacksonExclusionStrategy.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/JacksonFieldAdapterFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/JacksonHandler.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/JacksonMethodAdapterFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/MapDoubleAdapterFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/OffsetDateTimeTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/OffsetTimeTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/StringTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnyGetter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnySetter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonIgnore.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonProperty.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/Adapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/AnyGetterSerializer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/AnySetterDeserializer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/ClassWalker.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/Deserializer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldDeserializer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldSerializer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/JacksonTypeAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/Lifter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodAdapter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodDeserializer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodSerializer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/gson/internal/Serializer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/Topic.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpoint.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointManager.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointProxy.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicListener.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicRegisterable.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSink.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSource.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/ApiKeyEnabled.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusConsumer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusPublisher.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicBase.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSink.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSource.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSink.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSource.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBase.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseHashedFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClient.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientException.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSinkFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSink.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapper.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactories.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSink.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSource.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSource.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpoint.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactories.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSink.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSource.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApi.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureProviders.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheck.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactory.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheck.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheck.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/properties/MessageBusProperties.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtils.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/message/bus/utils/NetLoggerUtil.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidator.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/FieldValidator.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/ItemValidator.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/ObjectValidationResult.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/ParameterConstants.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroup.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroupImpl.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResult.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResultImpl.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/ValidationStatus.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/ValueValidator.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/annotations/ClassName.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Max.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Min.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotBlank.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotNull.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Size.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/topic/BusTopicParams.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameterGroup.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameters.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupConstraint.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupValidator.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategy.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverter.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/coder/Coder.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/coder/CoderException.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoderObject.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/coder/YamlJsonTranslator.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonSerializer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtils.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilder.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/network/NetworkUtil.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/properties/PropertyUtils.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/resources/ResourceUtils.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/services/FeatureApiUtils.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedService.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedServiceImpl.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerContainer.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/test/ConstructionError.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/test/ErrorsTester.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/test/ExceptionsTester.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/test/ThrowablesTester.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/test/ToStringTester.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/test/log/logback/ExtractAppender.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/validation/Assertions.java [new file with mode: 0644]
policy-common/src/main/java/org/onap/policy/common/utils/validation/Version.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/JsonListenerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcherTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/ScoListenerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/DoubleConverterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/InstantTypeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/JacksonExclusionStrategyTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/JacksonFieldAdapterFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/JacksonHandlerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/JacksonMethodAdapterFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTypeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/MapDoubleAdapterFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/OffsetDateTimeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/OffsetTimeTypeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/StringTypeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/AdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/AnyGetterSerializerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/AnySetterDeserializerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/ClassWalkerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/DataAdapterFactory.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldDeserializerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldSerializerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/JacksonTypeAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/LifterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodAdapterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodDeserializerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodSerializerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/CommonTestData.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicParameterGroupTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusConsumerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicFactoryTestBase.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicBaseTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicFactoryTestBase.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicPropertyBuilder.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicTestBase.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicClientExceptionTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSinkTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapperTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactoryTestBase.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicPropertyBuilder.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSourceTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpointTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicPropertyBuilder.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApiTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactoryTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheckTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheckTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtilsTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/message/bus/utils/NetLoggerUtilTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/ParameterGroupTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/TestFieldValidator.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/TestItemValidator.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/TestObjectValidationResult.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/TestValidationResultImpl.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/TestValueValidator.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/ValidatorUtil.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/topic/BusTopicParamsTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/parameters/validation/ParameterGroupValidatorTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategyTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderExceptionTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderObjectTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/coder/YamlJsonTranslatorTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonSerializerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilderTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/network/NetworkUtilTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/properties/PropertyUtilsTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/services/FeatureApiUtilsTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceImplTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerContainerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/test/ConstructionErrorTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/test/ErrorsTesterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/test/ExceptionsTesterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/test/ThrowablesTesterTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/test/log/logback/ExtractAppenderTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/validation/AssertionsTest.java [new file with mode: 0644]
policy-common/src/test/java/org/onap/policy/common/utils/validation/VersionTest.java [new file with mode: 0644]
policy-common/src/test/resources/META-INF/services/org.onap.policy.common.message.bus.features.NetLoggerFeatureApi [new file with mode: 0644]
policy-common/src/test/resources/META-INF/services/org.onap.policy.common.utils.services.OrderedServiceImplTest$GenericService [new file with mode: 0644]
policy-common/src/test/resources/keystore-test [new file with mode: 0644]
policy-common/src/test/resources/logback-test.xml [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/endpoints/http/server/internal/HttpServerTest.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_invalid.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_valid.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_all_params.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_invalid.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_missing_mandatory.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_valid.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/TopicBaseTest.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_all_params.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_invalid.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_missing_mandatory.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_valid.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/utils/coder/StandardCoder.json [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml [new file with mode: 0644]
policy-common/src/test/resources/org/onap/policy/common/utils/gson/GsonTestUtilsTest.json [new file with mode: 0644]
policy-common/src/test/resources/testdir/testfile.xml [new file with mode: 0644]
policy-common/src/test/resources/version.txt [new file with mode: 0644]
policy-common/src/test/resources/webapps/alt-root/index.html [new file with mode: 0644]
policy-common/src/test/resources/webapps/root/index.html [new file with mode: 0644]
policy-models/pom.xml
pom.xml
runtime-acm/pom.xml

index 5fa183b..86f698c 100644 (file)
             <artifactId>policy-models</artifactId>
             <version>${project.version}</version>
         </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>message-bus</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
         <dependency>
             <groupId>jakarta.ws.rs</groupId>
             <artifactId>jakarta.ws.rs-api</artifactId>
index 0524f95..54d3316 100644 (file)
             <artifactId>policy-models</artifactId>
             <version>${project.version}</version>
         </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>common-parameters</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>utils</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>utils-test</artifactId>
-            <version>${policy.common.version}</version>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
index 5b6102f..48e8d35 100644 (file)
     </description>
 
     <dependencies>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>common-parameters</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>message-bus</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>policy-endpoints</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>utils</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
         <dependency>
             <groupId>io.micrometer</groupId>
             <artifactId>micrometer-core</artifactId>
diff --git a/policy-common/pom.xml b/policy-common/pom.xml
new file mode 100644 (file)
index 0000000..ebfd235
--- /dev/null
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ============LICENSE_START=======================================================
+  Copyright (C) 2025 OpenInfra Foundation Europe. 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=========================================================
+-->
+
+<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.clamp</groupId>
+        <artifactId>policy-clamp</artifactId>
+        <version>9.0.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>policy-common</artifactId>
+    <name>${project.artifactId}</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+        </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>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.ws.rs</groupId>
+            <artifactId>jakarta.ws.rs-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.kafka</groupId>
+            <artifactId>kafka-clients</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-collections4</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-jexl3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.hibernate.orm</groupId>
+            <artifactId>hibernate-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.re2j</groupId>
+            <artifactId>re2j</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.openpojo</groupId>
+            <artifactId>openpojo</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>compile</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.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/policy-common/src/main/java/org/onap/policy/common/capabilities/Lockable.java b/policy-common/src/main/java/org/onap/policy/common/capabilities/Lockable.java
new file mode 100644 (file)
index 0000000..cc9da0a
--- /dev/null
@@ -0,0 +1,48 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * policy-core
+ * ================================================================================
+ * Copyright (C) 2017 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.capabilities;
+
+/**
+ * Interface for classes that supports lock/unlock operations.
+ */
+public interface Lockable {
+
+    /**
+     * Locks this entity.
+     * 
+     * @return true is the lock operation was successful, false otherwise
+     */
+    public boolean lock();
+
+    /**
+     * Unlocks this entity.
+     * 
+     * @return true is the unlock operation was successful, false otherwise
+     */
+    public boolean unlock();
+
+    /**
+     * Checks if this entity is locked.
+     * 
+     * @return true if the entity is in a locked state, false otherwise
+     */
+    public boolean isLocked();
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/capabilities/Startable.java b/policy-common/src/main/java/org/onap/policy/common/capabilities/Startable.java
new file mode 100644 (file)
index 0000000..0ce4da5
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * ============LICENSE_START=======================================================
+ * policy-core
+ * ================================================================================
+ * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2023 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.capabilities;
+
+/**
+ * Interface for classes that supports start-like operations.
+ */
+public interface Startable {
+
+    /**
+     * Start operation. This operation starts the entity.
+     * 
+     * @return boolean. true if the start operation was successful, otherwise false.
+     * @throws IllegalStateException if the element is in a state that conflicts with the start
+     *         operation.
+     */
+    boolean start();
+
+    /**
+     * Stop operation. The entity can be restarted again by invoking the start operation.
+     * 
+     * @return boolean. true if the stop operation was successful, otherwise false.
+     * @throws IllegalStateException if the element is in a state that conflicts with the stop
+     *         operation.
+     */
+    boolean stop();
+
+    /**
+     * shutdown operation. The terminate operation yields the entity unusuable. It cannot be
+     * (re)started.
+     * 
+     * @throws IllegalStateException if the element is in a state that conflicts with the stop
+     *         operation.
+     */
+    void shutdown();
+
+    /**
+     * Checks if the entity is alive.
+     * 
+     * @return boolean. true if alive, otherwise false
+     */
+    boolean isAlive();
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/JsonListener.java b/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/JsonListener.java
new file mode 100644 (file)
index 0000000..90c8338
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.endpoints.listeners;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+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.coder.Coder;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.common.utils.coder.StandardCoderObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Listens for messages received on a topic, in JSON format, decodes them into a
+ * {@link StandardCoderObject}, and then offers the objects to the subclass.
+ */
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public abstract class JsonListener implements TopicListener {
+    private static final Logger logger = LoggerFactory.getLogger(JsonListener.class);
+
+    /**
+     * Used to decode the event.
+     */
+    private static final Coder coder = new StandardCoder();
+
+    @Override
+    public void onTopicEvent(CommInfrastructure infra, String topic, String event) {
+        // decode from JSON into a standard object
+        StandardCoderObject sco;
+        try {
+            sco = coder.decode(event, StandardCoderObject.class);
+
+        } catch (CoderException e) {
+            logger.warn("unable to decode: {}", event, e);
+            return;
+        }
+
+        onTopicEvent(infra, topic, sco);
+    }
+
+    /**
+     * Indicates that a standard object was received.
+     *
+     * @param infra infrastructure with which the message was received
+     * @param topic topic on which the message was received
+     * @param sco the standard object that was received
+     */
+    public abstract void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco);
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java b/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java
new file mode 100644 (file)
index 0000000..6639b41
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * ============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.endpoints.listeners;
+
+import java.util.concurrent.ConcurrentHashMap;
+import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure;
+import org.onap.policy.common.utils.coder.StandardCoderObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Dispatches standard objects to listeners, based on the message type extracted from the
+ * message. Only one listener may be registered for a given type.
+ */
+public class MessageTypeDispatcher extends JsonListener {
+    private static final Logger logger = LoggerFactory.getLogger(MessageTypeDispatcher.class);
+
+    /**
+     * Name of the message field, which may be hierarchical.
+     */
+    private final Object[] messageFieldNames;
+
+    /**
+     * Name of the message field, joined with "." - for logging.
+     */
+    private final String fullMessageFieldName;
+
+    /**
+     * Maps a message type to its listener.
+     */
+    private final ConcurrentHashMap<String, ScoListener<?>> type2listener = new ConcurrentHashMap<>();
+
+    /**
+     * Constructs the object.
+     *
+     * @param messageFieldNames name of the message field, which may be hierarchical
+     */
+    public MessageTypeDispatcher(String... messageFieldNames) {
+        this.messageFieldNames = messageFieldNames;
+        this.fullMessageFieldName = String.join(".", messageFieldNames);
+    }
+
+    /**
+     * Registers a listener for a certain type of message.
+     *
+     * @param type type of message of interest to the listener
+     * @param listener listener to register
+     */
+    public <T> void register(String type, ScoListener<T> listener) {
+        type2listener.put(type, listener);
+    }
+
+    /**
+     * Unregisters the listener associated with the specified message type.
+     *
+     * @param type type of message whose listener is to be unregistered
+     */
+    public void unregister(String type) {
+        type2listener.remove(type);
+    }
+
+    @Override
+    public void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco) {
+        // extract the message type
+        final var type = sco.getString(messageFieldNames);
+        if (type == null) {
+            logger.warn("unable to extract {}: {}", fullMessageFieldName, sco);
+            return;
+        }
+
+        // dispatch the message
+        ScoListener<?> listener = type2listener.get(type);
+        if (listener == null) {
+            logger.info("discarding event of type {}", type);
+            return;
+        }
+
+        listener.onTopicEvent(infra, topic, sco);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/ScoListener.java b/policy-common/src/main/java/org/onap/policy/common/endpoints/listeners/ScoListener.java
new file mode 100644 (file)
index 0000000..bdb30eb
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.endpoints.listeners;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import org.onap.policy.common.message.bus.event.Topic.CommInfrastructure;
+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.onap.policy.common.utils.coder.StandardCoderObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Listens for receipt of a {@link StandardCoderObject}, translating it into an object of
+ * the appropriate type, and then passing it to the subclass.
+ *
+ * @param <T> type of message/POJO this handles
+ */
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+public abstract class ScoListener<T> {
+
+    private static final Logger logger = LoggerFactory.getLogger(ScoListener.class);
+
+    /**
+     * Used to translate the standard object to an object of type "T".
+     */
+    private static final Coder coder = new StandardCoder();
+
+    /**
+     * Class of message this handles.
+     */
+    private final Class<T> clazz;
+
+    /**
+     * Receives an event, translates it into the desired type of object, and passes it to
+     * the subclass.
+     *
+     * @param infra infrastructure with which the message was received
+     * @param topic topic on which the message was received
+     * @param sco event that was received
+     */
+    public void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco) {
+        // translate the event to the desired object type
+        final T msg;
+        try {
+            msg = coder.fromStandard(sco, clazz);
+
+        } catch (CoderException e) {
+            logger.warn("unable to decode {}: {}", clazz.getName(), sco, e);
+            return;
+        }
+
+        onTopicEvent(infra, topic, sco, msg);
+    }
+
+    /**
+     * Indicates that a message was received.
+     *
+     * @param infra infrastructure with which the message was received
+     * @param topic topic on which the message was received
+     * @param sco event that was received
+     * @param message message that was received
+     */
+    public abstract void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco, T message);
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/DoubleConverter.java b/policy-common/src/main/java/org/onap/policy/common/gson/DoubleConverter.java
new file mode 100644 (file)
index 0000000..4d10bd1
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+/**
+ * Converter for Double values. By default, GSON treats all Objects that are numbers, as
+ * Double. This converts Doubles to Integer or Long, if possible. It converts stand-alone
+ * Doubles, as well as those found within Arrays and Maps.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class DoubleConverter {
+
+    /**
+     * Performs in-place conversion of all values in a list.
+     *
+     * @param list the list whose values are to be converted
+     */
+    public static void convertFromDouble(List<Object> list) {
+        if (list == null) {
+            return;
+        }
+
+        List<Object> original = new ArrayList<>(list);
+
+        list.clear();
+        original.forEach(item -> list.add(convertFromDouble(item)));
+    }
+
+    /**
+     * Performs in-place conversion of all values in a map.
+     *
+     * @param map the map whose values are to be converted
+     */
+    public static void convertFromDouble(Map<String, Object> map) {
+        if (map == null) {
+            return;
+        }
+
+        Set<Entry<String, Object>> set = map.entrySet();
+
+        for (Entry<String, Object> entry : set) {
+            entry.setValue(convertFromDouble(entry.getValue()));
+        }
+    }
+
+    /**
+     * Converts a value. If the value is a List, then it recursively converts the
+     * entries of the List. Likewise with a map, however, the map is converted in place.
+     *
+     * @param value value to be converted
+     * @return the converted value
+     */
+    @SuppressWarnings({"unchecked"})
+    public static Object convertFromDouble(Object value) {
+        if (value == null) {
+            return value;
+        }
+
+        if (value instanceof List) {
+            convertFromDouble((List<Object>) value);
+            return value;
+        }
+
+        if (value instanceof Map) {
+            convertFromDouble((Map<String, Object>) value);
+            return value;
+        }
+
+        if (!(value instanceof Double)) {
+            return value;
+        }
+
+        Double num = (Double) value;
+        var longval = num.longValue();
+
+        if (Double.compare(num.doubleValue(), longval) != 0) {
+            // it isn't integral - return unchanged value
+            return value;
+        }
+
+        // it's integral - determine if it's an integer or a long
+        var intval = (int) longval;
+
+        if (intval == longval) {
+            return intval;
+        }
+
+        return longval;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java b/policy-common/src/main/java/org/onap/policy/common/gson/GsonMessageBodyHandler.java
new file mode 100644 (file)
index 0000000..a693b7f
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2023 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.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.ext.MessageBodyReader;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.Provider;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import lombok.AccessLevel;
+import lombok.Getter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provider that serializes and de-serializes JSON via gson.
+ */
+@Provider
+@Consumes(MediaType.WILDCARD)
+@Produces(MediaType.WILDCARD)
+public class GsonMessageBodyHandler implements MessageBodyReader<Object>, MessageBodyWriter<Object> {
+
+    public static final Logger logger = LoggerFactory.getLogger(GsonMessageBodyHandler.class);
+
+    /**
+     * Object to be used to serialize and de-serialize.
+     */
+    @Getter(AccessLevel.PROTECTED)
+    private final Gson gson;
+
+    /**
+     * Constructs the object, using a Gson object that translates Doubles inside of Maps
+     * into Integer/Long, where possible.
+     */
+    public GsonMessageBodyHandler() {
+        this(configBuilder(new GsonBuilder()).create());
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson the Gson object to be used to serialize and de-serialize
+     */
+    public GsonMessageBodyHandler(Gson gson) {
+        this.gson = gson;
+
+        logger.info("Using GSON for REST calls");
+    }
+
+    /**
+     * Configures a builder with the adapters normally used by this handler (e.g., mapper
+     * that converts Double to Integer).
+     *
+     * @param builder builder to be configured
+     * @return the configured builder
+     */
+    public static GsonBuilder configBuilder(GsonBuilder builder) {
+        return builder.disableHtmlEscaping().registerTypeAdapterFactory(new MapDoubleAdapterFactory())
+                        .registerTypeAdapter(Instant.class, new InstantTypeAdapter())
+                        .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter())
+                        .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter())
+                        .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter())
+                        .registerTypeAdapter(OffsetTime.class, new OffsetTimeTypeAdapter())
+                        .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter())
+                        .registerTypeAdapter(ZoneOffset.class, new ZoneOffsetTypeAdapter());
+    }
+
+    @Override
+    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return canHandle(mediaType);
+    }
+
+    @Override
+    public long getSize(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return -1;
+    }
+
+    @Override
+    public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
+                    MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException {
+
+        try (var writer = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)) {
+            Type jsonType = (type.equals(genericType) ? type : genericType);
+            gson.toJson(object, jsonType, writer);
+        }
+    }
+
+    @Override
+    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return canHandle(mediaType);
+    }
+
+    /**
+     * Determines if this provider can handle the given media type.
+     *
+     * @param mediaType the media type of interest
+     * @return {@code true} if this provider handles the given media type, {@code false}
+     *         otherwise
+     */
+    private boolean canHandle(MediaType mediaType) {
+        if (mediaType == null) {
+            return true;
+        }
+
+        String subtype = mediaType.getSubtype();
+
+        if ("json".equalsIgnoreCase(subtype) || "javascript".equals(subtype)) {
+            return true;
+        }
+
+        return subtype.endsWith("+json") || "x-json".equals(subtype) || "x-javascript".equals(subtype);
+    }
+
+    @Override
+    public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
+                    MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException {
+
+        try (var streamReader = new InputStreamReader(entityStream, StandardCharsets.UTF_8)) {
+            Type jsonType = (type.equals(genericType) ? type : genericType);
+            return gson.fromJson(streamReader, jsonType);
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapter.java
new file mode 100644 (file)
index 0000000..c38a3e9
--- /dev/null
@@ -0,0 +1,54 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020-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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.time.Instant;
+
+/**
+ * GSON Type Adapter for "Instant" fields, that encodes them as milliseconds.
+ */
+public class InstantAsMillisTypeAdapter extends TypeAdapter<Instant> {
+
+    @Override
+    public void write(JsonWriter out, Instant value) throws IOException {
+        if (value == null) {
+            out.nullValue();
+        } else {
+            out.value(value.toEpochMilli());
+        }
+    }
+
+    @Override
+    public Instant read(JsonReader in) throws IOException {
+        if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+        } else {
+            var millis = in.nextLong();
+            return Instant.ofEpochMilli(millis);
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/InstantTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/InstantTypeAdapter.java
new file mode 100644 (file)
index 0000000..bad66af
--- /dev/null
@@ -0,0 +1,36 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020-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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import java.time.Instant;
+
+/**
+ * GSON Type Adapter for "Instant" fields, that uses the standard ISO_INSTANT formatter.
+ */
+public class InstantTypeAdapter extends StringTypeAdapter<Instant> {
+
+    /**
+     * Constructs an adapter.
+     */
+    public InstantTypeAdapter() {
+        super("date", Instant::parse, Instant::toString);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/JacksonExclusionStrategy.java b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonExclusionStrategy.java
new file mode 100644 (file)
index 0000000..cb959c4
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.JsonElement;
+import java.lang.reflect.GenericArrayType;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Excludes all fields from serialization/deserialization, if the class is managed.
+ */
+public class JacksonExclusionStrategy implements ExclusionStrategy {
+
+    /**
+     * Classes that are explicitly not managed by the GSON jackson adapters.
+     */
+    // @formatter:off
+    private static final Set<Class<?>> unmanaged = new HashSet<>(Arrays.asList(
+                    boolean.class,
+                    byte.class,
+                    short.class,
+                    int.class,
+                    long.class,
+                    float.class,
+                    double.class,
+                    char.class,
+                    Boolean.class,
+                    Byte.class,
+                    Short.class,
+                    Integer.class,
+                    Long.class,
+                    Float.class,
+                    Double.class,
+                    Character.class,
+                    String.class));
+    // @formatter:on
+
+    /**
+     * Classes whose subclasses are explicitly not managed by the GSON jackson adapters.
+     */
+    // @formatter:off
+    private static final Set<Class<?>> unmanagedSuper = new HashSet<>(Arrays.asList(
+                    GenericArrayType.class,
+                    Map.class,
+                    Collection.class,
+                    JsonElement.class));
+    // @formatter:on
+
+    @Override
+    public boolean shouldSkipField(FieldAttributes attrs) {
+        return isManaged(attrs.getDeclaringClass());
+    }
+
+    @Override
+    public boolean shouldSkipClass(Class<?> clazz) {
+        return false;
+    }
+
+    /**
+     * Determines if a class is managed by this adapter, which typically means that it is
+     * <i>not</i> a generic class such as {@link JsonElement} or some type of collection.
+     *
+     * @param clazz the class to be examined
+     * @return {@code true} if the class is managed by this adapter, {@code false}
+     *         otherwise
+     */
+    public static boolean isManaged(Class<?> clazz) {
+        if (clazz.isArray() || clazz.isEnum() || clazz.isPrimitive() || unmanaged.contains(clazz)) {
+            return false;
+        }
+
+        for (Class<?> sup : unmanagedSuper) {
+            if (sup.isAssignableFrom(clazz)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/JacksonFieldAdapterFactory.java b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonFieldAdapterFactory.java
new file mode 100644 (file)
index 0000000..18157b0
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import org.onap.policy.common.gson.internal.ClassWalker;
+import org.onap.policy.common.gson.internal.Deserializer;
+import org.onap.policy.common.gson.internal.FieldDeserializer;
+import org.onap.policy.common.gson.internal.FieldSerializer;
+import org.onap.policy.common.gson.internal.JacksonTypeAdapter;
+import org.onap.policy.common.gson.internal.Serializer;
+
+/**
+ * Factory that serializes/deserializes class fields following the normal behavior of
+ * jackson. Supports the following annotations:
+ * <ul>
+ * <li>GsonJsonIgnore</li>
+ * <li>GsonJsonProperty</li>
+ * </ul>
+ *
+ * <p>Note: {@link JacksonExclusionStrategy} must also be registered with the gson object.
+ */
+public class JacksonFieldAdapterFactory implements TypeAdapterFactory {
+
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        Class<? super T> clazz = type.getRawType();
+
+        if (!JacksonExclusionStrategy.isManaged(clazz)) {
+            return null;
+        }
+
+        var data = new ClassWalker();
+        data.walkClassHierarchy(clazz);
+
+        if (data.getInProps(Field.class).isEmpty() && data.getOutProps(Field.class).isEmpty()) {
+            // no fields to serialize
+            return null;
+        }
+
+        TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+        List<Serializer> sers = makeSerializers(gson, data);
+        List<Deserializer> desers = makeDeserializers(gson, data);
+
+        return new JacksonTypeAdapter<>(gson, delegate, sers, desers);
+    }
+
+    /**
+     * Creates a complete list of serializers.
+     *
+     * @param gson the associated gson object
+     * @param data data used to configure the serializers
+     * @return a list of all serializers
+     */
+    private List<Serializer> makeSerializers(Gson gson, ClassWalker data) {
+        List<Serializer> ser = new ArrayList<>();
+
+        data.getOutProps(Field.class).forEach(field -> ser.add(new FieldSerializer(gson, field)));
+
+        return ser;
+    }
+
+    /**
+     * Creates a complete list of deserializers.
+     *
+     * @param gson the associated gson object
+     * @param data data used to configure the deserializers
+     * @return a list of all deserializers
+     */
+    private List<Deserializer> makeDeserializers(Gson gson, ClassWalker data) {
+        List<Deserializer> deser = new ArrayList<>();
+
+        data.getInProps(Field.class).forEach(field -> deser.add(new FieldDeserializer(gson, field)));
+
+        return deser;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/JacksonHandler.java b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonHandler.java
new file mode 100644 (file)
index 0000000..420fbdb
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provider used to serialize and deserialize policy objects via gson using jackson
+ * default behaviors and annotations.
+ */
+public class JacksonHandler extends GsonMessageBodyHandler {
+
+    public static final Logger logger = LoggerFactory.getLogger(JacksonHandler.class);
+
+    /**
+     * Constructs the object.
+     */
+    public JacksonHandler() {
+        this(configBuilder(new GsonBuilder()).create());
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson the Gson object to be used to serialize and de-serialize
+     */
+    public JacksonHandler(Gson gson) {
+        super(gson);
+        logger.info("Using GSON with Jackson behaviors for REST calls");
+    }
+
+    /**
+     * Configures a builder with the adapters normally used by this handler (e.g.,
+     * adapters for GsonJsonXxx annotations).
+     *
+     * @param builder builder to be configured
+     * @return the configured builder
+     */
+    public static GsonBuilder configBuilder(GsonBuilder builder) {
+        return builder.registerTypeAdapterFactory(new JacksonFieldAdapterFactory())
+                        .registerTypeAdapterFactory(new JacksonMethodAdapterFactory())
+                        .registerTypeAdapterFactory(new MapDoubleAdapterFactory())
+                        .setExclusionStrategies(new JacksonExclusionStrategy())
+                        .disableHtmlEscaping();
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/JacksonMethodAdapterFactory.java b/policy-common/src/main/java/org/onap/policy/common/gson/JacksonMethodAdapterFactory.java
new file mode 100644 (file)
index 0000000..b741400
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.onap.policy.common.gson.internal.AnyGetterSerializer;
+import org.onap.policy.common.gson.internal.AnySetterDeserializer;
+import org.onap.policy.common.gson.internal.ClassWalker;
+import org.onap.policy.common.gson.internal.Deserializer;
+import org.onap.policy.common.gson.internal.JacksonTypeAdapter;
+import org.onap.policy.common.gson.internal.MethodDeserializer;
+import org.onap.policy.common.gson.internal.MethodSerializer;
+import org.onap.policy.common.gson.internal.Serializer;
+
+/**
+ * Factory that serializes/deserializes class methods following the normal behavior of
+ * jackson. Supports the following annotations:
+ * <ul>
+ * <li>GsonJsonIgnore</li>
+ * <li>GsonJsonProperty</li>
+ * <li>GsonJsonAnyGetter</li>
+ * <li>GsonJsonAnySetter</li>
+ * </ul>
+ */
+public class JacksonMethodAdapterFactory implements TypeAdapterFactory {
+
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        Class<? super T> clazz = type.getRawType();
+
+        if (!JacksonExclusionStrategy.isManaged(clazz)) {
+            return null;
+        }
+
+        var data = new ClassWalker();
+        data.walkClassHierarchy(clazz);
+
+        if (data.getInProps(Method.class).isEmpty() && data.getOutProps(Method.class).isEmpty()
+                        && data.getAnyGetter() == null && data.getAnySetter() == null) {
+            // no methods to serialize
+            return null;
+        }
+
+        Set<String> unliftedProps = new HashSet<>();
+        unliftedProps.addAll(data.getInNotIgnored());
+        unliftedProps.addAll(data.getOutNotIgnored());
+
+        TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+        List<Serializer> sers = makeSerializers(gson, data, unliftedProps);
+        List<Deserializer> desers = makeDeserializers(gson, data, unliftedProps);
+
+        return new JacksonTypeAdapter<>(gson, delegate, sers, desers);
+    }
+
+    /**
+     * Creates a complete list of serializers.
+     *
+     * @param gson the associated gson object
+     * @param data data used to configure the serializers
+     * @param unliftedProps properties that should not be lowered by "any-getters"
+     * @return a list of all serializers
+     */
+    private List<Serializer> makeSerializers(Gson gson, ClassWalker data, Set<String> unliftedProps) {
+        List<Serializer> ser = new ArrayList<>();
+
+        if (data.getAnyGetter() != null) {
+            ser.add(new AnyGetterSerializer(gson, unliftedProps, data.getAnyGetter()));
+        }
+
+        data.getOutProps(Method.class).forEach(method -> ser.add(new MethodSerializer(gson, method)));
+
+        return ser;
+    }
+
+    /**
+     * Creates a complete list of deserializers.
+     *
+     * @param gson the associated gson object
+     * @param data data used to configure the deserializers
+     * @param unliftedProps properties that should not be lifted by "any-setters"
+     * @return a list of all deserializers
+     */
+    private List<Deserializer> makeDeserializers(Gson gson, ClassWalker data, Set<String> unliftedProps) {
+        List<Deserializer> deser = new ArrayList<>();
+
+        if (data.getAnySetter() != null) {
+            deser.add(new AnySetterDeserializer(gson, unliftedProps, data.getAnySetter()));
+        }
+
+        data.getInProps(Method.class).forEach(method -> deser.add(new MethodDeserializer(gson, method)));
+
+        return deser;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapter.java
new file mode 100644 (file)
index 0000000..5dc597e
--- /dev/null
@@ -0,0 +1,39 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020-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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * GSON Type Adapter for "LocalDateTime" fields, that uses the standard
+ * ISO_LOCAL_DATE_TIME formatter, by default.
+ */
+public class LocalDateTimeTypeAdapter extends StringTypeAdapter<LocalDateTime> {
+
+    public LocalDateTimeTypeAdapter() {
+        this(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+    }
+
+    public LocalDateTimeTypeAdapter(DateTimeFormatter formatter) {
+        super("date", string -> LocalDateTime.parse(string, formatter), value -> value.format(formatter));
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/LocalDateTypeAdapter.java
new file mode 100644 (file)
index 0000000..0f666e5
--- /dev/null
@@ -0,0 +1,35 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+public class LocalDateTypeAdapter extends StringTypeAdapter<LocalDate> {
+
+    public LocalDateTypeAdapter() {
+        this(DateTimeFormatter.ISO_LOCAL_DATE);
+    }
+
+    public LocalDateTypeAdapter(DateTimeFormatter formatter) {
+        super("date", string -> LocalDate.parse(string, formatter), value -> value.format(formatter));
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/MapDoubleAdapterFactory.java b/policy-common/src/main/java/org/onap/policy/common/gson/MapDoubleAdapterFactory.java
new file mode 100644 (file)
index 0000000..057e97f
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Adapter factory for Map&lt;String,Object&gt; and List&lt;String&gt;. By default, GSON treats all Objects, that
+ * are numbers, as Double. This recursively walks a map/list and converts Doubles to Integer or Long, if
+ * possible.
+ */
+public class MapDoubleAdapterFactory implements TypeAdapterFactory {
+
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        if (!isMapType(type) && !isListType(type)) {
+            return null;
+        }
+
+        TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+
+        return new MapAdapter<>(delegate);
+    }
+
+    private <T> boolean isMapType(TypeToken<T> type) {
+        if (!Map.class.isAssignableFrom(type.getRawType())) {
+            return false;
+        }
+
+        // only supports Map<String,Object>
+
+        if (!(type.getType() instanceof ParameterizedType)) {
+            // untyped - assume the parameters are the correct type
+            return true;
+        }
+
+        Type[] actualParams = ((ParameterizedType) type.getType()).getActualTypeArguments();
+
+        return (actualParams[0] == String.class && actualParams[1] == Object.class);
+    }
+
+    private <T> boolean isListType(TypeToken<T> type) {
+        if (!List.class.isAssignableFrom(type.getRawType())) {
+            return false;
+        }
+
+        // only supports List<Object>
+
+        if (!(type.getType() instanceof ParameterizedType)) {
+            // untyped - assume the parameters are the correct type
+            return true;
+        }
+
+        Type[] actualParams = ((ParameterizedType) type.getType()).getActualTypeArguments();
+
+        return (actualParams[0] == Object.class);
+    }
+
+    /**
+     * Type adapter that performs conversion from Double to Integer/Long.
+     *
+     * @param <T> type of object on which this works (always Map.class)
+     */
+    private static class MapAdapter<T> extends TypeAdapter<T> {
+
+        /**
+         * Used to perform conversion between JSON and Map&lt;String,Object&gt;.
+         */
+        private final TypeAdapter<T> delegate;
+
+        /**
+         * Constructs the object.
+         *
+         * @param delegate JSON/Map converter
+         */
+        public MapAdapter(TypeAdapter<T> delegate) {
+            this.delegate = delegate;
+        }
+
+        @Override
+        public void write(JsonWriter out, T value) throws IOException {
+            delegate.write(out, value);
+        }
+
+        @Override
+        public T read(JsonReader in) throws IOException {
+            var value = delegate.read(in);
+
+            DoubleConverter.convertFromDouble(value);
+
+            return value;
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/OffsetDateTimeTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/OffsetDateTimeTypeAdapter.java
new file mode 100644 (file)
index 0000000..3f046b0
--- /dev/null
@@ -0,0 +1,35 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+
+public class OffsetDateTimeTypeAdapter extends StringTypeAdapter<OffsetDateTime> {
+
+    public OffsetDateTimeTypeAdapter() {
+        this(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+    }
+
+    public OffsetDateTimeTypeAdapter(DateTimeFormatter formatter) {
+        super("date", string -> OffsetDateTime.parse(string, formatter), value -> value.format(formatter));
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/OffsetTimeTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/OffsetTimeTypeAdapter.java
new file mode 100644 (file)
index 0000000..895b9de
--- /dev/null
@@ -0,0 +1,35 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import java.time.OffsetTime;
+import java.time.format.DateTimeFormatter;
+
+public class OffsetTimeTypeAdapter extends StringTypeAdapter<OffsetTime> {
+
+    public OffsetTimeTypeAdapter() {
+        this(DateTimeFormatter.ISO_OFFSET_TIME);
+    }
+
+    public OffsetTimeTypeAdapter(DateTimeFormatter formatter) {
+        super("time", string -> OffsetTime.parse(string, formatter), value -> value.format(formatter));
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/StringTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/StringTypeAdapter.java
new file mode 100644 (file)
index 0000000..2248169
--- /dev/null
@@ -0,0 +1,76 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.function.Function;
+
+/**
+ * GSON Type Adapter for fields that are encoded as Strings.
+ */
+public class StringTypeAdapter<T> extends TypeAdapter<T> {
+    private final String exMessage;
+    private final Function<String, T> deserializer;
+    private final Function<T, String> serializer;
+
+    /**
+     * Constructs an adapter.
+     *
+     * @param type type of value, used in exception messages
+     * @param deserializer function used to deserialize a String into a value
+     * @param serializer function used to serialize a value into a String
+     */
+    public StringTypeAdapter(String type, Function<String, T> deserializer, Function<T, String> serializer) {
+        this.exMessage = "invalid " + type;
+        this.deserializer = deserializer;
+        this.serializer = serializer;
+    }
+
+    @Override
+    public T read(JsonReader in) throws IOException {
+        try {
+            if (in.peek() == JsonToken.NULL) {
+                in.nextNull();
+                return null;
+            } else {
+                return deserializer.apply(in.nextString());
+            }
+
+        } catch (RuntimeException e) {
+            throw new JsonParseException(exMessage, e);
+        }
+    }
+
+    @Override
+    public void write(JsonWriter out, T value) throws IOException {
+        if (value == null) {
+            out.nullValue();
+        } else {
+            String text = serializer.apply(value);
+            out.value(text);
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapter.java
new file mode 100644 (file)
index 0000000..60758ff
--- /dev/null
@@ -0,0 +1,30 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import java.time.ZoneOffset;
+
+public class ZoneOffsetTypeAdapter extends StringTypeAdapter<ZoneOffset> {
+
+    public ZoneOffsetTypeAdapter() {
+        super("zone", ZoneOffset::of, ZoneOffset::toString);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapter.java
new file mode 100644 (file)
index 0000000..928fae9
--- /dev/null
@@ -0,0 +1,47 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020-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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * GSON Type Adapter for "ZonedDateTime" fields, that uses the standard
+ * ISO_ZONED_DATE_TIME formatter.
+ */
+public class ZonedDateTimeTypeAdapter extends StringTypeAdapter<ZonedDateTime> {
+
+    /**
+     * Constructs an adapter that uses the ISO_ZONED_DATE_TIME formatter.
+     */
+    public ZonedDateTimeTypeAdapter() {
+        this(DateTimeFormatter.ISO_ZONED_DATE_TIME);
+    }
+
+    /**
+     * Constructs an adapter that uses the specified formatter for reading and writing.
+     *
+     * @param formatter date-time formatter
+     */
+    public ZonedDateTimeTypeAdapter(DateTimeFormatter formatter) {
+        super("date", string -> ZonedDateTime.parse(string, formatter), value -> value.format(formatter));
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnyGetter.java b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnyGetter.java
new file mode 100644 (file)
index 0000000..859f538
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.annotation;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Mimics Jackson JsonAnyGetter annotation, but used by gson. This requires the gson
+ * object to be configured with the jackson default behaviors (i.e., the associated
+ * JacksonXxx strategy and adapters must be registered with the gson object).
+ */
+@Retention(RUNTIME)
+@Target(METHOD)
+public @interface GsonJsonAnyGetter {
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnySetter.java b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonAnySetter.java
new file mode 100644 (file)
index 0000000..87e0f33
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.annotation;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Mimics Jackson JsonAnySetter annotation, but used by gson. This requires the gson
+ * object to be configured with the jackson default behaviors (i.e., the associated
+ * JacksonXxx strategy and adapters must be registered with the gson object).
+ */
+@Retention(RUNTIME)
+@Target(METHOD)
+public @interface GsonJsonAnySetter {
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonIgnore.java b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonIgnore.java
new file mode 100644 (file)
index 0000000..cf2d439
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.annotation;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Mimics Jackson JsonIgnore annotation, but used by gson. This requires the gson object
+ * to be configured with the jackson default behaviors (i.e., the associated JacksonXxx
+ * strategy and adapters must be registered with the gson object).
+ */
+@Retention(RUNTIME)
+@Target({FIELD, METHOD})
+public @interface GsonJsonIgnore {
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonProperty.java b/policy-common/src/main/java/org/onap/policy/common/gson/annotation/GsonJsonProperty.java
new file mode 100644 (file)
index 0000000..c31c19b
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.annotation;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Mimics Jackson JsonProperty annotation, but used by gson. This requires the gson object
+ * to be configured with the jackson default behaviors (i.e., the associated JacksonXxx
+ * strategy and adapters must be registered with the gson object).
+ */
+@Retention(RUNTIME)
+@Target({FIELD, METHOD})
+public @interface GsonJsonProperty {
+
+    /**
+     * Property name of this item when placed into a JsonObject.
+     * @return the item's serialized name
+     */
+    String value() default "";
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/Adapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Adapter.java
new file mode 100644 (file)
index 0000000..af4a746
--- /dev/null
@@ -0,0 +1,360 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.TypeAdapter;
+import com.google.gson.reflect.TypeToken;
+import com.google.re2j.Pattern;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.function.Supplier;
+import lombok.Getter;
+import org.onap.policy.common.gson.annotation.GsonJsonProperty;
+
+/**
+ * Super class of adapters used to serialize and de-serialize an item.
+ */
+public class Adapter {
+
+    /**
+     * Pattern to match valid identifiers.
+     */
+    private static final Pattern VALID_NAME_PAT = Pattern.compile("[a-zA-Z_]\\w*");
+
+    /**
+     * Factory to access objects.  Overridden by junit tests.
+     */
+    private static Factory factory = new Factory();
+
+    /**
+     * Name of the property within the json structure containing the item.
+     */
+    @Getter
+    private final String propName;
+
+    /**
+     * Gson object that will provide the type converter.
+     */
+    private final Gson gson;
+
+    /**
+     * Converter used when reading.
+     */
+    private final ConvInfo reader;
+
+    /**
+     * Converter used when writing, allocated lazily, once an actual type is determined.
+     */
+    private ConvInfo writer = null;
+
+    /**
+     * Name of the item being lifted - used when throwing exceptions.
+     */
+    @Getter
+    private final String fullName;
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param field field used to access the item from within an object
+     */
+    public Adapter(Gson gson, Field field) {
+        this.propName = detmPropName(field);
+        this.reader = new ConvInfo(TypeToken.get(field.getGenericType()));
+        this.gson = gson;
+        this.fullName = getQualifiedName(field);
+
+        /*
+         * Turning off sonar, as this is required for emulation of "jackson".
+         */
+        field.setAccessible(true);  // NOSONAR
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param accessor method used to access the item from within an object
+     * @param valueType the class of value on which this operates
+     */
+    public Adapter(Gson gson, Method accessor, Type valueType) {
+        boolean forSetter = (accessor.getReturnType() == void.class);
+        this.propName = (forSetter ? detmSetterPropName(accessor) : detmGetterPropName(accessor));
+        this.reader = new ConvInfo(TypeToken.get(valueType));
+        this.gson = gson;
+        this.fullName = getQualifiedName(accessor);
+
+        /*
+         * Turning off sonar, as this is required for emulation of "jackson".
+         */
+        accessor.setAccessible(true); // NOSONAR
+    }
+
+    /**
+     * Converts an object to a json tree.
+     *
+     * @param object the object to be converted
+     * @return a json tree representing the object
+     */
+    @SuppressWarnings("unchecked")
+    public JsonElement toJsonTree(Object object) {
+        // always use a converter for the specific subclass
+        Class<? extends Object> clazz = object.getClass();
+
+        if (writer == null) {
+            // race condition here, but it's ok to overwrite a previous value
+            writer = new ConvInfo(TypeToken.get(clazz));
+        }
+
+        ConvInfo wtr = writer;
+
+        TypeAdapter<Object> conv =
+                        (wtr.clazz == clazz ? wtr.getConverter() : (TypeAdapter<Object>) gson.getAdapter(clazz));
+
+        return conv.toJsonTree(object);
+    }
+
+    /**
+     * Converts a json tree to an object.
+     *
+     * @param tree the tree to be converted
+     * @return the object represented by the tree
+     */
+    public Object fromJsonTree(JsonElement tree) {
+        return reader.getConverter().fromJsonTree(tree);
+    }
+
+    /**
+     * Makes an error message, appending the item's full name to the message prefix.
+     *
+     * @param prefix the message prefix
+     * @return the error message
+     */
+    public String makeError(String prefix) {
+        return (prefix + fullName);
+    }
+
+    /**
+     * Determines if the field is managed by the walker.
+     *
+     * @param field the field to examine
+     * @return {@code true} if the field is managed by the walker, {@code false} otherwise
+     */
+    public static boolean isManaged(Field field) {
+        return VALID_NAME_PAT.matcher(factory.getName(field)).matches();
+    }
+
+    /**
+     * Determines if the method is managed by the walker.
+     *
+     * @param method the method to examine
+     * @return {@code true} if the method is managed by the walker, {@code false}
+     *         otherwise
+     */
+    public static boolean isManaged(Method method) {
+        return VALID_NAME_PAT.matcher(factory.getName(method)).matches();
+    }
+
+    /**
+     * Determines the property name of an item within the json structure.
+     *
+     * @param field the item within the object
+     * @return the json property name for the item or {@code null} if the name is invalid
+     */
+    public static String detmPropName(Field field) {
+        // use the serialized name, if specified
+        GsonJsonProperty prop = field.getAnnotation(GsonJsonProperty.class);
+        if (prop != null && !prop.value().isEmpty()) {
+            return prop.value();
+        }
+
+        // no name provided - use it as is
+        return (isManaged(field) ? factory.getName(field) : null);
+    }
+
+    /**
+     * Determines the property name of an item, within the json structure, associated with
+     * a "get" method.
+     *
+     * @param method method to be invoked to get the item within the object
+     * @return the json property name for the item, or {@code null} if the method name is
+     *         not valid
+     */
+    public static String detmGetterPropName(Method method) {
+
+        return detmPropNameCommon(method, () -> {
+
+            if (!isManaged(method)) {
+                return null;
+            }
+
+            String name = factory.getName(method);
+
+            if (name.startsWith("get")) {
+                return name.substring(3);
+
+            } else if (name.startsWith("is")) {
+                Class<?> treturn = method.getReturnType();
+
+                if (treturn == boolean.class || treturn == Boolean.class) {
+                    return name.substring(2);
+                }
+            }
+
+            // not a valid name for a "getter" method
+            return null;
+        });
+    }
+
+    /**
+     * Determines the property name of an item, within the json structure, associated with
+     * a "set" method.
+     *
+     * @param method method to be invoked to set the item within the object
+     * @return the json property name for the item, or {@code null} if the method name is
+     *         not valid
+     */
+    public static String detmSetterPropName(Method method) {
+
+        return detmPropNameCommon(method, () -> {
+
+            if (!isManaged(method)) {
+                return null;
+            }
+
+            String name = factory.getName(method);
+
+            if (name.startsWith("set")) {
+                return name.substring(3);
+            }
+
+            // not a valid name for a "setter" method
+            return null;
+        });
+    }
+
+    /**
+     * Determines the property name of an item within the json structure.
+     *
+     * @param method method to be invoked to get/set the item within the object
+     * @param extractor function to extract the name directly from the method name
+     * @return the json property name for the item, or {@code null} if the method name is
+     *         not valid
+     */
+    private static String detmPropNameCommon(Method method, Supplier<String> extractor) {
+
+        // use the property name, if specified
+        GsonJsonProperty propName = method.getAnnotation(GsonJsonProperty.class);
+        if (propName != null && !propName.value().isEmpty()) {
+            return propName.value();
+        }
+
+        // no name provided - must compute it from the method name
+        String name = extractor.get();
+
+        if (name == null || name.isEmpty()) {
+            // nothing left after stripping the prefix - invalid name
+            return null;
+        }
+
+        // translate the first letter to lower-case
+        return name.substring(0, 1).toLowerCase() + name.substring(1);
+    }
+
+    /**
+     * Gets the fully qualified name of a field.
+     *
+     * @param field field whose name is desired
+     * @return the field fully qualified name
+     */
+    public static String getQualifiedName(Field field) {
+        return (field.getDeclaringClass().getName() + "." + factory.getName(field));
+    }
+
+    /**
+     * Gets the fully qualified name of a method.
+     *
+     * @param method method whose name is desired
+     * @return the method's fully qualified name
+     */
+    public static String getQualifiedName(Method method) {
+        return (method.getDeclaringClass().getName() + "." + factory.getName(method));
+    }
+
+    /**
+     * Converter info.
+     */
+    private class ConvInfo {
+
+        /**
+         * Type on which the converter works.
+         */
+        private TypeToken<?> type;
+
+        /**
+         * Class of object on which the converter works.
+         */
+        private Class<?> clazz;
+
+        /**
+         * Converter to use, initialized lazily.
+         */
+        private TypeAdapter<Object> conv = null;
+
+        /**
+         * Constructs the object.
+         *
+         * @param type type of object to be converted
+         */
+        public ConvInfo(TypeToken<?> type) {
+            this.type = type;
+            this.clazz = type.getRawType();
+        }
+
+        @SuppressWarnings("unchecked")
+        public final TypeAdapter<Object> getConverter() {
+            if (conv == null) {
+                // race condition here, but it's ok to overwrite a previous value
+                this.conv = (TypeAdapter<Object>) gson.getAdapter(type);
+            }
+
+            return conv;
+        }
+    }
+
+    /**
+     * Factory used to access various objects.
+     */
+    public static class Factory {
+
+        public String getName(Field field) {
+            return field.getName();
+        }
+
+        public String getName(Method method) {
+            return method.getName();
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnyGetterSerializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnyGetterSerializer.java
new file mode 100644 (file)
index 0000000..4ad924a
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import java.lang.reflect.Method;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Serializer for methods having a JsonAnyGetter annotation.
+ */
+public class AnyGetterSerializer extends Lifter implements Serializer {
+
+    public static final String NOT_AN_OBJECT_ERR = "expecting a JsonObject for ";
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param unliftedProps property names that should not be lifted
+     * @param getter method used to get the item from within an object
+     */
+    public AnyGetterSerializer(Gson gson, Set<String> unliftedProps, Method getter) {
+        super(gson, unliftedProps, getter, getter.getGenericReturnType());
+    }
+
+    @Override
+    public void addToTree(Object source, JsonObject target) {
+        // get the value from the object
+        Object value = invoke(source);
+        if (value == null) {
+            // nothing to lift
+            return;
+        }
+
+        JsonElement inner = toJsonTree(value);
+        if (!inner.isJsonObject()) {
+            throw new JsonParseException(makeError(NOT_AN_OBJECT_ERR));
+        }
+
+        // lift items from inner into the target
+        copyLiftedItems(inner.getAsJsonObject(), target);
+    }
+
+    /**
+     * Copies lifted items from one tree into another, without removing them from the
+     * source tree.
+     *
+     * @param source tree from which items are to be copied
+     * @param target tree into which items are to be copied
+     */
+    private void copyLiftedItems(JsonObject source, JsonObject target) {
+        for (Entry<String, JsonElement> ent : source.entrySet()) {
+            String name = ent.getKey();
+            if (shouldLift(name)) {
+                target.add(name, ent.getValue());
+            }
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnySetterDeserializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/AnySetterDeserializer.java
new file mode 100644 (file)
index 0000000..411d30c
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.lang.reflect.Method;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * De-serializer for methods having a JsonAnySetter annotation.
+ */
+public class AnySetterDeserializer extends Lifter implements Deserializer {
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param unliftedProps property names that should not be lifted
+     * @param setter method used to set the item within an object
+     */
+    public AnySetterDeserializer(Gson gson, Set<String> unliftedProps, Method setter) {
+        super(gson, unliftedProps, setter, setter.getGenericParameterTypes()[1]);
+    }
+
+    @Override
+    public void getFromTree(JsonObject source, Object target) {
+        for (Entry<String, JsonElement> ent : source.entrySet()) {
+            String name = ent.getKey();
+
+            if (shouldLift(name)) {
+                Object value = fromJsonTree(ent.getValue());
+                invoke(target, name, value);
+            }
+        }
+    }
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/ClassWalker.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/ClassWalker.java
new file mode 100644 (file)
index 0000000..954d3f4
--- /dev/null
@@ -0,0 +1,389 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.JsonParseException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import lombok.Getter;
+import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter;
+import org.onap.policy.common.gson.annotation.GsonJsonAnySetter;
+import org.onap.policy.common.gson.annotation.GsonJsonIgnore;
+import org.onap.policy.common.gson.annotation.GsonJsonProperty;
+
+/**
+ * Data populated while walking the hierarchy of a class.
+ */
+public class ClassWalker {
+
+    public static final String ANY_GETTER_MISMATCH_ERR =
+                    GsonJsonAnyGetter.class.getSimpleName() + " parameter mismatch for: ";
+
+    public static final String ANY_SETTER_MISMATCH_ERR =
+                    GsonJsonAnySetter.class.getSimpleName() + " parameter mismatch for: ";
+
+    public static final String ANY_SETTER_TYPE_ERR =
+                    GsonJsonAnySetter.class.getSimpleName() + " first parameter must be a string: ";
+
+    /**
+     * Maps an input property name to an item within the class, where item is one of:
+     * {@link Field}, {@link Method}, or {@code null}. Entries are overwritten as new
+     * items are added.
+     */
+    private final Map<String, Object> inProps = new HashMap<>();
+
+    /**
+     * Maps an output property name to an item within the class, where item is one of:
+     * {@link Field}, {@link Method}, or {@code null}. Entries are overwritten as new
+     * items are added.
+     */
+    private final Map<String, Object> outProps = new HashMap<>();
+
+    /**
+     * Maps a method name to a "get" method. Used when overriding properties associated
+     * with a method.
+     */
+    private final Map<String, Method> getters = new HashMap<>();
+
+    /**
+     * Maps a method name to a "set" method. Used when overriding properties associated
+     * with a method.
+     */
+    private final Map<String, Method> setters = new HashMap<>();
+
+    /**
+     * Method having {@link GsonJsonAnyGetter} annotation. Overwritten as new "any-getters"
+     * are identified.
+     */
+    @Getter
+    private Method anyGetter = null;
+
+    /**
+     * Method having {@link GsonJsonAnySetter} annotation. Overwritten as new "any-setters"
+     * are identified.
+     */
+    @Getter
+    private Method anySetter = null;
+
+    /**
+     * Gets the names of input properties that are not being ignored.
+     *
+     * @return the non-ignored input property names
+     */
+    public List<String> getInNotIgnored() {
+        return getNonNull(inProps);
+    }
+
+    /**
+     * Gets the names of output properties that are not being ignored.
+     *
+     * @return the non-ignored output property names
+     */
+    public List<String> getOutNotIgnored() {
+        return getNonNull(outProps);
+    }
+
+    /**
+     * Gets the property names, associated with a non-null value, from a set of
+     * properties.
+     *
+     * @param props set of properties from which to extract the names
+     * @return the property names having a non-null value
+     */
+    private List<String> getNonNull(Map<String, Object> props) {
+        List<String> lst = new ArrayList<>(props.size());
+
+        for (Entry<String, Object> ent : props.entrySet()) {
+            if (ent.getValue() != null) {
+                lst.add(ent.getKey());
+            }
+        }
+
+        return lst;
+    }
+
+    /**
+     * Gets the input properties whose values are of the given class.
+     *
+     * @param clazz class of properties to get
+     * @return the input properties of the given class
+     */
+    public <T> List<T> getInProps(Class<T> clazz) {
+        return getProps(clazz, inProps.values());
+    }
+
+    /**
+     * Gets the output properties whose values are of the given class.
+     *
+     * @param clazz class of properties to get
+     * @return the output properties of the given class
+     */
+    public <T> List<T> getOutProps(Class<T> clazz) {
+        return getProps(clazz, outProps.values());
+    }
+
+    /**
+     * Gets the properties whose values are of the given class.
+     *
+     * @param clazz class of properties to get
+     * @param values values from which to select
+     * @return the output properties of the given class
+     */
+    @SuppressWarnings("unchecked")
+    private <T> List<T> getProps(Class<T> clazz, Collection<Object> values) {
+        List<T> lst = new ArrayList<>(values.size());
+
+        for (Object val : values) {
+            if (val != null && val.getClass() == clazz) {
+                lst.add((T) val);
+            }
+        }
+
+        return lst;
+    }
+
+    /**
+     * Recursively walks a class hierarchy, including super classes and interfaces,
+     * examining each class for various annotations.
+     *
+     * @param clazz class whose hierarchy is to be walked
+     */
+    public void walkClassHierarchy(Class<?> clazz) {
+        if (clazz == Object.class) {
+            return;
+        }
+
+        // walk interfaces first
+        for (Class<?> intfc : clazz.getInterfaces()) {
+            walkClassHierarchy(intfc);
+        }
+
+        // walk superclass next, overwriting previous items
+        Class<?> sup = clazz.getSuperclass();
+        if (sup != null) {
+            walkClassHierarchy(sup);
+        }
+
+        // finally, examine this class, overwriting previous items
+        examine(clazz);
+    }
+
+    /**
+     * Examines a class for annotations, examining fields and then methods.
+     *
+     * @param clazz class to be examined
+     */
+    protected void examine(Class<?> clazz) {
+        for (Field field : clazz.getDeclaredFields()) {
+            examine(field);
+        }
+
+        for (Method method : clazz.getDeclaredMethods()) {
+            examine(method);
+        }
+    }
+
+    /**
+     * Examines a field for annotations.
+     *
+     * @param field field to be examined
+     */
+    protected void examine(Field field) {
+        if (field.isSynthetic()) {
+            return;
+        }
+
+        int mod = field.getModifiers();
+
+        if (Modifier.isStatic(mod)) {
+            // skip static fields
+            return;
+        }
+
+        if (!Modifier.isPublic(mod) && field.getAnnotation(GsonJsonProperty.class) == null) {
+            // private/protected - skip it unless explicitly exposed
+            return;
+        }
+
+        if (Modifier.isTransient(mod) && field.getAnnotation(GsonJsonProperty.class) == null) {
+            // transient - skip it unless explicitly exposed
+            return;
+        }
+
+        String name = detmPropName(field);
+        if (name == null) {
+            // invalid name
+            return;
+        }
+
+        // if ignoring, then insert null into the map, otherwise insert the field
+        Field annotField = (field.getAnnotation(GsonJsonIgnore.class) != null ? null : field);
+
+        // a field can be both an input and an output
+
+        inProps.put(name, annotField);
+        outProps.put(name, annotField);
+    }
+
+    /**
+     * Examines a method for annotations.
+     *
+     * @param method method to be examined
+     */
+    protected void examine(Method method) {
+        if (method.isSynthetic()) {
+            return;
+        }
+
+        int mod = method.getModifiers();
+
+        if (Modifier.isStatic(mod)) {
+            // static methods are not exposed
+            return;
+        }
+
+        GsonJsonProperty prop = method.getAnnotation(GsonJsonProperty.class);
+        GsonJsonAnyGetter get = method.getAnnotation(GsonJsonAnyGetter.class);
+        GsonJsonAnySetter set = method.getAnnotation(GsonJsonAnySetter.class);
+
+        if (!Modifier.isPublic(mod) && prop == null && get == null && set == null) {
+            // private/protected methods are not exposed, unless annotated
+            return;
+        }
+
+
+        if (method.getReturnType() == void.class) {
+            // "void" return type - must be a "setter" method
+            if (set == null) {
+                examineSetter(method);
+
+            } else {
+                examineAnySetter(method);
+            }
+
+        } else {
+            // must be a "getter" method
+            if (get == null) {
+                examineGetter(method);
+
+            } else {
+                examineAnyGetter(method);
+            }
+        }
+    }
+
+    /**
+     * Examines a "setter" method.
+     *
+     * @param method method to be examined
+     */
+    private void examineSetter(Method method) {
+        String name = Adapter.detmSetterPropName(method);
+        if (name != null && method.getParameterCount() == 1) {
+            // remove old name mapping, if any
+            Method old = setters.get(method.getName());
+            if (old != null) {
+                inProps.remove(Adapter.detmSetterPropName(old));
+            }
+
+            setters.put(method.getName(), method);
+
+            // if ignoring, then insert null into the map, otherwise insert the method
+            inProps.put(name, (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method));
+        }
+    }
+
+    /**
+     * Examines a "getter" method.
+     *
+     * @param method method to be examined
+     */
+    private void examineGetter(Method method) {
+        String name = Adapter.detmGetterPropName(method);
+        if (name != null && method.getParameterCount() == 0) {
+            // remove old name mapping, if any
+            Method old = getters.get(method.getName());
+            if (old != null) {
+                outProps.remove(Adapter.detmGetterPropName(old));
+            }
+
+            getters.put(method.getName(), method);
+
+            // if ignoring, then insert null into the map, otherwise insert the method
+            outProps.put(name, (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method));
+        }
+    }
+
+    /**
+     * Examines a method having a {@link GsonJsonAnySetter} annotation.
+     *
+     * @param method method to be examined
+     */
+    private void examineAnySetter(Method method) {
+        if (method.getParameterCount() != 2) {
+            throw new JsonParseException(ANY_SETTER_MISMATCH_ERR + getFqdn(method));
+        }
+
+        if (method.getParameterTypes()[0] != String.class) {
+            throw new JsonParseException(ANY_SETTER_TYPE_ERR + getFqdn(method));
+        }
+
+        // if ignoring, then use null, otherwise use the method
+        anySetter = (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method);
+    }
+
+    /**
+     * Examines a method having a {@link GsonJsonAnyGetter} annotation.
+     *
+     * @param method method to be examined
+     */
+    private void examineAnyGetter(Method method) {
+        if (method.getParameterCount() != 0) {
+            throw new JsonParseException(ANY_GETTER_MISMATCH_ERR + getFqdn(method));
+        }
+
+        // if ignoring, then use null, otherwise use the method
+        anyGetter = (method.getAnnotation(GsonJsonIgnore.class) != null ? null : method);
+    }
+
+    /**
+     * Gets the fully qualified name of a method.
+     *
+     * @param method method whose name is desired
+     * @return the fully qualified method name
+     */
+    private String getFqdn(Method method) {
+        return (method.getDeclaringClass().getName() + "." + method.getName());
+    }
+
+    // these may be overridden by junit tests
+
+    protected String detmPropName(Field field) {
+        return Adapter.detmPropName(field);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/Deserializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Deserializer.java
new file mode 100644 (file)
index 0000000..4bf6e0c
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Super class of all de-serializers.
+ */
+@FunctionalInterface
+public interface Deserializer {
+
+    /**
+     * Gets an value from a tree, converts it, and puts it into a target object.
+     *
+     * @param source tree from which to get the value
+     * @param target where to place the converted value
+     */
+    void getFromTree(JsonObject source, Object target);
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldDeserializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldDeserializer.java
new file mode 100644 (file)
index 0000000..123b019
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019-2020 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import java.lang.reflect.Field;
+
+/**
+ * De-serializer for fields that are exposed.
+ */
+public class FieldDeserializer extends Adapter implements Deserializer {
+
+    public static final String SET_ERR = "cannot set field: ";
+
+    /**
+     * Field within the object.
+     */
+    private final Field field;
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param field field within the object
+     */
+    public FieldDeserializer(Gson gson, Field field) {
+        super(gson, field);
+
+        this.field = field;
+
+        /*
+         * Turning off sonar, as this is required for emulation of "jackson".
+         */
+        field.setAccessible(true);  // NOSONAR
+    }
+
+    @Override
+    public void getFromTree(JsonObject source, Object target) {
+        JsonElement jsonEl = source.get(getPropName());
+        if (jsonEl == null || jsonEl.isJsonNull()) {
+            return;
+        }
+
+        Object value = fromJsonTree(jsonEl);
+
+        try {
+            /*
+             * Turning off sonar, as this is required for emulation of "jackson".
+             */
+            field.set(target, value);   // NOSONAR
+
+        } catch (IllegalArgumentException | IllegalAccessException e) {
+            throw new JsonParseException(makeError(SET_ERR), e);
+        }
+    }
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldSerializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/FieldSerializer.java
new file mode 100644 (file)
index 0000000..348ef5a
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019-2020 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import java.lang.reflect.Field;
+
+/**
+ * Serializer for fields that are exposed.
+ */
+public class FieldSerializer extends Adapter implements Serializer {
+
+    public static final String GET_ERR = "cannot get field: ";
+
+    /**
+     * Field within the object.
+     */
+    private final Field field;
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param field field within the object
+     */
+    public FieldSerializer(Gson gson, Field field) {
+        super(gson, field);
+
+        this.field = field;
+
+        /*
+         * Turning off sonar, as this is required for emulation of "jackson".
+         */
+        field.setAccessible(true);  // NOSONAR
+    }
+
+    @Override
+    public void addToTree(Object source, JsonObject target) {
+        Object value;
+        try {
+            value = getFromObject(source);
+
+        } catch (IllegalArgumentException | IllegalAccessException e) {
+            throw new JsonParseException(makeError(GET_ERR), e);
+        }
+
+        JsonElement jsonEl = (value == null ? JsonNull.INSTANCE : toJsonTree(value));
+        target.add(getPropName(), jsonEl);
+    }
+
+    /**
+     * Gets the field from the source object. Overridden during junit testing.
+     *
+     * @param source object containing the field
+     * @return the field's value
+     * @throws IllegalAccessException if an error occurs
+     */
+    protected Object getFromObject(Object source) throws IllegalAccessException {
+        return field.get(source);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/JacksonTypeAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/JacksonTypeAdapter.java
new file mode 100644 (file)
index 0000000..34d61f4
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.List;
+
+
+/**
+ * Adapter for a single class that implements a jackson-style behavior.
+ *
+ * @param <T> type of class on which the adapter works
+ */
+public class JacksonTypeAdapter<T> extends TypeAdapter<T> {
+
+    /**
+     * Used to create an object of the given class.
+     */
+    private final TypeAdapter<T> delegate;
+
+    /**
+     * Used to serialize/deserialize a JsonElement.
+     */
+    private final TypeAdapter<JsonElement> elementAdapter;
+
+    /**
+     * Serializers for each item within the object.
+     */
+    private final Serializer[] serializers;
+
+    /**
+     * Deserializers for each item within the object.
+     */
+    private final Deserializer[] deserializers;
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson the associated gson object
+     * @param delegate default constructor for the type
+     * @param serializers the serializers to use to serialize items within the object
+     * @param deserializers the deserializers to use to deserialize items into the object
+     */
+    public JacksonTypeAdapter(Gson gson, TypeAdapter<T> delegate, List<Serializer> serializers,
+                    List<Deserializer> deserializers) {
+        this.delegate = delegate;
+        this.elementAdapter = gson.getAdapter(JsonElement.class);
+        this.serializers = serializers.toArray(new Serializer[0]);
+        this.deserializers = deserializers.toArray(new Deserializer[0]);
+    }
+
+    @Override
+    public void write(JsonWriter out, T value) throws IOException {
+        JsonElement tree = delegate.toJsonTree(value);
+
+        if (tree.isJsonObject()) {
+            var jsonObj = tree.getAsJsonObject();
+
+            // serialize each item from the value into the target tree
+            for (Serializer serializer : serializers) {
+                serializer.addToTree(value, jsonObj);
+            }
+        }
+
+        elementAdapter.write(out, tree);
+    }
+
+    @Override
+    public T read(JsonReader in) throws IOException {
+        JsonElement tree = elementAdapter.read(in);
+        var object = delegate.fromJsonTree(tree);
+
+        if (tree.isJsonObject()) {
+            var jsonObj = tree.getAsJsonObject();
+
+            // deserialize each item from the tree into the target object
+            for (Deserializer dser : deserializers) {
+                dser.getFromTree(jsonObj, object);
+            }
+        }
+
+        return object;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/Lifter.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Lifter.java
new file mode 100644 (file)
index 0000000..bb8ec32
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Set;
+
+/**
+ * Super class of serializers and de-serializers that deal with "lifted" data, that is,
+ * data that is lifted from a nested json object into the containing object.
+ */
+public class Lifter extends MethodAdapter {
+
+    /**
+     * Names of the properties that are <i>not</i> to be lifted.
+     */
+    private final Set<String> unliftedProps;
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param unliftedProps property names that should not be lifted
+     * @param accessor method used to access the item from within an object
+     * @param type the class of value on which this operates
+     */
+    public Lifter(Gson gson, Set<String> unliftedProps, Method accessor, Type type) {
+        super(gson, accessor, type);
+
+        this.unliftedProps = unliftedProps;
+    }
+
+    /**
+     * Determines if a property should be lifted.
+     *
+     * @param propName the name of the property
+     * @return {@code true} if the property should be lifted, {@code false} otherwise
+     */
+    public boolean shouldLift(String propName) {
+        return !unliftedProps.contains(propName);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodAdapter.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodAdapter.java
new file mode 100644 (file)
index 0000000..579864d
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParseException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+
+/**
+ * Super class of adapters used to serialize and de-serialize a method.
+ */
+public class MethodAdapter extends Adapter {
+
+    public static final String INVOKE_ERR = "cannot invoke method to serialize/deserialize: ";
+
+    /**
+     * Method used to access the item within an object.
+     */
+    private final Method accessor;
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param accessor method used to access the item from within an object
+     * @param type the class of value on which this operates
+     */
+    public MethodAdapter(Gson gson, Method accessor, Type type) {
+        super(gson, accessor, type);
+
+        this.accessor = accessor;
+    }
+
+    /**
+     * Invokes the accessor method.
+     *
+     * @param self object on which to invoke the method
+     * @param args arguments to be passed to the method
+     * @return the method's result
+     */
+    public Object invoke(Object self, Object... args) {
+        try {
+            return accessor.invoke(self, args);
+
+        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+            throw new JsonParseException(makeError(INVOKE_ERR), e);
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodDeserializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodDeserializer.java
new file mode 100644 (file)
index 0000000..bb0724e
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.lang.reflect.Method;
+
+/**
+ * De-serializer for methods that are exposed.
+ */
+public class MethodDeserializer extends MethodAdapter implements Deserializer {
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param setter method used to set the item within an object
+     */
+    public MethodDeserializer(Gson gson, Method setter) {
+        super(gson, setter, setter.getGenericParameterTypes()[0]);
+    }
+
+    @Override
+    public void getFromTree(JsonObject source, Object target) {
+        JsonElement jsonEl = source.get(getPropName());
+        if (jsonEl == null || jsonEl.isJsonNull()) {
+            return;
+        }
+
+        invoke(target, fromJsonTree(jsonEl));
+    }
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodSerializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/MethodSerializer.java
new file mode 100644 (file)
index 0000000..ab54461
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import java.lang.reflect.Method;
+
+/**
+ * Serializer for methods that are exposed.
+ */
+public class MethodSerializer extends MethodAdapter implements Serializer {
+
+    /**
+     * Constructs the object.
+     *
+     * @param gson Gson object providing type adapters
+     * @param getter method used to get the item from within an object
+     */
+    public MethodSerializer(Gson gson, Method getter) {
+        super(gson, getter, getter.getGenericReturnType());
+    }
+
+    @Override
+    public void addToTree(Object source, JsonObject target) {
+        Object value = invoke(source);
+        target.add(getPropName(), (value == null ? null : toJsonTree(value)));
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/gson/internal/Serializer.java b/policy-common/src/main/java/org/onap/policy/common/gson/internal/Serializer.java
new file mode 100644 (file)
index 0000000..254a5fe
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Super class of all serializers.
+ */
+@FunctionalInterface
+public interface Serializer {
+
+    /**
+     * Converts an object and then adds it to a tree.
+     *
+     * @param source object to be converted
+     * @param target tree into which to place the converted object
+     */
+    void addToTree(Object source, JsonObject target);
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/event/Topic.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/Topic.java
new file mode 100644 (file)
index 0000000..48dbb71
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpoint.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpoint.java
new file mode 100644 (file)
index 0000000..5511a82
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointManager.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointManager.java
new file mode 100644 (file)
index 0000000..40b9c23
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointProxy.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicEndpointProxy.java
new file mode 100644 (file)
index 0000000..9dbf541
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicListener.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicListener.java
new file mode 100644 (file)
index 0000000..f989624
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicRegisterable.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicRegisterable.java
new file mode 100644 (file)
index 0000000..1d6873e
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSink.java
new file mode 100644 (file)
index 0000000..d5269b9
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/TopicSource.java
new file mode 100644 (file)
index 0000000..f0dc3b7
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/ApiKeyEnabled.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/ApiKeyEnabled.java
new file mode 100644 (file)
index 0000000..360a88a
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusConsumer.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusConsumer.java
new file mode 100644 (file)
index 0000000..925949a
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusPublisher.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusPublisher.java
new file mode 100644 (file)
index 0000000..10c7db2
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicBase.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicBase.java
new file mode 100644 (file)
index 0000000..6516945
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSink.java
new file mode 100644 (file)
index 0000000..54b0861
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/BusTopicSource.java
new file mode 100644 (file)
index 0000000..974c02b
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSink.java
new file mode 100644 (file)
index 0000000..6a30f00
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSource.java
new file mode 100644 (file)
index 0000000..912b698
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBase.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBase.java
new file mode 100644 (file)
index 0000000..4d1fbc9
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseFactory.java
new file mode 100644 (file)
index 0000000..d98de65
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseHashedFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/base/TopicBaseHashedFactory.java
new file mode 100644 (file)
index 0000000..70ff04e
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClient.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClient.java
new file mode 100644 (file)
index 0000000..131bf2d
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientException.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientException.java
new file mode 100644 (file)
index 0000000..fad5e11
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSinkFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSinkFactory.java
new file mode 100644 (file)
index 0000000..0497f1f
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactory.java
new file mode 100644 (file)
index 0000000..1aac89c
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSink.java
new file mode 100644 (file)
index 0000000..4bdd2b0
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapper.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapper.java
new file mode 100644 (file)
index 0000000..86b9e93
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactories.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactories.java
new file mode 100644 (file)
index 0000000..c10285f
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSink.java
new file mode 100644 (file)
index 0000000..f784a98
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactory.java
new file mode 100644 (file)
index 0000000..8feecbe
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSource.java
new file mode 100644 (file)
index 0000000..bddced7
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactory.java
new file mode 100644 (file)
index 0000000..06f4412
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSource.java
new file mode 100644 (file)
index 0000000..5691cb1
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpoint.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpoint.java
new file mode 100644 (file)
index 0000000..e25aca5
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactories.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactories.java
new file mode 100644 (file)
index 0000000..506cc9f
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactory.java
new file mode 100644 (file)
index 0000000..e81dfae
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSink.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSink.java
new file mode 100644 (file)
index 0000000..b7d78ff
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactory.java
new file mode 100644 (file)
index 0000000..cdf4a17
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSource.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSource.java
new file mode 100644 (file)
index 0000000..6e7d021
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactory.java
new file mode 100644 (file)
index 0000000..5e3a365
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApi.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApi.java
new file mode 100644 (file)
index 0000000..6e3b830
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureProviders.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureProviders.java
new file mode 100644 (file)
index 0000000..4f57ab2
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheck.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheck.java
new file mode 100644 (file)
index 0000000..f371eda
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025 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.healthcheck;
+
+import java.util.List;
+
+public interface TopicHealthCheck {
+    boolean healthCheck(List<String> topics);
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactory.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactory.java
new file mode 100644 (file)
index 0000000..d779050
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025 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.healthcheck;
+
+import org.onap.policy.common.message.bus.event.Topic;
+import org.onap.policy.common.message.bus.healthcheck.kafka.KafkaHealthCheck;
+import org.onap.policy.common.message.bus.healthcheck.noop.NoopHealthCheck;
+import org.onap.policy.common.parameters.topic.TopicParameters;
+
+public class TopicHealthCheckFactory {
+
+    /**
+     * Get Topic HealthCheck.
+     *
+     * @param param TopicParameters
+     * @return TopicHealthCheck
+     */
+    public TopicHealthCheck getTopicHealthCheck(TopicParameters param) {
+        return switch (Topic.CommInfrastructure.valueOf(param.getTopicCommInfrastructure().toUpperCase())) {
+            case KAFKA -> new KafkaHealthCheck(param);
+            case NOOP ->  new NoopHealthCheck();
+            default -> null;
+        };
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheck.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheck.java
new file mode 100644 (file)
index 0000000..ef8a0f7
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025 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.healthcheck.kafka;
+
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+import org.apache.kafka.clients.admin.AdminClient;
+import org.apache.kafka.clients.admin.AdminClientConfig;
+import org.apache.kafka.common.KafkaException;
+import org.onap.policy.common.message.bus.healthcheck.TopicHealthCheck;
+import org.onap.policy.common.parameters.topic.TopicParameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class KafkaHealthCheck implements TopicHealthCheck {
+
+    private static final Logger logger = LoggerFactory.getLogger(KafkaHealthCheck.class);
+    private final TopicParameters parameters;
+
+    public KafkaHealthCheck(TopicParameters parameters) {
+        this.parameters = parameters;
+    }
+
+    /**
+     * Check that Kafka is OnLine and topics are available.
+     *
+     * @return true if Kafka is OnLine
+     */
+    public boolean healthCheck(List<String> topics) {
+        if (parameters.getServers() == null || parameters.getServers().isEmpty()) {
+            logger.warn("Kafka Address not defined!");
+            return true;
+        }
+        try (var client = createAdminClient()) {
+            if (!checkConnection(client)) {
+                logger.warn("Kafka not UP yet!");
+                return false;
+            }
+            if (topics.isEmpty()) {
+                logger.warn("Kafka is UP");
+                return true;
+            }
+
+            return checkTopics(client, topics);
+        } catch (KafkaException | ExecutionException e) {
+            logger.error(e.getMessage());
+            return false;
+        } catch (InterruptedException e) {
+            logger.error(e.getMessage());
+            Thread.currentThread().interrupt();
+            return false;
+        }
+    }
+
+    private boolean checkConnection(AdminClient client) throws ExecutionException, InterruptedException {
+        var nodes = client.describeCluster().nodes().get();
+        if (nodes == null || nodes.isEmpty()) {
+            return false;
+        }
+        nodes.forEach(node -> logger.debug("nodeId {}", node.id()));
+        return true;
+    }
+
+    private boolean checkTopics(AdminClient client, List<String> topics)
+            throws ExecutionException, InterruptedException {
+        var listTopics = client.listTopics().names().get();
+        if (listTopics == null || listTopics.isEmpty()) {
+            logger.warn("Kafka topics not available!");
+            return false;
+        }
+        var setTopics = listTopics.stream().map(String::toLowerCase).collect(Collectors.toSet());
+        for (var topic : topics) {
+            if (!setTopics.contains(topic.toLowerCase())) {
+                logger.warn("Kafka topic {} not available!", topic);
+                return false;
+            }
+        }
+        logger.info("Kafka is UP and topics available!");
+        return true;
+    }
+
+    protected AdminClient createAdminClient() {
+        var kafkaProps = new Properties();
+        kafkaProps.setProperty(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, parameters.getServers().get(0));
+
+        if (parameters.isAdditionalPropsValid()) {
+            kafkaProps.putAll(parameters.getAdditionalProps());
+        }
+        return AdminClient.create(kafkaProps);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheck.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheck.java
new file mode 100644 (file)
index 0000000..c0006ab
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025 OpenInfra Foundation Europe. 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.message.bus.healthcheck.noop;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.onap.policy.common.message.bus.event.Topic;
+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.healthcheck.TopicHealthCheck;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NoopHealthCheck implements TopicHealthCheck {
+
+    private final Logger logger = LoggerFactory.getLogger(NoopHealthCheck.class);
+
+    private final TopicEndpoint topicEndpoint = TopicEndpointManager.getManager();
+
+    private Map<String, Topic> actualTopics;
+
+    @Override
+    public boolean healthCheck(List<String> topics) {
+        var topicsHealthy = true;
+
+        this.populateActualTopics();
+
+        if (topicEndpoint.isAlive()) {
+            for (String topic : topics) {
+                var actualTopic = actualTopics.get(topic.toLowerCase());
+                if (!actualTopic.isAlive()) {
+                    logger.warn("Topic {} is not alive!", topic);
+                    topicsHealthy = false;
+                    break;
+                }
+            }
+        } else {
+            logger.warn("Topic Endpoint is not alive!");
+            return false;
+        }
+
+        return topicsHealthy;
+    }
+
+    private void populateActualTopics() {
+        actualTopics = new HashMap<>();
+        topicEndpoint.getNoopTopicSinks().forEach(sink -> actualTopics.put(sink.getTopic(), sink));
+        topicEndpoint.getNoopTopicSources().forEach(source -> actualTopics.put(source.getTopic(), source));
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/message/bus/properties/MessageBusProperties.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/properties/MessageBusProperties.java
new file mode 100644 (file)
index 0000000..9aa529f
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtils.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtils.java
new file mode 100644 (file)
index 0000000..cfe6220
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/NetLoggerUtil.java b/policy-common/src/main/java/org/onap/policy/common/message/bus/utils/NetLoggerUtil.java
new file mode 100644 (file)
index 0000000..b5454e5
--- /dev/null
@@ -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/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java b/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidationResult.java
new file mode 100644 (file)
index 0000000..e162053
--- /dev/null
@@ -0,0 +1,210 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  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.parameters;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/**
+ * This class holds the result of the validation of an arbitrary bean.
+ */
+public class BeanValidationResult extends ValidationResultImpl {
+
+    /**
+     * Validation results for each item in the bean.
+     */
+    private final List<ValidationResult> itemResults = new ArrayList<>();
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param name name of the bean being validated
+     * @param object object being validated
+     */
+    public BeanValidationResult(String name, Object object) {
+        super(name, object);
+    }
+
+    /**
+     * Adds a result to this result.
+     *
+     * @param result the result to be added
+     * @return {@code true} if the result is {@code null} or valid, {@code false} if the
+     *         result is invalid
+     */
+    public boolean addResult(ValidationResult result) {
+        if (result == null) {
+            return true;
+        }
+
+        itemResults.add(result);
+        setResult(result.getStatus());
+
+        return result.isValid();
+    }
+
+    /**
+     * Adds a result to this result.
+     * @param name name of the object of this result
+     * @param object object being validated
+     * @param status status of the new result
+     * @param message new result message
+     * @return {@code true} if the status is {@code null} or valid, {@code false} if the
+     *         status is invalid
+     */
+    public boolean addResult(String name, Object object, ValidationStatus status, String message) {
+        return addResult(new ObjectValidationResult(name, object, status, message));
+    }
+
+    /**
+     * Validates that a sub-object within the bean is not {@code null}.
+     *
+     * @param subName name of the sub-object
+     * @param subObject the sub-object
+     * @return {@code true} if the value is not null, {@code false} otherwise
+     */
+    public boolean validateNotNull(String subName, Object subObject) {
+        var result = new ObjectValidationResult(subName, subObject);
+
+        if (result.validateNotNull()) {
+            return true;
+
+        } else {
+            addResult(result);
+            return false;
+        }
+    }
+
+    /**
+     * Validates the items in a list, after validating that the list, itself, is not null.
+     *
+     * @param listName name of the list
+     * @param list list whose items are to be validated, or {@code null}
+     * @param itemValidator function to validate an item in the list
+     * @return {@code true} if all items in the list are valid, {@code false} otherwise
+     */
+    public <T> boolean validateNotNullList(String listName, Collection<T> list,
+                    Function<T, ValidationResult> itemValidator) {
+
+        return validateNotNull(listName, list) && validateList(listName, list, itemValidator);
+    }
+
+    /**
+     * Validates the items in a list.
+     *
+     * @param listName name of the list
+     * @param list list whose items are to be validated, or {@code null}
+     * @param itemValidator function to validate an item in the list
+     * @return {@code true} if all items in the list are valid, {@code false} otherwise
+     */
+    public <T> boolean validateList(String listName, Collection<T> list, Function<T, ValidationResult> itemValidator) {
+        if (list == null) {
+            return true;
+        }
+
+        var result = new BeanValidationResult(listName, null);
+        for (T item : list) {
+            if (item == null) {
+                result.addResult("item", item, ValidationStatus.INVALID, "null");
+            } else {
+                result.addResult(itemValidator.apply(item));
+            }
+        }
+
+        if (result.isValid()) {
+            return true;
+
+        } else {
+            addResult(result);
+            return false;
+        }
+    }
+
+    /**
+     * Validates the entries in a map.
+     *
+     * @param mapName name of the list
+     * @param map map whose entries are to be validated, or {@code null}
+     * @param entryValidator function to validate an entry in the map
+     * @return {@code true} if all entries in the map are valid, {@code false} otherwise
+     */
+    public <V> boolean validateMap(String mapName, Map<String, V> map,
+                    BiConsumer<BeanValidationResult, Entry<String, V>> entryValidator) {
+        if (map == null) {
+            return true;
+        }
+
+        var result = new BeanValidationResult(mapName, null);
+        for (Entry<String, V> ent : map.entrySet()) {
+            entryValidator.accept(result, ent);
+        }
+
+        if (result.isValid()) {
+            return true;
+
+        } else {
+            addResult(result);
+            return false;
+        }
+    }
+
+    /**
+     * Gets the validation result.
+     *
+     * @param initialIndentation the indentation to use on the main result output
+     * @param subIndentation the indentation to use on sub parts of the result output
+     * @param showClean output information on clean fields
+     * @return the result
+     */
+    @Override
+    public String getResult(final String initialIndentation, final String subIndentation, final boolean showClean) {
+        if (!showClean && getStatus() == ValidationStatus.CLEAN) {
+            return null;
+        }
+
+        var builder = new StringBuilder();
+
+        builder.append(initialIndentation);
+        builder.append('"');
+        builder.append(getName());
+
+        builder.append("\" ");
+        builder.append(getStatus());
+        builder.append(", ");
+        builder.append(getMessage());
+        builder.append('\n');
+
+        for (ValidationResult itemResult : itemResults) {
+            String message = itemResult.getResult(initialIndentation + subIndentation, subIndentation, showClean);
+            if (message != null) {
+                builder.append(message);
+            }
+        }
+
+        return builder.toString();
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/BeanValidator.java
new file mode 100644 (file)
index 0000000..c4244b2
--- /dev/null
@@ -0,0 +1,433 @@
+/*-
+ * ============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.parameters;
+
+import java.lang.reflect.Field;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.policy.common.parameters.annotations.ClassName;
+import org.onap.policy.common.parameters.annotations.Max;
+import org.onap.policy.common.parameters.annotations.Min;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+import org.onap.policy.common.parameters.annotations.Pattern;
+import org.onap.policy.common.parameters.annotations.Size;
+import org.onap.policy.common.parameters.annotations.Valid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Bean validator, supporting the parameter annotations.
+ */
+public class BeanValidator {
+    public static final Logger logger = LoggerFactory.getLogger(BeanValidator.class);
+
+    /**
+     * Validates top level fields within an object. For each annotated field, it retrieves
+     * the value using the public "getter" method for the field. If there is no public
+     * "getter" method, then it throws an exception. Otherwise, it validates the retrieved
+     * value based on the annotations. This recurses through super classes looking for
+     * fields to be verified, but it does not examine any interfaces.
+     *
+     * @param name name of the object being validated
+     * @param object object to be validated. If {@code null}, then an empty result is
+     *        returned
+     * @return the validation result
+     */
+    public BeanValidationResult validateTop(String name, Object object) {
+        var result = new BeanValidationResult(name, object);
+        if (object == null) {
+            return result;
+        }
+
+        // check class hierarchy - don't need to check interfaces
+        for (Class<?> clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+            validateFields(result, object, clazz);
+        }
+
+        return result;
+    }
+
+    /**
+     * Adds validators based on the annotations that are available.
+     *
+     * @param validator where to add the validators
+     */
+    protected void addValidators(ValueValidator validator) {
+        validator.addAnnotation(NotNull.class, this::verNotNull);
+        validator.addAnnotation(NotBlank.class, this::verNotBlank);
+        validator.addAnnotation(Size.class, this::verSize);
+        validator.addAnnotation(Max.class, this::verMax);
+        validator.addAnnotation(Min.class, this::verMin);
+        validator.addAnnotation(Pattern.class, this::verRegex);
+        validator.addAnnotation(ClassName.class, this::verClassName);
+        validator.addAnnotation(Valid.class, this::verCascade);
+    }
+
+    /**
+     * Performs validation of all annotated fields found within the class.
+     *
+     * @param result validation results are added here
+     * @param object object whose fields are to be validated
+     * @param clazz class, within the object's hierarchy, to be examined for fields to be
+     *        verified
+     */
+    private void validateFields(BeanValidationResult result, Object object, Class<?> clazz) {
+        for (Field field : clazz.getDeclaredFields()) {
+            var validator = makeFieldValidator(clazz, field);
+            validator.validateField(result, object);
+        }
+    }
+
+    /**
+     * Verifies that the value is not null.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verNotNull(BeanValidationResult result, String fieldName, Object value) {
+        if (value == null) {
+            result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, "is null");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Verifies that the value is not blank. Note: this does <i>not</i> verify that the
+     * value is not {@code null}.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verNotBlank(BeanValidationResult result, String fieldName, Object value) {
+        if (value instanceof String && StringUtils.isBlank(value.toString())) {
+            result.addResult(fieldName, xlate(value), ValidationStatus.INVALID, "is blank");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Verifies that the value has the specified number of elements.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param annot annotation against which the value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verSize(BeanValidationResult result, String fieldName, Size annot, Object value) {
+        int size;
+        if (value instanceof Collection) {
+            size = ((Collection<?>) value).size();
+
+        } else if (value instanceof Map) {
+            size = ((Map<?, ?>) value).size();
+
+        } else {
+            return true;
+        }
+
+
+        if (size < annot.min()) {
+            result.addResult(fieldName, xlate(value), ValidationStatus.INVALID,
+                            "minimum number of elements: " + annot.min());
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Verifies that the value matches a regular expression.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param annot annotation against which the value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verRegex(BeanValidationResult result, String fieldName, Pattern annot, Object value) {
+        try {
+            if (value instanceof String && com.google.re2j.Pattern.matches(annot.regexp(), value.toString())) {
+                return true;
+            }
+
+        } catch (RuntimeException e) {
+            logger.warn("validation error for regular expression: {}", annot.regexp(), e);
+        }
+
+        result.addResult(fieldName, xlate(value), ValidationStatus.INVALID,
+                        "does not match regular expression " + annot.regexp());
+        return false;
+    }
+
+    /**
+     * Verifies that the value is <= the minimum value.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param annot annotation against which the value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verMax(BeanValidationResult result, String fieldName, Max annot, Object value) {
+        if (!(value instanceof Number)) {
+            return true;
+        }
+
+        Number num = (Number) value;
+        if (num instanceof Integer || num instanceof Long) {
+            if (num.longValue() <= annot.value()) {
+                return true;
+            }
+
+        } else if (num instanceof Float || num instanceof Double) {
+            if (num.doubleValue() <= annot.value()) {
+                return true;
+            }
+
+        } else {
+            return true;
+        }
+
+        result.addResult(fieldName, xlate(value), ValidationStatus.INVALID,
+                        "exceeds the maximum value: " + annot.value());
+        return false;
+    }
+
+    /**
+     * Verifies that the value is >= the minimum value.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param annot annotation against which the value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verMin(BeanValidationResult result, String fieldName, Min annot, Object value) {
+        return verMin(result, fieldName, annot.value(), value);
+    }
+
+    /**
+     * Verifies that the value is >= the minimum value.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param min minimum against which the value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verMin(BeanValidationResult result, String fieldName, long min, Object value) {
+        if (!(value instanceof Number)) {
+            return true;
+        }
+
+        Number num = (Number) value;
+        if (num instanceof Integer || num instanceof Long) {
+            if (num.longValue() >= min) {
+                return true;
+            }
+
+        } else if (num instanceof Float || num instanceof Double) {
+            if (num.doubleValue() >= min) {
+                return true;
+            }
+
+        } else {
+            return true;
+        }
+
+        result.addResult(fieldName, xlate(value), ValidationStatus.INVALID,
+                        "is below the minimum value: " + min);
+        return false;
+    }
+
+    /**
+     * Verifies that the value is a valid class name.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verClassName(BeanValidationResult result, String fieldName, Object value) {
+        if (!(value instanceof String)) {
+            return true;
+        }
+
+        try {
+            Class.forName(value.toString());
+            return true;
+
+        } catch (final ClassNotFoundException exp) {
+            result.addResult(fieldName, value, ValidationStatus.INVALID, "class is not in the classpath");
+            return false;
+        }
+    }
+
+    /**
+     * Verifies that the value is valid by recursively invoking
+     * {@link #validateTop(String, Object)}.
+     *
+     * @param result where to add the validation result
+     * @param fieldName field whose value is being verified
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verCascade(BeanValidationResult result, String fieldName, Object value) {
+        if (value == null || value instanceof Collection || value instanceof Map) {
+            return true;
+        }
+
+        BeanValidationResult result2 = (value instanceof ParameterGroup parameterGroup ? parameterGroup.validate()
+                        : validateTop(fieldName, value));
+
+        if (result2.isClean()) {
+            return true;
+        }
+
+        result.addResult(result2);
+
+        return result2.isValid();
+    }
+
+    /**
+     * Validates the items in a collection.
+     *
+     * @param result where to add the validation result
+     * @param fieldName name of the field containing the collection
+     * @param itemValidator validator for individual items within the list
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verCollection(BeanValidationResult result, String fieldName, ValueValidator itemValidator,
+                    Object value) {
+
+        if (!(value instanceof Collection)) {
+            return true;
+        }
+
+        Collection<?> list = (Collection<?>) value;
+
+        var result2 = new BeanValidationResult(fieldName, value);
+        var count = 0;
+        for (Object item : list) {
+            itemValidator.validateValue(result2, String.valueOf(count++), item);
+        }
+
+        if (result2.isClean()) {
+            return true;
+        }
+
+        result.addResult(result2);
+        return false;
+    }
+
+    /**
+     * Validates the items in a Map.
+     *
+     * @param result where to add the validation result
+     * @param fieldName name of the field containing the map
+     * @param keyValidator validator for an individual key within the Map entry
+     * @param valueValidator validator for an individual value within the Map entry
+     * @param value value to be verified
+     * @return {@code true} if the next check should be performed, {@code false} otherwise
+     */
+    public boolean verMap(BeanValidationResult result, String fieldName, ValueValidator keyValidator,
+                    ValueValidator valueValidator, Object value) {
+
+        if (!(value instanceof Map)) {
+            return true;
+        }
+
+        Map<?, ?> map = (Map<?, ?>) value;
+
+        var result2 = new BeanValidationResult(fieldName, value);
+
+        for (Entry<?, ?> entry : map.entrySet()) {
+            String name = getEntryName(entry);
+
+            var result3 = new BeanValidationResult(name, entry);
+            keyValidator.validateValue(result3, "key", entry.getKey());
+            valueValidator.validateValue(result3, "value", entry.getValue());
+
+            if (!result3.isClean()) {
+                result2.addResult(result3);
+            }
+        }
+
+        if (result2.isClean()) {
+            return true;
+        }
+
+        result.addResult(result2);
+        return false;
+    }
+
+    /**
+     * Gets a name for an entry.
+     *
+     * @param entry entry whose name is to be determined
+     * @return a name for the entry
+     */
+    protected <K, V> String getEntryName(Map.Entry<K, V> entry) {
+        var key = entry.getKey();
+        if (key == null) {
+            return "";
+        }
+
+        return key.toString();
+    }
+
+    /**
+     * Makes a field validator.
+     *
+     * @param clazz class containing the field
+     * @param field field of interest
+     * @return a validator for the given field
+     */
+    protected FieldValidator makeFieldValidator(Class<?> clazz, Field field) {
+        return new FieldValidator(this, clazz, field);
+    }
+
+    /**
+     * Translates a value to something printable, for use by
+     * {@link ObjectValidationResult}. This default method simply returns the original
+     * value.
+     *
+     * @param value value to be translated
+     * @return the translated value
+     */
+    public Object xlate(Object value) {
+        return value;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/FieldValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/FieldValidator.java
new file mode 100644 (file)
index 0000000..d441c28
--- /dev/null
@@ -0,0 +1,292 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020-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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+import com.google.gson.annotations.SerializedName;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedParameterizedType;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collection;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Validator of the contents of a field, supporting the parameter annotations.
+ */
+public class FieldValidator extends ValueValidator {
+
+    /**
+     * {@code True} if there is a field-level annotation, {@code false} otherwise.
+     */
+    @Getter
+    @Setter(AccessLevel.PROTECTED)
+    private boolean fieldAnnotated = false;
+
+    /**
+     * Class containing the field of interest.
+     */
+    private final Class<?> clazz;
+
+    /**
+     * Field of interest.
+     */
+    private final Field field;
+
+    /**
+     * Name of the field when serialized (i.e., as the client would know it).
+     */
+    private final String serializedName;
+
+    /**
+     * Method to retrieve the field's value.
+     */
+    private Method accessor;
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param validator provider of validation methods
+     * @param clazz class containing the field
+     * @param field field whose value is to be validated
+     */
+    public FieldValidator(BeanValidator validator, Class<?> clazz, Field field) {
+        this.clazz = clazz;
+        this.field = field;
+
+        String fieldName = field.getName();
+        if (fieldName.contains("$")) {
+            serializedName = fieldName;
+            return;
+        }
+
+        SerializedName serAnnot = field.getAnnotation(SerializedName.class);
+        serializedName = (serAnnot != null ? serAnnot.value() : fieldName);
+
+        validator.addValidators(this);
+        addListValidator(validator);
+        addMapValidator(validator);
+
+        if (checkers.isEmpty()) {
+            // has no annotations - nothing to check
+            return;
+        }
+
+        // verify the field type is of interest
+        int mod = field.getModifiers();
+        if (Modifier.isStatic(mod)) {
+            classOnly(clazz.getName() + "." + fieldName + " is annotated but the field is static");
+            checkers.clear();
+            return;
+        }
+
+        // get the field's "getter" method
+        accessor = getAccessor(clazz, fieldName);
+        if (accessor == null) {
+            classOnly(clazz.getName() + "." + fieldName + " is annotated but has no \"get\" method");
+            checkers.clear();
+            return;
+        }
+
+        // determine if null is allowed
+        if (field.getAnnotation(NotNull.class) != null || clazz.getAnnotation(NotNull.class) != null) {
+            setNullAllowed(false);
+        }
+    }
+
+    /**
+     * Adds validators for the individual items within a collection, if the field is a
+     * collection.
+     *
+     * @param validator provider of validation methods
+     */
+    private void addListValidator(BeanValidator validator) {
+        if (!Collection.class.isAssignableFrom(field.getType())) {
+            return;
+        }
+
+        var tannot = field.getAnnotatedType();
+        if (!(tannot instanceof AnnotatedParameterizedType)) {
+            return;
+        }
+
+        AnnotatedType[] targs = ((AnnotatedParameterizedType) tannot).getAnnotatedActualTypeArguments();
+        if (targs.length != 1) {
+            return;
+        }
+
+        var itemValidator = new ItemValidator(validator, targs[0]);
+        if (itemValidator.isEmpty()) {
+            return;
+        }
+
+        checkers.add((result, fieldName, value) -> validator.verCollection(result, fieldName, itemValidator, value));
+    }
+
+    /**
+     * Adds validators for the individual entries within a map, if the field is a map.
+     *
+     * @param validator provider of validation methods
+     */
+    private void addMapValidator(BeanValidator validator) {
+        if (!Map.class.isAssignableFrom(field.getType())) {
+            return;
+        }
+
+        var tannot = field.getAnnotatedType();
+        if (!(tannot instanceof AnnotatedParameterizedType)) {
+            return;
+        }
+
+        AnnotatedType[] targs = ((AnnotatedParameterizedType) tannot).getAnnotatedActualTypeArguments();
+        if (targs.length != 2) {
+            return;
+        }
+
+        var keyValidator = new ItemValidator(validator, targs[0]);
+        var valueValidator = new ItemValidator(validator, targs[1]);
+        if (keyValidator.isEmpty() && valueValidator.isEmpty()) {
+            return;
+        }
+
+        checkers.add((result, fieldName, value) -> validator.verMap(result, fieldName, keyValidator, valueValidator,
+                        value));
+    }
+
+    /**
+     * Performs validation of a single field.
+     *
+     * @param result validation results are added here
+     * @param object object whose field is to be validated
+     */
+    public void validateField(BeanValidationResult result, Object object) {
+        if (isEmpty()) {
+            // has no annotations - nothing to check
+            return;
+        }
+
+        // get the value
+        Object value = getValue(object, accessor);
+
+        validateValue(result, serializedName, value);
+    }
+
+    /**
+     * Gets the value from the object using the accessor function.
+     *
+     * @param object object whose value is to be retrieved
+     * @param accessor "getter" method
+     * @return the object's value
+     */
+    private Object getValue(Object object, Method accessor) {
+        try {
+            return accessor.invoke(object);
+
+        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+            throw new IllegalArgumentException(clazz.getName() + "." + field.getName() + " accessor threw an exception",
+                            e);
+        }
+    }
+
+    /**
+     * Throws an exception if there are field-level annotations.
+     *
+     * @param exceptionMessage exception message
+     */
+    private void classOnly(String exceptionMessage) {
+        if (isFieldAnnotated()) {
+            throw new IllegalArgumentException(exceptionMessage);
+        }
+    }
+
+    /**
+     * Gets an annotation from the field or the class.
+     *
+     * @param annotClass annotation class of interest
+     * @return the annotation, or {@code null} if neither the field nor the class has the
+     *         desired annotation
+     */
+    @Override
+    public <T extends Annotation> T getAnnotation(Class<T> annotClass) {
+
+        // field annotation takes precedence over class annotation
+        var annot = field.getAnnotation(annotClass);
+        if (annot != null) {
+            setFieldAnnotated(true);
+            return annot;
+        }
+
+        return clazz.getAnnotation(annotClass);
+    }
+
+    /**
+     * Gets an accessor method for the given field.
+     *
+     * @param clazz class whose methods are to be searched
+     * @param fieldName field whose "getter" is to be identified
+     * @return the field's "getter" method, or {@code null} if it is not found
+     */
+    private Method getAccessor(Class<?> clazz, String fieldName) {
+        var capname = StringUtils.capitalize(fieldName);
+        var accessor2 = getMethod(clazz, "get" + capname);
+        if (accessor2 != null) {
+            return accessor2;
+        }
+
+        return getMethod(clazz, "is" + capname);
+    }
+
+    /**
+     * Gets the "getter" method having the specified name.
+     *
+     * @param clazz class whose methods are to be searched
+     * @param methodName name of the method of interest
+     * @return the method, or {@code null} if it is not found
+     */
+    private Method getMethod(Class<?> clazz, String methodName) {
+        for (Method method : clazz.getMethods()) {
+            if (methodName.equals(method.getName()) && validMethod(method)) {
+                return method;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Determines if a method is a valid "getter".
+     *
+     * @param method method to be checked
+     * @return {@code true} if the method is a valid "getter", {@code false} otherwise
+     */
+    private boolean validMethod(Method method) {
+        int mod = method.getModifiers();
+        return !(Modifier.isStatic(mod) || method.getReturnType() == void.class || method.getParameterCount() != 0);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ItemValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ItemValidator.java
new file mode 100644 (file)
index 0000000..44b70cd
--- /dev/null
@@ -0,0 +1,71 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedType;
+
+/**
+ * Validator of an "item", which is typically found in a collection, or the key or value
+ * components of an entry in a Map.
+ */
+public class ItemValidator extends ValueValidator {
+    private final AnnotatedType annotatedType;
+
+    /**
+     * Constructs the object.
+     *
+     * @param validator provider of validation methods
+     * @param annotatedType a type having validation annotations to be
+     *        applied to the item
+     */
+    public ItemValidator(BeanValidator validator, AnnotatedType annotatedType) {
+        this(validator, annotatedType, true);
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param validator provider of validation methods
+     * @param annotatedType a type having validation annotations to be
+     *        applied to the item
+     * @param addValidators {@code true} if to add validators
+     */
+    public ItemValidator(BeanValidator validator, AnnotatedType annotatedType, boolean addValidators) {
+        this.annotatedType = annotatedType;
+
+        if (addValidators) {
+            validator.addValidators(this);
+        }
+    }
+
+    /**
+     * Gets an annotation from the field or the class.
+     *
+     * @param annotClass annotation class of interest
+     * @return the annotation, or {@code null} if the {@link #annotatedType} does
+     *         not contain the desired annotation
+     */
+    @Override
+    public <T extends Annotation> T getAnnotation(Class<T> annotClass) {
+        return annotatedType.getAnnotation(annotClass);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ObjectValidationResult.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ObjectValidationResult.java
new file mode 100644 (file)
index 0000000..af1884a
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  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.parameters;
+
+/**
+ * This class holds the result of the validation of an object within a bean.
+ */
+public class ObjectValidationResult extends ValidationResultImpl {
+
+    /**
+     * Constructs the object.
+     *
+     * @param name name of the object of this result
+     * @param object object being validated
+     */
+    public ObjectValidationResult(String name, Object object) {
+        super(name, object);
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param name name of the object of this result
+     * @param object object being validated
+     * @param status result status
+     * @param message result message
+     */
+    public ObjectValidationResult(String name, Object object, ValidationStatus status, String message) {
+        super(name, object, status, message);
+    }
+
+    /**
+     * Gets the validation result.
+     *
+     * @param initialIndentation the result indentation
+     * @param subIndentation the indentation to use on sub parts of the result output
+     * @param showClean output information on clean fields
+     * @return the result
+     */
+    @Override
+    public String getResult(final String initialIndentation, final String subIndentation, final boolean showClean) {
+        if (!showClean && getStatus() == ValidationStatus.CLEAN) {
+            return null;
+        }
+
+        return initialIndentation + "item \"" + getName() + "\" value \"" + getObject() + "\" " + getStatus() + ", "
+                        + getMessage() + '\n';
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterConstants.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterConstants.java
new file mode 100644 (file)
index 0000000..300d49c
--- /dev/null
@@ -0,0 +1,43 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2018 Ericsson. All rights reserved.
+ *  Modifications Copyright (C) 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.parameters;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+/**
+ * This static class holds the values of constants for parameter handling.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class ParameterConstants {
+    // Indentation is 0 on the left and 2 for each level of hierarchy
+    public static final String DEFAULT_INITIAL_RESULT_INDENTATION = "";
+    public static final String DEFAULT_RESULT_INDENTATION = "  ";
+
+    // By default we do not show validation results for parameters that are validated as clean
+    public static final boolean DO_NOT_SHOW_CLEAN_RESULTS = false;
+
+    // Messages for clean validations
+    public static final String PARAMETER_GROUP_HAS_STATUS_MESSAGE = "parameter group has status ";
+    public static final String PARAMETER_GROUP_MAP_HAS_STATUS_MESSAGE = "parameter group map has status ";
+    public static final String PARAMETER_HAS_STATUS_MESSAGE       = "parameter has status ";
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroup.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroup.java
new file mode 100644 (file)
index 0000000..c26b7b4
--- /dev/null
@@ -0,0 +1,61 @@
+/*-
+ * ============LICENSE_START=========================================================
+ *  Copyright (C) 2018 Ericsson. All rights reserved.
+ *  Modifications Copyright (C) 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+/**
+ * This interface acts as a base interface for all parameter groups in the ONAP Policy Framework. All parameter group
+ * POJOs are implementations of the parameter group interface and can be used with the {@link ParameterService}.
+ *
+ * @author Liam Fallon (liam.fallon@ericsson.com)
+ */
+public interface ParameterGroup {
+    /**
+     * Get the group name.
+     *
+     * @return the group name
+     */
+    String getName();
+
+    /**
+     * Set the group name.
+     *
+     * @param name the group name
+     */
+    void setName(final String name);
+
+    /**
+     * Validate parameters.
+     *
+     * @return the result of the parameter validation
+     */
+    BeanValidationResult validate();
+
+    /**
+     * Check if the parameters are valid.
+     *
+     * @return true if the parameters are valid
+     */
+    default boolean isValid() {
+        return validate().getStatus().isValid();
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroupImpl.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ParameterGroupImpl.java
new file mode 100644 (file)
index 0000000..8a987bd
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Implementation of a parameter group.
+ */
+@NotNull
+@NotBlank
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class ParameterGroupImpl implements ParameterGroup {
+    /**
+     * Group name.
+     */
+    private String name;
+
+    @Override
+    public BeanValidationResult validate() {
+        return new BeanValidator().validateTop(getClass().getSimpleName(), this);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResult.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResult.java
new file mode 100644 (file)
index 0000000..e601741
--- /dev/null
@@ -0,0 +1,89 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2018 Ericsson. 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+/**
+ * This interface defines the result of a parameter validation.
+ */
+public interface ValidationResult {
+    /**
+     * Gets the name of the entity being validated.
+     *
+     * @return the name
+     */
+    String getName();
+
+    /**
+     * Gets the status of validation.
+     *
+     * @return the status
+     */
+    ValidationStatus getStatus();
+
+    /**
+     * Checks if the result is valid.
+     *
+     * @return true, if is valid
+     */
+    default boolean isValid() {
+        return getStatus().isValid();
+    }
+
+    /**
+     * Checks if the result is clean.
+     *
+     * @return true, if is clean
+     */
+    default boolean isClean() {
+        return getStatus().isClean();
+    }
+
+    /**
+     * Gets the validation result.
+     *
+     * @return the full validation result
+     */
+    default String getResult() {
+        return getResult(
+            ParameterConstants.DEFAULT_INITIAL_RESULT_INDENTATION,
+            ParameterConstants.DEFAULT_RESULT_INDENTATION,
+            ParameterConstants.DO_NOT_SHOW_CLEAN_RESULTS);
+    }
+
+    /**
+     * Gets the validation result.
+     *
+     * @param initialIndentation the indentation to use on the main result output
+     * @param subIndentation     the indentation to use on sub parts of the result output
+     * @param showClean          output information on clean fields
+     * @return the result
+     */
+    String getResult(final String initialIndentation, final String subIndentation, final boolean showClean);
+
+    /**
+     * Set a validation result.
+     *
+     * @param status  The validation status the field is receiving
+     * @param message The validation message explaining the validation status
+     */
+    void setResult(final ValidationStatus status, final String message);
+}
\ No newline at end of file
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResultImpl.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationResultImpl.java
new file mode 100644 (file)
index 0000000..4589881
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved.
+ *  Modifications Copyright (C) 2020 Bell Canada. 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.parameters;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * Basic implementation of a ValidationResult.
+ */
+@Getter
+@AllArgsConstructor
+public abstract class ValidationResultImpl implements ValidationResult {
+    public static final String ITEM_HAS_STATUS_MESSAGE = "item has status ";
+
+    /**
+     * Name of the object of this result.
+     */
+    private final String name;
+
+    /**
+     * Object being validated.
+     */
+    private final Object object;
+
+    /**
+     * Validation status of this object.
+     */
+    private ValidationStatus status = ValidationStatus.CLEAN;
+
+    /**
+     * Validation message.
+     */
+    private String message = ITEM_HAS_STATUS_MESSAGE + status.toString();
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param name name of the object of this result
+     * @param object object being validated
+     */
+    protected ValidationResultImpl(String name, Object object) {
+        this.name = name;
+        this.object = object;
+    }
+
+    /**
+     * Validates that the value is not {@code null}.
+     *
+     * @return {@code true} if the value is not null, {@code false} otherwise
+     */
+    public boolean validateNotNull() {
+        if (object == null) {
+            setResult(ValidationStatus.INVALID, "is null");
+            return false;
+
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Set the validation result status. On a sequence of calls, the most serious
+     * validation status is recorded, assuming the status enum ordinals increase in order
+     * of severity.
+     *
+     * @param status validation status the bean is receiving
+     */
+    public void setResult(final ValidationStatus status) {
+        setResult(status, ITEM_HAS_STATUS_MESSAGE + status.toString());
+    }
+
+    /**
+     * Set the validation result status. On a sequence of calls, the most serious
+     * validation status is recorded, assuming the status enum ordinals increase in order
+     * of severity.
+     *
+     * @param status the validation status
+     * @param message the validation message explaining the validation status
+     */
+    @Override
+    public void setResult(final ValidationStatus status, final String message) {
+        if (this.status.ordinal() < status.ordinal()) {
+            this.status = status;
+            this.message = message;
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationStatus.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ValidationStatus.java
new file mode 100644 (file)
index 0000000..8692506
--- /dev/null
@@ -0,0 +1,44 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2018 Ericsson. 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.parameters;
+
+public enum ValidationStatus {
+    CLEAN,
+    OBSERVATION,
+    WARNING,
+    INVALID;
+    
+    /**
+     * The result of a validation is valid unless the status is INVALID.
+     * @return true if the validation has passed
+     */
+    public boolean isValid() {
+        return !this.equals(INVALID);
+    }
+    
+    /**
+     * Check if the validation was clean.
+     * @return true if the validation is clean
+     */
+    public boolean isClean() {
+        return this.equals(CLEAN);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/ValueValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/ValueValidator.java
new file mode 100644 (file)
index 0000000..faf4100
--- /dev/null
@@ -0,0 +1,146 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020-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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Validator of a value.
+ * <p/>
+ * Note: this currently does not support Min/Max validation of "short" or "byte"; these
+ * annotations are simply ignored for these types.
+ */
+@NoArgsConstructor
+public class ValueValidator {
+
+    /**
+     * {@code True} if the value is allowed to be {@code null}, {@code false} otherwise.
+     * Subclasses are expected to set this, typically based on the validation annotations
+     * associated with the value.
+     */
+    @Getter
+    @Setter(AccessLevel.PROTECTED)
+    private boolean nullAllowed = true;
+
+    /**
+     * Predicates to invoke to validate an object.
+     * <p/>
+     * Note: each predicate is expected to return {@code true} if the next check is
+     * allowed to proceed, {@code false} otherwise. In addition, if {@link #nullAllowed}
+     * is {@code true}, then the predicates must be prepared to deal with a {@code null}
+     * Object as their input parameter.
+     */
+    protected List<Checker> checkers = new ArrayList<>(10);
+
+    /**
+     * Determines if the validator has anything to check.
+     *
+     * @return {@code true} if the validator is empty (i.e., has nothing to check)
+     */
+    public boolean isEmpty() {
+        return checkers.isEmpty();
+    }
+
+    /**
+     * Performs validation of a single field.
+     *
+     * @param result validation results are added here
+     * @param fieldName field whose value is being verified
+     * @param value value to be validated
+     */
+    protected void validateValue(BeanValidationResult result, String fieldName, Object value) {
+
+        if (value == null && isNullAllowed()) {
+            // value is null and null is allowed - just return
+            return;
+        }
+
+        for (Checker checker : checkers) {
+            if (!checker.test(result, fieldName, value)) {
+                // invalid - don't bother with additional checks
+                return;
+            }
+        }
+    }
+
+    /**
+     * Looks for an annotation at the class or field level. If an annotation is found at
+     * either the field or class level, then it adds a verifier to
+     * {@link ValueValidator#checkers}.
+     *
+     * @param annotClass class of annotation to find
+     * @param checker function to validate the value
+     */
+    public <T extends Annotation> void addAnnotation(Class<T> annotClass, Checker checker) {
+        var annot = getAnnotation(annotClass);
+        if (annot != null) {
+            checkers.add(checker);
+
+            if (annotClass == NotNull.class) {
+                setNullAllowed(false);
+            }
+        }
+    }
+
+    /**
+     * Looks for an annotation at the class or field level. If an annotation is found at
+     * either the field or class level, then it adds a verifier to
+     * {@link ValueValidator#checkers}.
+     *
+     * @param annotClass class of annotation to find
+     * @param checker function to validate the value
+     */
+    public <T extends Annotation> void addAnnotation(Class<T> annotClass, CheckerWithAnnot<T> checker) {
+        var annot = getAnnotation(annotClass);
+        if (annot != null) {
+            checkers.add((result, fieldName, value) -> checker.test(result, fieldName, annot, value));
+        }
+    }
+
+    /**
+     * Gets an annotation from the field or the class. The default method simply returns
+     * {@code null}.
+     *
+     * @param annotClass annotation class of interest
+     * @return the annotation, or {@code null} if neither the field nor the class has the
+     *         desired annotation
+     */
+    public <T extends Annotation> T getAnnotation(Class<T> annotClass) {
+        return null;
+    }
+
+    // functions to validate a value extracted from a field
+
+    public static interface Checker {
+        boolean test(BeanValidationResult result, String fieldName, Object value);
+    }
+
+    public static interface CheckerWithAnnot<T extends Annotation> {
+        boolean test(BeanValidationResult result, String fieldName, T annotation, Object value);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/ClassName.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/ClassName.java
new file mode 100644 (file)
index 0000000..14d76fd
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that a field (i.e., String) identifies the name of a class in the classpath.
+ */
+@Retention(RUNTIME)
+@Target({FIELD, TYPE_USE})
+public @interface ClassName {
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Max.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Max.java
new file mode 100644 (file)
index 0000000..f28fd2c
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({FIELD, TYPE_USE})
+public @interface Max {
+
+    /**
+     * The maximum value allowed.
+     *
+     * @return the maximum value allowed
+     */
+    long value();
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Min.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Min.java
new file mode 100644 (file)
index 0000000..305d981
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({FIELD, TYPE_USE})
+public @interface Min {
+
+    /**
+     * The minimum value allowed.
+     *
+     * @return the minimum value allowed
+     */
+    long value();
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotBlank.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotBlank.java
new file mode 100644 (file)
index 0000000..0744beb
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that a field (i.e., String) may not be empty.
+ */
+@Retention(RUNTIME)
+@Target({TYPE, FIELD, TYPE_USE})
+public @interface NotBlank {
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotNull.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/NotNull.java
new file mode 100644 (file)
index 0000000..8135600
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that a field may not be null.
+ */
+@Retention(RUNTIME)
+@Target({TYPE, FIELD, TYPE_USE})
+public @interface NotNull {
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Pattern.java
new file mode 100644 (file)
index 0000000..91a74d7
--- /dev/null
@@ -0,0 +1,38 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020-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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({FIELD, TYPE_USE})
+public @interface Pattern {
+
+    /**
+     * Regular expression to be matched.
+     */
+    String regexp();
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Size.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Size.java
new file mode 100644 (file)
index 0000000..160e012
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates the size of a Map or Collection.
+ */
+@Retention(RUNTIME)
+@Target({FIELD, TYPE_USE})
+public @interface Size {
+
+    /**
+     * The minimum size allowed.
+     *
+     * @return the minimum size allowed
+     */
+    int min();
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java b/policy-common/src/main/java/org/onap/policy/common/parameters/annotations/Valid.java
new file mode 100644 (file)
index 0000000..227a726
--- /dev/null
@@ -0,0 +1,35 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020-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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.TYPE_USE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({TYPE, FIELD, TYPE_USE})
+public @interface Valid {
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/topic/BusTopicParams.java b/policy-common/src/main/java/org/onap/policy/common/parameters/topic/BusTopicParams.java
new file mode 100644 (file)
index 0000000..66ac57c
--- /dev/null
@@ -0,0 +1,338 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved.
+ * Modifications Copyright (C) 2018-2019, 2021 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.parameters.topic;
+
+import java.util.List;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Member variables of this Params class are as follows.
+ *
+ * <p>servers Kafka servers
+ * topic Kafka Topic to be monitored
+ * apiKey Kafka API Key (optional)
+ * apiSecret Kafka API Secret (optional)
+ * consumerGroup kafka Reader Consumer Group
+ * consumerInstance Kafka Reader Instance
+ * fetchTimeout kafka fetch timeout
+ * fetchLimit Kafka fetch limit
+ * environment DME2 Environment
+ * aftEnvironment DME2 AFT Environment
+ * partner DME2 Partner
+ * latitude DME2 Latitude
+ * longitude DME2 Longitude
+ * additionalProps Additional properties to pass to DME2
+ * useHttps does connection use HTTPS?
+ * allowTracing is message tracing allowed?
+ * allowSelfSignedCerts are self-signed certificates allow
+ */
+@Getter
+@Setter
+public class BusTopicParams {
+
+    private int port;
+    private List<String> servers;
+    private Map<String, String> additionalProps;
+    private String topic;
+    private String effectiveTopic;
+    private String apiKey;
+    private String apiSecret;
+    private String consumerGroup;
+    private String consumerInstance;
+    private int fetchTimeout;
+    private int fetchLimit;
+    private boolean useHttps;
+    private boolean allowTracing;
+    private boolean allowSelfSignedCerts;
+    private boolean managed;
+
+    private String userName;
+    private String password;
+    private String environment;
+    private String aftEnvironment;
+    private String partner;
+    private String latitude;
+    private String longitude;
+    private String partitionId;
+    private String clientName;
+    private String hostname;
+    private String basePath;
+    @Getter
+    private String serializationProvider;
+
+    public static TopicParamsBuilder builder() {
+        return new TopicParamsBuilder();
+    }
+
+    /**
+     * Methods to Check if the property is INVALID.
+     */
+
+    boolean isEnvironmentInvalid() {
+        return StringUtils.isBlank(environment);
+    }
+
+    boolean isAftEnvironmentInvalid() {
+        return StringUtils.isBlank(aftEnvironment);
+    }
+
+    boolean isLatitudeInvalid() {
+        return StringUtils.isBlank(latitude);
+    }
+
+    boolean isLongitudeInvalid() {
+        return StringUtils.isBlank(longitude);
+    }
+
+    public boolean isConsumerInstanceInvalid() {
+        return StringUtils.isBlank(consumerInstance);
+    }
+
+    public boolean isConsumerGroupInvalid() {
+        return StringUtils.isBlank(consumerGroup);
+    }
+
+    public boolean isClientNameInvalid() {
+        return StringUtils.isBlank(clientName);
+    }
+
+    boolean isPartnerInvalid() {
+        return StringUtils.isBlank(partner);
+    }
+
+    boolean isServersInvalid() {
+        return (servers == null || servers.isEmpty()
+            || (servers.size() == 1 && ("".equals(servers.get(0)))));
+    }
+
+    public boolean isTopicInvalid() {
+        return StringUtils.isBlank(topic);
+    }
+
+    public boolean isPartitionIdInvalid() {
+        return StringUtils.isBlank(partitionId);
+    }
+
+    public boolean isHostnameInvalid() {
+        return StringUtils.isBlank(hostname);
+    }
+
+    public boolean isPortInvalid() {
+        return (getPort() <= 0 || getPort() >= 65535);
+    }
+
+    /**
+     * Methods to Check if the property is Valid.
+     */
+
+    boolean isApiKeyValid() {
+        return StringUtils.isNotBlank(apiKey);
+    }
+
+    boolean isApiSecretValid() {
+        return StringUtils.isNotBlank(apiSecret);
+    }
+
+    boolean isUserNameValid() {
+        return StringUtils.isNotBlank(userName);
+    }
+
+    boolean isPasswordValid() {
+        return StringUtils.isNotBlank(password);
+    }
+
+    public boolean isAdditionalPropsValid() {
+        return additionalProps != null;
+    }
+
+    public void setEffectiveTopic(String effectiveTopic) {
+        this.effectiveTopic = topicToLowerCase(effectiveTopic);
+    }
+
+    public void setTopic(String topic) {
+        this.topic = topicToLowerCase(topic);
+    }
+
+    public String getEffectiveTopic() {
+        return topicToLowerCase(effectiveTopic);
+    }
+
+    public String getTopic() {
+        return topicToLowerCase(topic);
+    }
+
+    private String topicToLowerCase(String topic) {
+        return (topic == null || topic.isEmpty()) ? topic : topic.toLowerCase();
+    }
+
+    @NoArgsConstructor(access = AccessLevel.PRIVATE)
+    public static class TopicParamsBuilder {
+
+        final BusTopicParams params = new BusTopicParams();
+
+        public TopicParamsBuilder servers(List<String> servers) {
+            this.params.servers = servers;
+            return this;
+        }
+
+        public TopicParamsBuilder topic(String topic) {
+            this.params.setTopic(topic);
+            return this;
+        }
+
+        public TopicParamsBuilder effectiveTopic(String effectiveTopic) {
+            this.params.setEffectiveTopic(effectiveTopic);
+            return this;
+        }
+
+        public TopicParamsBuilder apiKey(String apiKey) {
+            this.params.apiKey = apiKey;
+            return this;
+        }
+
+        public TopicParamsBuilder apiSecret(String apiSecret) {
+            this.params.apiSecret = apiSecret;
+            return this;
+        }
+
+        public TopicParamsBuilder consumerGroup(String consumerGroup) {
+            this.params.consumerGroup = consumerGroup;
+            return this;
+        }
+
+        public TopicParamsBuilder consumerInstance(String consumerInstance) {
+            this.params.consumerInstance = consumerInstance;
+            return this;
+        }
+
+        public TopicParamsBuilder fetchTimeout(int fetchTimeout) {
+            this.params.fetchTimeout = fetchTimeout;
+            return this;
+        }
+
+        public TopicParamsBuilder fetchLimit(int fetchLimit) {
+            this.params.fetchLimit = fetchLimit;
+            return this;
+        }
+
+        public TopicParamsBuilder useHttps(boolean useHttps) {
+            this.params.useHttps = useHttps;
+            return this;
+        }
+
+        public TopicParamsBuilder allowTracing(boolean allowTracing) {
+            this.params.allowTracing = allowTracing;
+            return this;
+        }
+
+        public TopicParamsBuilder allowSelfSignedCerts(boolean allowSelfSignedCerts) {
+            this.params.allowSelfSignedCerts = allowSelfSignedCerts;
+            return this;
+        }
+
+        public TopicParamsBuilder userName(String userName) {
+            this.params.userName = userName;
+            return this;
+        }
+
+        public TopicParamsBuilder password(String password) {
+            this.params.password = password;
+            return this;
+        }
+
+        public TopicParamsBuilder environment(String environment) {
+            this.params.environment = environment;
+            return this;
+        }
+
+        public TopicParamsBuilder aftEnvironment(String aftEnvironment) {
+            this.params.aftEnvironment = aftEnvironment;
+            return this;
+        }
+
+        public TopicParamsBuilder partner(String partner) {
+            this.params.partner = partner;
+            return this;
+        }
+
+        public TopicParamsBuilder latitude(String latitude) {
+            this.params.latitude = latitude;
+            return this;
+        }
+
+        public TopicParamsBuilder longitude(String longitude) {
+            this.params.longitude = longitude;
+            return this;
+        }
+
+        public TopicParamsBuilder additionalProps(Map<String, String> additionalProps) {
+            this.params.additionalProps = additionalProps;
+            return this;
+        }
+
+        public TopicParamsBuilder partitionId(String partitionId) {
+            this.params.partitionId = partitionId;
+            return this;
+        }
+
+        public BusTopicParams build() {
+            return params;
+        }
+
+        public TopicParamsBuilder managed(boolean managed) {
+            this.params.managed = managed;
+            return this;
+        }
+
+        public TopicParamsBuilder hostname(String hostname) {
+            this.params.hostname = hostname;
+            return this;
+        }
+
+        public TopicParamsBuilder clientName(String clientName) {
+            this.params.clientName = clientName;
+            return this;
+        }
+
+        public TopicParamsBuilder port(int port) {
+            this.params.port = port;
+            return this;
+        }
+
+        public TopicParamsBuilder basePath(String basePath) {
+            this.params.basePath = basePath;
+            return this;
+        }
+
+        public TopicParamsBuilder serializationProvider(String serializationProvider) {
+            this.params.serializationProvider = serializationProvider;
+            return this;
+        }
+    }
+}
+
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameterGroup.java b/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameterGroup.java
new file mode 100644 (file)
index 0000000..c9d5e3e
--- /dev/null
@@ -0,0 +1,92 @@
+/*-
+ * ============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.parameters.topic;
+
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.policy.common.parameters.BeanValidationResult;
+import org.onap.policy.common.parameters.ParameterGroupImpl;
+import org.onap.policy.common.parameters.ValidationStatus;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Class to hold all parameters needed for topic properties.
+ *
+ * @author Ajith Sreekumar (ajith.sreekumar@est.tech)
+ */
+@NotNull
+@NotBlank
+@Getter
+@Setter
+public class TopicParameterGroup extends ParameterGroupImpl {
+
+    private List<TopicParameters> topicSources;
+    private List<TopicParameters> topicSinks;
+
+    public TopicParameterGroup() {
+        super(TopicParameterGroup.class.getSimpleName());
+    }
+
+    /**
+     * {@inheritDoc}.
+     */
+    @Override
+    public BeanValidationResult validate() {
+        BeanValidationResult result = super.validate();
+        if (result.isValid()) {
+            var errorMsg = new StringBuilder();
+            StringBuilder missingSourceParams = checkMissingMandatoryParams(topicSources);
+            if (!missingSourceParams.isEmpty()) {
+                errorMsg.append(missingSourceParams.append("missing in topicSources. "));
+            }
+            StringBuilder missingSinkParams = checkMissingMandatoryParams(topicSinks);
+            if (!missingSinkParams.isEmpty()) {
+                errorMsg.append(missingSinkParams.append("missing in topicSinks."));
+            }
+
+            if (!errorMsg.isEmpty()) {
+                errorMsg.insert(0, "Mandatory parameters are missing. ");
+                result.setResult(ValidationStatus.INVALID, errorMsg.toString());
+            }
+        }
+        return result;
+    }
+
+    private StringBuilder checkMissingMandatoryParams(List<TopicParameters> topicParametersList) {
+        var missingParams = new StringBuilder();
+        for (TopicParameters topicParameters : topicParametersList) {
+            if (StringUtils.isBlank(topicParameters.getTopic())) {
+                missingParams.append("topic, ");
+            }
+            if (StringUtils.isBlank(topicParameters.getTopicCommInfrastructure())) {
+                missingParams.append("topicCommInfrastructure, ");
+            }
+            if (null == topicParameters.getServers() || topicParameters.getServers().isEmpty()) {
+                missingParams.append("servers, ");
+            }
+        }
+        return missingParams;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameters.java b/policy-common/src/main/java/org/onap/policy/common/parameters/topic/TopicParameters.java
new file mode 100644 (file)
index 0000000..2b8bfbd
--- /dev/null
@@ -0,0 +1,47 @@
+/*-
+ * ============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.parameters.topic;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Class to hold topic details such as name, server and topicCommInfrastructure.
+ *
+ * @author Ajith Sreekumar (ajith.sreekumar@est.tech)
+ */
+@NotNull
+@NotBlank
+@Getter
+@Setter
+@EqualsAndHashCode(callSuper = false)
+public class TopicParameters extends BusTopicParams {
+    private String topicCommInfrastructure;
+
+    public TopicParameters() {
+        // this defaults to true
+        setManaged(true);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupConstraint.java b/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupConstraint.java
new file mode 100644 (file)
index 0000000..c73d135
--- /dev/null
@@ -0,0 +1,57 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2021, 2023 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.validation;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Documented
+@Constraint(validatedBy = ParameterGroupValidator.class)
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ParameterGroupConstraint {
+
+    /**
+     * Get error Message.
+     *
+     * @return error Message
+     */
+    String message() default "validation error(s) on parameters";
+
+    /**
+     * Get groups.
+     *
+     * @return Class arrays
+     */
+    Class<?>[] groups() default {};
+
+    /**
+     * Get payload.
+     *
+     * @return Class arrays
+     */
+    Class<? extends Payload>[] payload() default {};
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupValidator.java b/policy-common/src/main/java/org/onap/policy/common/parameters/validation/ParameterGroupValidator.java
new file mode 100644 (file)
index 0000000..024ca13
--- /dev/null
@@ -0,0 +1,42 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2021, 2023 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters.validation;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import org.onap.policy.common.parameters.BeanValidationResult;
+import org.onap.policy.common.parameters.ParameterGroup;
+
+public class ParameterGroupValidator implements ConstraintValidator<ParameterGroupConstraint, ParameterGroup> {
+
+    @Override
+    public boolean isValid(ParameterGroup value, ConstraintValidatorContext context) {
+        if (value == null) {
+            return true;
+        }
+        final BeanValidationResult result = value.validate();
+        if (!result.isValid()) {
+            context.buildConstraintViolationWithTemplate(result.getMessage()).addConstraintViolation();
+        }
+        return result.isValid();
+    }
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategy.java b/policy-common/src/main/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategy.java
new file mode 100644 (file)
index 0000000..d9a7e97
--- /dev/null
@@ -0,0 +1,39 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2022 Bell Canada. All rights reserved.
+ * Modifications Copyright (C) 2023 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.spring.utils;
+
+import java.io.Serial;
+import org.hibernate.boot.model.naming.Identifier;
+import org.hibernate.boot.model.naming.ImplicitJoinColumnNameSource;
+import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl;
+
+public class CustomImplicitNamingStrategy extends ImplicitNamingStrategyJpaCompliantImpl {
+
+    @Serial
+    private static final long serialVersionUID = 8666774028328486896L;
+
+    @Override
+    public Identifier determineJoinColumnName(ImplicitJoinColumnNameSource source) {
+        String name = source.getReferencedColumnName().getText();
+        return toIdentifier(name, source.getBuildingContext());
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverter.java b/policy-common/src/main/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverter.java
new file mode 100644 (file)
index 0000000..5fa0260
--- /dev/null
@@ -0,0 +1,108 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2022-2023 Bell Canada. 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.spring.utils;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import org.onap.policy.common.utils.coder.YamlJsonTranslator;
+import org.springframework.core.GenericTypeResolver;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.lang.Nullable;
+
+/**
+ * Custom converter to marshal/unmarshall data structured with YAML media type.
+ */
+public class YamlHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
+
+    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+    private static final YamlJsonTranslator TRANSLATOR = new YamlJsonTranslator();
+
+    public YamlHttpMessageConverter() {
+        super(new MediaType("application", "yaml"));
+        setDefaultCharset(DEFAULT_CHARSET);
+    }
+
+    @Override
+    public final Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
+        throws IOException {
+        return readResolved(GenericTypeResolver.resolveType(type, contextClass), inputMessage);
+    }
+
+    @Override
+    protected final Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException {
+        return readResolved(clazz, inputMessage);
+    }
+
+    private Object readInternal(Type resolvedType, Reader reader) {
+        Class<?> clazz = (Class<?>) resolvedType;
+        return TRANSLATOR.fromYaml(reader, clazz);
+    }
+
+    @Override
+    protected final void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
+        throws IOException {
+        try (var writer = getWriter(outputMessage)) {
+            writeInternal(object, writer);
+            writer.flush();
+        } catch (Exception ex) {
+            throw new HttpMessageNotWritableException("Could not write YAML: " + ex.getMessage(), ex);
+        }
+    }
+
+    private void writeInternal(Object object, Writer writer) {
+        TRANSLATOR.toYaml(writer, object);
+    }
+
+    private Object readResolved(Type resolvedType, HttpInputMessage inputMessage) throws IOException {
+        try (var reader = getReader(inputMessage)) {
+            return readInternal(resolvedType, reader);
+        } catch (Exception ex) {
+            throw new HttpMessageNotReadableException("Could not read YAML: " + ex.getMessage(), ex, inputMessage);
+        }
+    }
+
+    private static Reader getReader(HttpInputMessage inputMessage) throws IOException {
+        return new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders()));
+    }
+
+    private static Writer getWriter(HttpOutputMessage outputMessage) throws IOException {
+        return new OutputStreamWriter(outputMessage.getBody(), getCharset(outputMessage.getHeaders()));
+    }
+
+    private static Charset getCharset(HttpHeaders headers) {
+        MediaType contentType = headers.getContentType();
+        Charset charset = (contentType == null ? null : contentType.getCharset());
+        return (charset != null ? charset : DEFAULT_CHARSET);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/Coder.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/Coder.java
new file mode 100644 (file)
index 0000000..3049a5c
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019-2020 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.coder;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+
+/**
+ * JSON encoder and decoder.
+ */
+public interface Coder {
+
+    /**
+     * Converts an object/POJO to an object of the given type.
+     *
+     * @param <T> desired type
+     * @param source source object
+     * @param clazz class of the desired object type
+     * @return the converted object
+     * @throws CoderException if an error occurs
+     */
+    default <S, T> T convert(S source, Class<T> clazz) throws CoderException {
+        if (source == null) {
+            return null;
+
+        } else if (clazz == source.getClass()) {
+            // same class - just cast it
+            return clazz.cast(source);
+
+        } else if (clazz == String.class) {
+            // target is a string - just encode the source
+            return (clazz.cast(encode(source)));
+
+        } else if (source.getClass() == String.class) {
+            // source is a string - just decode it
+            return decode(source.toString(), clazz);
+
+        } else {
+            // do it the long way: encode to a string and then decode the string
+            return decode(encode(source), clazz);
+        }
+    }
+
+    /**
+     * Encodes an object into json.
+     *
+     * @param object object to be encoded
+     * @return a json string representing the object
+     * @throws CoderException if an error occurs
+     */
+    String encode(Object object) throws CoderException;
+
+    /**
+     * Encodes an object into json, optionally making it "pretty".
+     *
+     * @param object object to be encoded
+     * @param pretty {@code true} if it should be encoded as "pretty" json, {@code false}
+     *        otherwise
+     * @return a json string representing the object
+     * @throws CoderException if an error occurs
+     */
+    String encode(Object object, boolean pretty) throws CoderException;
+
+    /**
+     * Encodes an object into json, writing to the given target.
+     *
+     * @param target target to which to write the encoded json
+     * @param object object to be encoded
+     * @throws CoderException if an error occurs
+     */
+    void encode(Writer target, Object object) throws CoderException;
+
+    /**
+     * Encodes an object into json, writing to the given target.
+     *
+     * @param target target to which to write the encoded json
+     * @param object object to be encoded
+     * @throws CoderException if an error occurs
+     */
+    void encode(OutputStream target, Object object) throws CoderException;
+
+    /**
+     * Encodes an object into json, writing to the given target.
+     *
+     * @param target target to which to write the encoded json
+     * @param object object to be encoded
+     * @throws CoderException if an error occurs
+     */
+    void encode(File target, Object object) throws CoderException;
+
+    /**
+     * Decodes json into an object.
+     *
+     * @param json json string to be decoded
+     * @param clazz class of object to be decoded
+     * @return the object represented by the given json string
+     * @throws CoderException if an error occurs
+     */
+    <T> T decode(String json, Class<T> clazz) throws CoderException;
+
+    /**
+     * Decodes json into an object, reading it from the given source.
+     *
+     * @param source source from which to read the json string to be decoded
+     * @param clazz class of object to be decoded
+     * @return the object represented by the given json string
+     * @throws CoderException if an error occurs
+     */
+    <T> T decode(Reader source, Class<T> clazz) throws CoderException;
+
+    /**
+     * Decodes json into an object, reading it from the given source.
+     *
+     * @param source source from which to read the json string to be decoded
+     * @param clazz class of object to be decoded
+     * @return the object represented by the given json string
+     * @throws CoderException if an error occurs
+     */
+    <T> T decode(InputStream source, Class<T> clazz) throws CoderException;
+
+    /**
+     * Decodes json into an object, reading it from the given source.
+     *
+     * @param source source from which to read the json string to be decoded
+     * @param clazz class of object to be decoded
+     * @return the object represented by the given json string
+     * @throws CoderException if an error occurs
+     */
+    <T> T decode(File source, Class<T> clazz) throws CoderException;
+
+    /**
+     * Converts an object/POJO to a standard object.
+     *
+     * @param object object to be converted
+     * @return a new standard object representing the original object
+     * @throws CoderException if an error occurs
+     */
+    StandardCoderObject toStandard(Object object) throws CoderException;
+
+    /**
+     * Converts a standard object to an object/POJO.
+     *
+     * @param sco the standard object to be converted
+     * @return a new object represented by the standard object
+     * @throws CoderException if an error occurs
+     */
+    <T> T fromStandard(StandardCoderObject sco, Class<T> clazz) throws CoderException;
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/CoderException.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/CoderException.java
new file mode 100644 (file)
index 0000000..8390d17
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * ============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.utils.coder;
+
+import java.io.Serial;
+
+/**
+ * Exceptions generated by coders.
+ */
+public class CoderException extends Exception {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    public CoderException() {
+        super();
+    }
+
+    public CoderException(String message) {
+        super(message);
+    }
+
+    public CoderException(Throwable cause) {
+        super(cause);
+    }
+
+    public CoderException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoder.java
new file mode 100644 (file)
index 0000000..834a850
--- /dev/null
@@ -0,0 +1,393 @@
+/*
+ * ============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.utils.coder;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import org.onap.policy.common.gson.DoubleConverter;
+import org.onap.policy.common.gson.GsonMessageBodyHandler;
+
+/**
+ * JSON encoder and decoder using the "standard" mechanism, which is currently gson.
+ */
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+public class StandardCoder implements Coder {
+
+    /**
+     * Gson object used to encode and decode messages.
+     */
+    private static final Gson GSON_STD;
+
+    /**
+     * Gson object used to encode messages in "pretty" format.
+     */
+    private static final Gson GSON_STD_PRETTY;
+
+    static {
+        GsonBuilder builder = GsonMessageBodyHandler.configBuilder(
+                        new GsonBuilder().registerTypeAdapter(StandardCoderObject.class, new StandardTypeAdapter()));
+
+        GSON_STD = builder.create();
+        GSON_STD_PRETTY = builder.setPrettyPrinting().create();
+    }
+
+    /**
+     * Gson object used to encode and decode messages.
+     */
+    protected final Gson gson;
+
+    /**
+     * Gson object used to encode messages in "pretty" format.
+     */
+    protected final Gson gsonPretty;
+
+    /**
+     * Constructs the object.
+     */
+    public StandardCoder() {
+        this(GSON_STD, GSON_STD_PRETTY);
+    }
+
+    @Override
+    public <S, T> T convert(S source, Class<T> clazz) throws CoderException {
+        if (source == null) {
+            return null;
+
+        } else if (clazz == source.getClass()) {
+            // same class - just cast it
+            return clazz.cast(source);
+
+        } else if (clazz == String.class) {
+            // target is a string - just encode the source
+            return (clazz.cast(encode(source)));
+
+        } else if (source.getClass() == String.class) {
+            // source is a string - just decode it
+            return decode(source.toString(), clazz);
+
+        } else {
+            /*
+             * Do it the long way: encode to a tree and then decode the tree. This entire
+             * method could have been left out and the default Coder.convert() used
+             * instead, but this should perform slightly better as it only uses a
+             * JsonElement as the intermediate data structure, while Coder.convert() goes
+             * all the way to a String as the intermediate data structure.
+             */
+            try {
+                return fromJson(toJsonTree(source), clazz);
+            } catch (RuntimeException e) {
+                throw new CoderException(e);
+            }
+        }
+    }
+
+    @Override
+    public String encode(Object object) throws CoderException {
+        return encode(object, false);
+    }
+
+    @Override
+    public String encode(Object object, boolean pretty) throws CoderException {
+        try {
+            if (pretty) {
+                return toPrettyJson(object);
+
+            } else {
+                return toJson(object);
+            }
+
+        } catch (RuntimeException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    @Override
+    public void encode(Writer target, Object object) throws CoderException {
+        try {
+            toJson(target, object);
+
+        } catch (RuntimeException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    @Override
+    public void encode(OutputStream target, Object object) throws CoderException {
+        try {
+            var wtr = makeWriter(target);
+            toJson(wtr, object);
+
+            // flush, but don't close
+            wtr.flush();
+
+        } catch (RuntimeException | IOException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    @Override
+    public void encode(File target, Object object) throws CoderException {
+        try (var wtr = makeWriter(target)) {
+            toJson(wtr, object);
+
+            // no need to flush or close here
+
+        } catch (RuntimeException | IOException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    @Override
+    public <T> T decode(String json, Class<T> clazz) throws CoderException {
+        try {
+            return fromJson(json, clazz);
+        } catch (RuntimeException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    @Override
+    public <T> T decode(Reader source, Class<T> clazz) throws CoderException {
+        try {
+            return fromJson(source, clazz);
+
+        } catch (RuntimeException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    @Override
+    public <T> T decode(InputStream source, Class<T> clazz) throws CoderException {
+        try {
+            return fromJson(makeReader(source), clazz);
+
+        } catch (RuntimeException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    @Override
+    public <T> T decode(File source, Class<T> clazz) throws CoderException {
+        try (var input = makeReader(source)) {
+            return fromJson(input, clazz);
+
+        } catch (RuntimeException | IOException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    /**
+     * Encodes the object as "pretty" json.
+     *
+     * @param object object to be encoded
+     * @return the encoded object
+     */
+    protected String toPrettyJson(Object object) {
+        return gsonPretty.toJson(object);
+    }
+
+    @Override
+    public StandardCoderObject toStandard(Object object) throws CoderException {
+        try {
+            return new StandardCoderObject(gson.toJsonTree(object));
+
+        } catch (RuntimeException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    @Override
+    public <T> T fromStandard(StandardCoderObject sco, Class<T> clazz) throws CoderException {
+        try {
+            return gson.fromJson(sco.getData(), clazz);
+
+        } catch (RuntimeException e) {
+            throw new CoderException(e);
+        }
+    }
+
+    // the remaining methods are wrappers that can be overridden by junit tests
+
+    /**
+     * Makes a writer for the given file.
+     *
+     * @param target file of interest
+     * @return a writer for the file
+     * @throws FileNotFoundException if the file cannot be created
+     */
+    protected Writer makeWriter(File target) throws FileNotFoundException {
+        return makeWriter(new FileOutputStream(target));
+    }
+
+    /**
+     * Makes a writer for the given stream.
+     *
+     * @param target stream of interest
+     * @return a writer for the stream
+     */
+    protected Writer makeWriter(OutputStream target) {
+        return new OutputStreamWriter(target, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Makes a reader for the given file.
+     *
+     * @param source file of interest
+     * @return a reader for the file
+     * @throws FileNotFoundException if the file does not exist
+     */
+    protected Reader makeReader(File source) throws FileNotFoundException {
+        return makeReader(new FileInputStream(source));
+    }
+
+    /**
+     * Makes a reader for the given stream.
+     *
+     * @param source stream of interest
+     * @return a reader for the stream
+     */
+    protected Reader makeReader(InputStream source) {
+        return new InputStreamReader(source, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Encodes an object into a json tree, without catching exceptions.
+     *
+     * @param object object to be encoded
+     * @return a json element representing the object
+     */
+    protected JsonElement toJsonTree(Object object) {
+        return gson.toJsonTree(object);
+    }
+
+    /**
+     * Encodes an object into json, without catching exceptions.
+     *
+     * @param object object to be encoded
+     * @return a json string representing the object
+     */
+    protected String toJson(Object object) {
+        return gson.toJson(object);
+    }
+
+    /**
+     * Encodes an object into json, without catching exceptions.
+     *
+     * @param target target to which to write the encoded json
+     * @param object object to be encoded
+     */
+    protected void toJson(Writer target, Object object) {
+        gson.toJson(object, object.getClass(), target);
+    }
+
+    /**
+     * Decodes a json element into an object, without catching exceptions.
+     *
+     * @param json json element to be decoded
+     * @param clazz class of object to be decoded
+     * @return the object represented by the given json element
+     */
+    protected <T> T fromJson(JsonElement json, Class<T> clazz) {
+        return convertFromDouble(clazz, gson.fromJson(json, clazz));
+    }
+
+    /**
+     * Decodes a json string into an object, without catching exceptions.
+     *
+     * @param json json string to be decoded
+     * @param clazz class of object to be decoded
+     * @return the object represented by the given json string
+     */
+    protected <T> T fromJson(String json, Class<T> clazz) {
+        return convertFromDouble(clazz, gson.fromJson(json, clazz));
+    }
+
+    /**
+     * Decodes a json string into an object, without catching exceptions.
+     *
+     * @param source source from which to read the json string to be decoded
+     * @param clazz class of object to be decoded
+     * @return the object represented by the given json string
+     */
+    protected <T> T fromJson(Reader source, Class<T> clazz) {
+        return convertFromDouble(clazz, gson.fromJson(source, clazz));
+    }
+
+    /**
+     * Converts a value from Double to Integer/Long, walking the value's contents if it's
+     * a List/Map. Only applies if the specified class refers to the Object class.
+     * Otherwise, it leaves the value unchanged.
+     *
+     * @param clazz class of object to be decoded
+     * @param value value to be converted
+     * @return the converted value
+     */
+    protected <T> T convertFromDouble(Class<T> clazz, T value) {
+        if (clazz != Object.class && !Map.class.isAssignableFrom(clazz) && !List.class.isAssignableFrom(clazz)) {
+            return value;
+        }
+
+        return clazz.cast(DoubleConverter.convertFromDouble(value));
+    }
+
+    /**
+     * Adapter for standard objects.
+     */
+    @AllArgsConstructor
+    protected static class StandardTypeAdapter extends TypeAdapter<StandardCoderObject> {
+
+        /**
+         * Used to read/write a JsonElement.
+         */
+        private static final TypeAdapter<JsonElement> elementAdapter = new Gson().getAdapter(JsonElement.class);
+
+        @Override
+        public void write(JsonWriter out, StandardCoderObject value) throws IOException {
+            elementAdapter.write(out, value.getData());
+        }
+
+        @Override
+        public StandardCoderObject read(JsonReader in) throws IOException {
+            return new StandardCoderObject(elementAdapter.read(in));
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoderObject.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardCoderObject.java
new file mode 100644 (file)
index 0000000..5402f37
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * ============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.utils.coder;
+
+import com.google.gson.JsonElement;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * Object type used by the {@link StandardCoder}. Different serialization tools have
+ * different "standard objects". For instance, GSON uses {@link JsonElement}. This class
+ * wraps that object so that it can be used without exposing the object, itself.
+ */
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+public class StandardCoderObject implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Data wrapped by this.
+     */
+    /*
+     * this should not be transient, but since it isn't serializable, we're stuck with it
+     * until there's time to address the issue
+     */
+    @Getter(AccessLevel.PROTECTED)
+    private final transient JsonElement data;
+
+    /**
+     * Constructs the object.
+     */
+    public StandardCoderObject() {
+        data = null;
+    }
+
+    /**
+     * Gets a field's value from this object, traversing the object hierarchy.
+     *
+     * @param fields field hierarchy. These may be strings, identifying fields within the
+     *               object, or Integers, identifying an index within an array
+     * @return the field value or {@code null} if the field does not exist or is not a primitive
+     */
+    public String getString(Object... fields) {
+
+        JsonElement jel = data;
+
+        for (Object field : fields) {
+            if (jel == null) {
+                return null;
+            }
+
+            if (field instanceof String) {
+                jel = getFieldFromObject(jel, field.toString());
+
+            } else if (field instanceof Integer) {
+                jel = getItemFromArray(jel, (int) field);
+
+            } else {
+                throw new IllegalArgumentException("subscript is not a string or integer: " + field);
+            }
+        }
+
+        return (jel != null && jel.isJsonPrimitive() ? jel.getAsString() : null);
+    }
+
+    /**
+     * Gets an item from an object.
+     *
+     * @param element object from which to extract the item
+     * @param field   name of the field from which to extract the item
+     * @return the item, or {@code null} if the element is not an object or if the field does not exist
+     */
+    protected JsonElement getFieldFromObject(JsonElement element, String field) {
+        if (!element.isJsonObject()) {
+            return null;
+        }
+
+        return element.getAsJsonObject().get(field);
+    }
+
+    /**
+     * Gets an item from an array.
+     *
+     * @param element array from which to extract the item
+     * @param index   index of the item to extract
+     * @return the item, or {@code null} if the element is not an array or if the index is out of bounds
+     */
+    protected JsonElement getItemFromArray(JsonElement element, int index) {
+        if (index < 0) {
+            throw new IllegalArgumentException("subscript is invalid: " + index);
+        }
+
+        if (!element.isJsonArray()) {
+            return null;
+        }
+
+        var array = element.getAsJsonArray();
+
+        if (index >= array.size()) {
+            return null;
+        }
+
+        return array.get(index);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/StandardYamlCoder.java
new file mode 100644 (file)
index 0000000..8ee2e81
--- /dev/null
@@ -0,0 +1,71 @@
+/*-
+ * ============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.utils.coder;
+
+import java.io.Reader;
+import java.io.Writer;
+
+/**
+ * YAML encoder and decoder using the "standard" mechanism, which is currently gson.
+ * All the methods perform conversion to/from YAML (instead of JSON).
+ */
+public class StandardYamlCoder extends StandardCoder {
+    private final YamlJsonTranslator translator;
+
+    /**
+     * Constructs the object.
+     */
+    public StandardYamlCoder() {
+        translator = new YamlJsonTranslator(gson) {
+            @Override
+            protected <T> T convertFromDouble(Class<T> clazz, T value) {
+                return StandardYamlCoder.this.convertFromDouble(clazz, value);
+            }
+        };
+    }
+
+    @Override
+    protected String toPrettyJson(Object object) {
+        // YAML is already "pretty"
+        return toJson(object);
+    }
+
+    @Override
+    protected String toJson(Object object) {
+        return translator.toYaml(object);
+    }
+
+    @Override
+    protected void toJson(Writer target, Object object) {
+        translator.toYaml(target, object);
+    }
+
+    @Override
+    protected <T> T fromJson(String yaml, Class<T> clazz) {
+        return translator.fromYaml(yaml, clazz);
+    }
+
+    @Override
+    protected <T> T fromJson(Reader source, Class<T> clazz) {
+        return translator.fromYaml(source, clazz);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/coder/YamlJsonTranslator.java b/policy-common/src/main/java/org/onap/policy/common/utils/coder/YamlJsonTranslator.java
new file mode 100644 (file)
index 0000000..ffd9d05
--- /dev/null
@@ -0,0 +1,340 @@
+/*-
+ * ============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.utils.coder;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import lombok.AllArgsConstructor;
+import org.onap.policy.common.gson.InstantTypeAdapter;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.emitter.Emitter;
+import org.yaml.snakeyaml.error.YAMLException;
+import org.yaml.snakeyaml.nodes.MappingNode;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.NodeTuple;
+import org.yaml.snakeyaml.nodes.ScalarNode;
+import org.yaml.snakeyaml.nodes.SequenceNode;
+import org.yaml.snakeyaml.nodes.Tag;
+import org.yaml.snakeyaml.resolver.Resolver;
+import org.yaml.snakeyaml.serializer.Serializer;
+
+/**
+ * YAML-JSON translator. The methods may throw either of the runtime exceptions,
+ * YAMLException or JsonSyntaxException.
+ * <p/>
+ * Note: if the invoker wishes Double to be converted to Integer/Long when type
+ * Object.class is requested, then a Gson object must be used that will perform the
+ * translation. In addition, the {@link #convertFromDouble(Class, Object)} method should
+ * be overridden with an appropriate conversion method.
+ */
+@AllArgsConstructor
+public class YamlJsonTranslator {
+
+    /**
+     * Object to be used to translate between YAML and JsonElement.
+     */
+    private final Gson gson;
+
+    /**
+     * Constructs the object.
+     */
+    public YamlJsonTranslator() {
+        GsonBuilder builder = new GsonBuilder();
+        builder.registerTypeAdapter(Instant.class, new InstantTypeAdapter());
+        gson = builder.create();
+    }
+
+    /**
+     * Translates a POJO into a YAML String.
+     *
+     * @param object POJO to be translated
+     * @return YAML representing the original object
+     */
+    public String toYaml(Object object) {
+        var output = new StringWriter();
+        toYaml(output, object);
+        return output.toString();
+    }
+
+    /**
+     * Serializes a POJO to a writer, as YAML.
+     *
+     * @param target target writer
+     * @param object POJO to be translated
+     */
+    public void toYaml(Writer target, Object object) {
+        var dumper = new DumperOptions();
+        var serializer = new Serializer(new Emitter(target, dumper), new Resolver(), dumper, null);
+
+        try {
+            serializer.open();
+            serializer.serialize(makeYaml(toJsonTree(object)));
+            serializer.close();
+
+        } catch (IOException e) {
+            throw new YAMLException(e);
+        }
+    }
+
+    /**
+     * Translates a POJO into a JsonElement.
+     *
+     * @param object POJO to be translated
+     * @return a JsonElement representing the original object
+     */
+    protected JsonElement toJsonTree(Object object) {
+        return gson.toJsonTree(object);
+    }
+
+    /**
+     * Translates a YAML string to a POJO.
+     *
+     * @param yaml YAML string to be translated
+     * @param clazz class of POJO to be created
+     * @return a POJO representing the original YAML
+     */
+    public <T> T fromYaml(String yaml, Class<T> clazz) {
+        return fromYaml(new StringReader(yaml), clazz);
+    }
+
+    /**
+     * Translates a YAML string, read from a reader, into a POJO.
+     *
+     * @param source source of the YAML string to be translated
+     * @param clazz class of POJO to be created
+     * @return a POJO representing the YAML read from the reader
+     */
+    public <T> T fromYaml(Reader source, Class<T> clazz) {
+        var node = new Yaml().compose(source);
+        return fromJson(makeJson(node), clazz);
+    }
+
+    /**
+     * Translates a JsonElement to a POJO of the given class.
+     *
+     * @param jel element to be translated
+     * @param clazz class of POJO to be created
+     * @return a POJO representing the original element
+     */
+    protected <T> T fromJson(JsonElement jel, Class<T> clazz) {
+        return convertFromDouble(clazz, gson.fromJson(jel, clazz));
+    }
+
+    /**
+     * Converts a value from Double to Integer/Long, walking the value's contents if it's
+     * a List/Map. Only applies if the specified class refers to the Object class.
+     * Otherwise, it leaves the value unchanged.
+     * <p/>
+     * The default method simply returns the original value.
+     *
+     * @param clazz class of object to be decoded
+     * @param value value to be converted
+     * @return the converted value
+     */
+    protected <T> T convertFromDouble(Class<T> clazz, T value) {
+        return value;
+    }
+
+    /**
+     * Converts an arbitrary gson element into a corresponding Yaml node.
+     *
+     * @param jel gson element to be converted
+     * @return a yaml node corresponding to the element
+     */
+    protected Node makeYaml(JsonElement jel) {
+        if (jel.isJsonArray()) {
+            return makeYamlSequence((JsonArray) jel);
+
+        } else if (jel.isJsonObject()) {
+            return makeYamlMap((JsonObject) jel);
+
+        } else if (jel.isJsonPrimitive()) {
+            return makeYamlPrim((JsonPrimitive) jel);
+
+        } else {
+            return new ScalarNode(Tag.NULL, "", null, null, DumperOptions.ScalarStyle.PLAIN);
+        }
+    }
+
+    /**
+     * Converts an arbitrary gson array into a corresponding Yaml sequence.
+     *
+     * @param jel gson element to be converted
+     * @return a yaml node corresponding to the element
+     */
+    protected SequenceNode makeYamlSequence(JsonArray jel) {
+        List<Node> nodes = new ArrayList<>(jel.size());
+        jel.forEach(item -> nodes.add(makeYaml(item)));
+
+        return new SequenceNode(Tag.SEQ, true, nodes, null, null, DumperOptions.FlowStyle.AUTO);
+    }
+
+    /**
+     * Converts an arbitrary gson object into a corresponding Yaml map.
+     *
+     * @param jel gson element to be converted
+     * @return a yaml node corresponding to the element
+     */
+    protected MappingNode makeYamlMap(JsonObject jel) {
+        List<NodeTuple> nodes = new ArrayList<>(jel.size());
+
+        for (Entry<String, JsonElement> entry : jel.entrySet()) {
+            Node key = new ScalarNode(Tag.STR, entry.getKey(), null, null, DumperOptions.ScalarStyle.PLAIN);
+            Node value = makeYaml(entry.getValue());
+
+            nodes.add(new NodeTuple(key, value));
+        }
+
+        return new MappingNode(Tag.MAP, true, nodes, null, null, DumperOptions.FlowStyle.AUTO);
+    }
+
+    /**
+     * Converts an arbitrary gson primitive into a corresponding Yaml scalar.
+     *
+     * @param jel gson element to be converted
+     * @return a yaml node corresponding to the element
+     */
+    protected ScalarNode makeYamlPrim(JsonPrimitive jel) {
+        Tag tag;
+        if (jel.isNumber()) {
+            Class<? extends Number> clazz = jel.getAsNumber().getClass();
+
+            if (clazz == Double.class || clazz == Float.class) {
+                tag = Tag.FLOAT;
+
+            } else {
+                tag = Tag.INT;
+            }
+
+        } else if (jel.isBoolean()) {
+            tag = Tag.BOOL;
+
+        } else {
+            // treat anything else as a string
+            tag = Tag.STR;
+        }
+
+        return new ScalarNode(tag, jel.getAsString(), null, null, DumperOptions.ScalarStyle.PLAIN);
+    }
+
+    /**
+     * Converts an arbitrary Yaml node into a corresponding gson element.
+     *
+     * @param node node to be converted
+     * @return a gson element corresponding to the node
+     */
+    protected JsonElement makeJson(Node node) {
+        if (node instanceof MappingNode mappingNode) {
+            return makeJsonObject(mappingNode);
+
+        } else if (node instanceof SequenceNode sequenceNode) {
+            return makeJsonArray(sequenceNode);
+
+        } else {
+            return makeJsonPrim((ScalarNode) node);
+        }
+
+        // yaml doesn't appear to use anchor nodes when decoding so ignore them for now
+    }
+
+    /**
+     * Converts a Yaml sequence into a corresponding gson array.
+     *
+     * @param node node to be converted
+     * @return a gson element corresponding to the node
+     */
+    protected JsonArray makeJsonArray(SequenceNode node) {
+        List<Node> nodes = node.getValue();
+
+        var array = new JsonArray(nodes.size());
+        nodes.forEach(subnode -> array.add(makeJson(subnode)));
+
+        return array;
+    }
+
+    /**
+     * Converts a Yaml map into a corresponding gson object.
+     *
+     * @param node node to be converted
+     * @return a gson element corresponding to the node
+     */
+    protected JsonObject makeJsonObject(MappingNode node) {
+        var obj = new JsonObject();
+
+        for (NodeTuple tuple : node.getValue()) {
+            var key = tuple.getKeyNode();
+            String skey = ((ScalarNode) key).getValue();
+
+            obj.add(skey, makeJson(tuple.getValueNode()));
+        }
+
+        return obj;
+    }
+
+    /**
+     * Converts a Yaml scalar into a corresponding gson primitive.
+     *
+     * @param node node to be converted
+     * @return a gson element corresponding to the node
+     */
+    protected JsonElement makeJsonPrim(ScalarNode node) {
+        try {
+            var tag = node.getTag();
+
+            if (tag == Tag.INT) {
+                return new JsonPrimitive(Long.valueOf(node.getValue()));
+
+            } else if (tag == Tag.FLOAT) {
+                return new JsonPrimitive(Double.valueOf(node.getValue()));
+
+            } else if (tag == Tag.BOOL) {
+                return new JsonPrimitive(Boolean.valueOf(node.getValue()));
+
+            } else if (tag == Tag.NULL) {
+                return JsonNull.INSTANCE;
+
+            } else {
+                // treat anything else as a string
+                return new JsonPrimitive(node.getValue());
+            }
+
+        } catch (NumberFormatException ex) {
+            // just treat it as a string
+            return new JsonPrimitive(node.getValue());
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonSerializer.java b/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonSerializer.java
new file mode 100644 (file)
index 0000000..db9c1c7
--- /dev/null
@@ -0,0 +1,37 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * policy-management
+ * ================================================================================
+ * Copyright (C) 2017-2018 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.gson;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import java.io.IOException;
+
+/**
+ * Gson serializer, providing stub implementation of "read".
+ *
+ * @param <T> type of object that this serializes
+ */
+public abstract class GsonSerializer<T> extends TypeAdapter<T> {
+    @Override
+    public T read(JsonReader in) throws IOException {
+        throw new UnsupportedOperationException("read from pseudo TypeAdapter");
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtils.java b/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtils.java
new file mode 100644 (file)
index 0000000..1d131ae
--- /dev/null
@@ -0,0 +1,314 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * policy-management
+ * ================================================================================
+ * Copyright (C) 2017-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.utils.gson;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.re2j.Pattern;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.apache.commons.jexl3.JexlBuilder;
+import org.apache.commons.jexl3.JexlContext;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.MapContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utilities used to test encoding and decoding of Policy objects.
+ */
+@Getter
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+public class GsonTestUtils {
+
+    private static final Logger logger = LoggerFactory.getLogger(GsonTestUtils.class);
+
+    /**
+     * Matches script items, of the form ${xxx}, within text.
+     */
+    private static final Pattern SCRIPT_PAT = Pattern.compile("\\$\\{([^}]+)\\}");
+
+    /**
+     * Engine used to interpolate strings before they're compared.
+     */
+    private static JexlEngine engineInstance = null;
+
+    /**
+     * Used to encode and decode an object via gson.
+     */
+    private Gson gson;
+
+    /**
+     * Constructs the object.
+     */
+    public GsonTestUtils() {
+        GsonTestUtils other = new GsonTestUtilsBuilder().build();
+
+        gson = other.gson;
+    }
+
+    /**
+     * Serializes and then deserializes an object using gson.
+     *
+     * @param object the object to be serialized
+     * @param clazz the class of object to deserialize
+     * @return the deserialized object
+     */
+    public <T> T gsonRoundTrip(T object, Class<T> clazz) {
+        String sgson = gsonEncode(object);
+        return gson.fromJson(sgson, clazz);
+    }
+
+    /**
+     * Encodes an object using gson and then compares it to the expected value, after
+     * sorting the elements. The class name is used to find the json file, whose contents
+     * is interpolated (i.e., script elements, of the form ${obj.xxx}, are expanded).
+     *
+     * @param object the object to be encoded
+     * @param expected the expected value
+     */
+    public void compareGson(Object object, Class<?> expected) {
+        compareGson(object, new File(expected.getSimpleName() + ".json"));
+    }
+
+    /**
+     * Encodes an object using gson and then compares it to the expected value, after
+     * sorting the elements. The content of the file is interpolated (i.e., script
+     * elements, of the form ${obj.xxx}, are expanded).
+     *
+     * @param object the object to be encoded
+     * @param expected the expected value
+     */
+    public void compareGson(Object object, File expected) {
+        // file is not required to have a full path - find it via getResource()
+        var url = object.getClass().getResource(expected.getName());
+        if (url == null) {
+            throw new JsonParseException(new FileNotFoundException(expected.getName()));
+        }
+
+        String expectedText;
+        try {
+            expectedText = readFile(new File(url.getFile()));
+
+        } catch (IOException e) {
+            throw new JsonParseException("error reading: " + expected, e);
+        }
+
+        compareGson(object, expectedText);
+    }
+
+    /**
+     * Encodes an object using gson and then compares it to the expected value, after
+     * sorting the elements. The expected value is interpolated (i.e., script elements, of
+     * the form ${obj.xxx}, are expanded).
+     *
+     * @param object the object to be encoded
+     * @param expected the expected value
+     */
+    public void compareGson(Object object, String expected) {
+        String result = applyScripts(expected, object);
+        compareGson(object, gson.fromJson(result, JsonElement.class));
+    }
+
+    /**
+     * Encodes an object using gson and then compares it to the expected value, after
+     * sorting the elements.
+     *
+     * @param object the object to be encoded
+     * @param expected the expected value
+     */
+    public void compareGson(Object object, JsonElement expected) {
+        String sgson = gsonEncode(object);
+
+        JsonElement gsonjo = reorder(gson.fromJson(sgson, JsonElement.class));
+        JsonElement expjo = reorder(expected);
+
+        /*
+         * As this method is only used within junit tests, it is OK to use assert calls,
+         * thus sonar is disabled.
+         */
+        assertEquals(expjo.toString(), gsonjo.toString());      // NOSONAR
+    }
+
+    /**
+     * Reads the content of a file.
+     * @param file file to read
+     * @return the content of the file
+     * @throws IOException if an error occurs
+     */
+    protected String readFile(File file) throws IOException {
+        return Files.readString(file.toPath());
+    }
+
+
+    /**
+     * Interpolates script elements, of the form ${obj.xxx}, within some text. The script
+     * is evaluated via javascript, where "obj" references the object used by each script
+     * element.
+     *
+     * @param object object to be used by the script
+     * @param text text to be evaluated
+     * @return the text, after interpolating the script elements
+     */
+    public String applyScripts(String text, Object object) {
+        var mat = SCRIPT_PAT.matcher(text);
+        if (!mat.find()) {
+            // contains no script elements - just return it as is
+            return text;
+        }
+
+        // bind the object to the variable, "obj"
+        JexlEngine eng = getEngine();
+        JexlContext context = new MapContext();
+        context.set("obj", object);
+
+        // work our way through the text, interpolating script elements as we go
+        var bldr = new StringBuilder();
+        var ilast = 0;
+        mat.reset();
+        while (mat.find(ilast)) {
+            // append segment that appears between last match and this
+            int inext = mat.start();
+            bldr.append(text, ilast, inext);
+
+            // next match begins after the current match
+            ilast = mat.end();
+
+            // interpolate the script
+            String script = mat.group(1);
+            /*
+             * Note: must use "eng" instead of "engineInstance" to ensure that we use
+             * the same engine that's associated with the bindings.
+             */
+            Object result = eng.createExpression(script).evaluate(context);
+            bldr.append(result == null ? "null" : result.toString());
+        }
+
+        // append final segment
+        bldr.append(text.substring(ilast));
+
+        return bldr.toString();
+    }
+
+    /**
+     * Gets the script engine instance.
+     *
+     * @return the script engine
+     */
+    private static JexlEngine getEngine() {
+        if (engineInstance == null) {
+            // race condition here, but it's ok to overwrite with a new engine
+            engineInstance = new JexlBuilder().create();
+        }
+
+        return engineInstance;
+    }
+
+    /**
+     * Encodes an object using gson.
+     *
+     * @param object the object to be encoded
+     * @return the encoded object
+     */
+    public String gsonEncode(Object object) {
+        String sgson = gson.toJson(object);
+        logger.debug("gson={}", sgson);
+        return sgson;
+    }
+
+    /**
+     * Recursively re-orders a json object, arranging the keys alphabetically and removing
+     * null items.
+     *
+     * @param jsonObj object from which nulls are to be removed
+     * @return a new object, without the null items
+     */
+    public JsonObject reorder(JsonObject jsonObj) {
+        var newjo = new JsonObject();
+
+        // sort the keys before copying to the new object
+        List<Entry<String, JsonElement>> sortedSet = new ArrayList<>(jsonObj.entrySet());
+        sortedSet.sort(Entry.comparingByKey());
+
+        for (Entry<String, JsonElement> ent : sortedSet) {
+            JsonElement val = ent.getValue();
+            if (val.isJsonNull()) {
+                continue;
+            }
+
+            newjo.add(ent.getKey(), reorder(val));
+        }
+
+        return newjo;
+    }
+
+    /**
+     * Recursively re-orders a json array, arranging the keys alphabetically and removing
+     * null items.
+     *
+     * @param jsonArray array from which nulls are to be removed
+     * @return a new array, with null items removed from all elements
+     */
+    public JsonArray reorder(JsonArray jsonArray) {
+        var newarr = new JsonArray();
+        for (JsonElement ent : jsonArray) {
+            newarr.add(reorder(ent));
+        }
+
+        return newarr;
+    }
+
+    /**
+     * Recursively re-orders a json element, arranging the keys alphabetically and
+     * removing null items.
+     *
+     * @param jsonEl element from which nulls are to be removed
+     * @return a new element, with null items removed
+     */
+    public JsonElement reorder(JsonElement jsonEl) {
+        if (jsonEl == null) {
+            return null;
+
+        } else if (jsonEl.isJsonObject()) {
+            return reorder(jsonEl.getAsJsonObject());
+
+        } else if (jsonEl.isJsonArray()) {
+            return reorder(jsonEl.getAsJsonArray());
+
+        } else {
+            return jsonEl;
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilder.java b/policy-common/src/main/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilder.java
new file mode 100644 (file)
index 0000000..dcf2775
--- /dev/null
@@ -0,0 +1,61 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * policy-management
+ * ================================================================================
+ * Copyright (C) 2017-2018 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.gson;
+
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapterFactory;
+import org.onap.policy.common.gson.JacksonHandler;
+
+/**
+ * Used to builder a utility class.
+ */
+public class GsonTestUtilsBuilder {
+    private final GsonBuilder gsonBldr;
+
+    /**
+     * Constructs the object.
+     */
+    public GsonTestUtilsBuilder() {
+        gsonBldr = new GsonBuilder();
+
+        // register jackson behaviors with the builder
+        JacksonHandler.configBuilder(gsonBldr);
+    }
+
+    /**
+     * Builds the utility.
+     *
+     * @return a new utility
+     */
+    public GsonTestUtils build() {
+        return new GsonTestUtils(gsonBldr.create());
+    }
+
+    /**
+     * Adds gson support for serializing a mock of a class.
+     *
+     * @param clazz mocked class to be supported
+     * @param sgson gson serializer
+     */
+    protected <T> void addMock(Class<T> clazz, TypeAdapterFactory sgson) {
+        gsonBldr.registerTypeAdapterFactory(sgson);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/network/NetworkUtil.java b/policy-common/src/main/java/org/onap/policy/common/utils/network/NetworkUtil.java
new file mode 100644 (file)
index 0000000..6698d7c
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2017-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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.network;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.UUID;
+import javax.net.ssl.TrustManager;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.commons.net.util.TrustManagerUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Network Utilities.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class NetworkUtil {
+
+    public static final Logger logger = LoggerFactory.getLogger(NetworkUtil.class.getName());
+
+    /**
+     * IPv4 Wildcard IP address.
+     */
+    public static final String IPV4_WILDCARD_ADDRESS = "0.0.0.0";
+
+
+    /**
+     * A trust manager that always trusts certificates.
+     */
+    private static final TrustManager[] ALWAYS_TRUST_MANAGER = { TrustManagerUtils.getAcceptAllTrustManager() };
+
+    /**
+     * Allocates an available port on which a server may listen.
+     *
+     * @return an available port
+     * @throws IOException if a socket cannot be created
+     */
+    public static int allocPort() throws IOException {
+        return allocPort((InetSocketAddress) null);
+    }
+
+    /**
+     * Allocates an available port on which a server may listen.
+     *
+     * @param hostName the server's host name
+     * @return an available port
+     * @throws IOException if a socket cannot be created
+     */
+    public static int allocPort(String hostName) throws IOException {
+        return allocPort(new InetSocketAddress(hostName, 0));
+    }
+
+    /**
+     * Allocates an available port on which a server may listen.
+     *
+     * @param hostAddr the server's host address on which to listen
+     * @return an available port
+     * @throws IOException if a socket cannot be created
+     */
+    public static int allocPort(InetSocketAddress hostAddr) throws IOException {
+        /*
+         * The socket is only used to find an unused address for a new server. As a
+         * result, it poses no security risk, thus the sonar issue can be ignored.
+         */
+        try (ServerSocket socket = new ServerSocket()) {    // NOSONAR
+            socket.bind(hostAddr);
+
+            return socket.getLocalPort();
+        }
+    }
+
+    /**
+     * Gets a trust manager that accepts all certificates.
+     *
+     * @return a trust manager that accepts all certificates
+     */
+    public static TrustManager[] getAlwaysTrustingManager() {
+        return ALWAYS_TRUST_MANAGER;
+    }
+
+    /**
+     * try to connect to $host:$port $retries times while we are getting connection failures.
+     *
+     * @param host host
+     * @param port port
+     * @param retries number of attempts
+     * @return true is port is open, false otherwise
+     * @throws InterruptedException if execution has been interrupted
+     */
+    public static boolean isTcpPortOpen(String host, int port, int retries, long interval)
+            throws InterruptedException {
+        var retry = 0;
+        while (retry < retries) {
+            /*
+             * As with the server socket, this is only used to see if the port is open,
+             * thus the sonar issue can be ignored.
+             */
+            try (Socket s = new Socket(host, port)) {   // NOSONAR
+                logger.debug("{}:{} connected - retries={} interval={}", host, port, retries, interval);
+                return true;
+            } catch (final IOException e) {
+                retry++;
+                logger.trace("{}:{} connected - retries={} interval={}", host, port, retries, interval, e);
+                Thread.sleep(interval);
+            }
+        }
+
+        logger.warn("{}:{} closed = retries={} interval={}", host, port, retries, interval);
+        return false;
+    }
+
+    /**
+     * Gets host name.
+     *
+     * @return host name
+     */
+    public static String getHostname() {
+
+        String hostname = System.getenv("HOSTNAME");
+        if (hostname != null && !hostname.isEmpty()) {
+            return hostname;
+        }
+
+        try {
+            return InetAddress.getLocalHost().getHostName();
+        } catch (UnknownHostException e) {
+            logger.warn("cannot resolve local hostname", e);
+            /* continue */
+        }
+
+        return "localhost";
+    }
+
+    /**
+     * Gets host's IP.
+     *
+     * @return host IP
+     */
+    public static String getHostIp() {
+
+        try {
+            return InetAddress.getLocalHost().getHostAddress();
+        } catch (UnknownHostException e) {
+            logger.warn("cannot resolve local hostname", e);
+            /* continue */
+        }
+
+        return "127.0.0.1";
+    }
+
+    /**
+     * Generates a globally unique name, typically for use in PDP messages, to uniquely
+     * identify a PDP (or PAP), regardless on what cluster it resides.
+     *
+     * @param prefix text to be prepended to the generated value
+     * @return a globally unique name
+     */
+    public static String genUniqueName(String prefix) {
+        return prefix + "-" + UUID.randomUUID();
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/properties/PropertyUtils.java b/policy-common/src/main/java/org/onap/policy/common/utils/properties/PropertyUtils.java
new file mode 100644 (file)
index 0000000..7155323
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.properties;
+
+import java.util.Properties;
+import lombok.AllArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Utilities for extracting property values and converting them to other types.
+ */
+@AllArgsConstructor
+public class PropertyUtils {
+    /**
+     * Properties on which to work.
+     */
+    private Properties properties;
+
+    /**
+     * Prefix to prepend to property names.
+     */
+    private String prefix;
+
+    /**
+     * Function to invoke if a property value is invalid.
+     */
+    private TriConsumer<String, String, Exception> invalidHandler;
+
+    /**
+     * Gets a string property.
+     *
+     * @param propName name of the property whose value is to be retrieved
+     * @param defaultValue value to use if the property value is empty or does not exist
+     * @return the property's value
+     */
+    public String getString(String propName, String defaultValue) {
+        String propValue = getProperty(propName);
+        return (StringUtils.isBlank(propValue) ? defaultValue : propValue);
+    }
+
+    /**
+     * Gets a boolean property.
+     *
+     * @param propName name of the property whose value is to be retrieved
+     * @param defaultValue value to use if the property value is empty or does not exist
+     * @return the property's value
+     */
+    public boolean getBoolean(String propName, boolean defaultValue) {
+        String propValue = getProperty(propName);
+
+        if (!StringUtils.isBlank(propValue)) {
+            return Boolean.parseBoolean(propValue);
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Gets an integer property.
+     *
+     * @param propName name of the property whose value is to be retrieved
+     * @param defaultValue value to use if the property value is empty or does not exist
+     * @return the property's value
+     */
+    public int getInteger(String propName, int defaultValue) {
+        String propValue = getProperty(propName);
+
+        if (!StringUtils.isBlank(propValue)) {
+            try {
+                return Integer.parseInt(propValue);
+
+            } catch (NumberFormatException nfe) {
+                invalidHandler.accept(getFullName(propName), propValue, nfe);
+            }
+        }
+
+        return defaultValue;
+    }
+
+
+    /**
+     * Gets a property's value.
+     *
+     * @param propName name of the property whose value is to be retrieved
+     * @return the property's value, or {@code null} if it does not exist
+     */
+    private String getProperty(String propName) {
+        return properties.getProperty(getFullName(propName));
+    }
+
+    /**
+     * Gets the full property name, with the prefix prepended.
+     *
+     * @param propName property name, without the prefix
+     * @return the full property name
+     */
+    private String getFullName(String propName) {
+        return prefix + propName;
+    }
+
+    @FunctionalInterface
+    public static interface TriConsumer<A, B, C> {
+        public void accept(A propName, B propValue, C exception);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/resources/ResourceUtils.java b/policy-common/src/main/java/org/onap/policy/common/utils/resources/ResourceUtils.java
new file mode 100644 (file)
index 0000000..3ee062f
--- /dev/null
@@ -0,0 +1,308 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2018 Ericsson. All rights reserved.
+ *  Modifications Copyright (C) 2020, 2023 Nordix Foundation.
+ *  Modifications Copyright (C) 2020-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.utils.resources;
+
+import com.google.re2j.Pattern;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is common utility class with static methods for handling Java resources on the class path. It is an abstract
+ * class to prevent any direct instantiation and private constructor to prevent extending this class.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class ResourceUtils {
+    // Get a reference to the logger
+    private static final Logger LOGGER = LoggerFactory.getLogger(ResourceUtils.class);
+
+    private static final Pattern SLASH_PAT = Pattern.compile("/");
+
+    // Resource types
+    private static final String FILE_PROTOCOL = "file";
+    private static final String JAR_PROTOCOL = "jar";
+
+    /**
+     * Method to resolve a resource; the local file system is checked first and then the class path is checked.
+     *
+     * @param resourceName The resource name
+     * @return A URL to a resource
+     */
+    public static URL getUrl4Resource(final String resourceName) {
+        // Check the local fine system first
+        final var urlToResource = getLocalFile(resourceName);
+
+        // Check if this is a local file
+        if (urlToResource != null) {
+            return urlToResource;
+        } else {
+            // Resort to the class path
+            return getUrlResource(resourceName);
+        }
+    }
+
+    /**
+     * Method to return a resource as a string. The resource can be on the local file system or in the class path. The
+     * resource is resolved and loaded into a string.
+     *
+     * @param resourceName The resource name
+     * @return A string containing the resource
+     */
+    public static String getResourceAsString(final String resourceName) {
+        // Get the resource as a stream, we'll convert it to a string then
+        // Read the stream contents, closing when done
+        try (var resourceStream = getResourceAsStream(resourceName)) {
+            if (resourceStream == null) {
+                return null;
+            }
+            return IOUtils.toString(resourceStream, StandardCharsets.UTF_8);
+        } catch (final IOException e) {
+            LOGGER.debug("error reading resource stream {}", resourceName, e);
+            return null;
+        }
+    }
+
+    /**
+     * Method to return a resource as a stream. The resource can be on the local file system or in the class path. The
+     * resource is resolved and returned as a stream.
+     *
+     * @param resourceName The resource name
+     * @return A stream attached to the resource
+     */
+    public static InputStream getResourceAsStream(final String resourceName) {
+        // Find a URL to the resource first
+        final var urlToResource = getUrl4Resource(resourceName);
+
+        // Check if the resource exists
+        if (urlToResource == null) {
+            // No resource found
+            LOGGER.debug("could not find resource \"{}\" : ", resourceName);
+            return null;
+        }
+
+        // Read the resource into a string
+        try {
+            return urlToResource.openStream();
+        } catch (final IOException e) {
+            // Any of many IO exceptions such as the resource is a directory
+            LOGGER.debug("error attaching resource {}", resourceName, e);
+            return null;
+        }
+    }
+
+    /**
+     * Method to get a URL resource from the class path.
+     *
+     * @param resourceName The resource name
+     * @return The URL to the resource
+     */
+    public static URL getUrlResource(final String resourceName) {
+        try {
+            final var classLoader = ResourceUtils.class.getClassLoader();
+
+            final String[] fileParts = SLASH_PAT.split(resourceName);
+            // Read the resource
+            var url = classLoader.getResource(resourceName);
+
+            // Check if the resource is defined
+            if (url != null) {
+                // Return the resource as a file name
+                LOGGER.debug("found URL resource \"{}\" : ", url);
+                return url;
+            } else {
+                url = classLoader.getResource(fileParts[fileParts.length - 1]);
+                if (url == null) {
+                    LOGGER.debug("cound not find URL resource \"{}\" : ", resourceName);
+                    return null;
+                }
+                LOGGER.debug("found URL resource \"{}\" : ", url);
+                return url;
+            }
+        } catch (final Exception e) {
+            LOGGER.debug("error getting URL resource {}", resourceName, e);
+            return null;
+        }
+    }
+
+    /**
+     * Method to get a URL resource from the local machine.
+     *
+     * @param resourceName The resource name
+     * @return The URL to the resource
+     */
+    public static URL getLocalFile(final String resourceName) {
+        try {
+            // Input might already be in URL format
+            final var ret = new URL(resourceName);
+            final var f = new File(ret.toURI());
+            if (f.exists()) {
+                return ret;
+            }
+        } catch (final Exception ignore) {
+            // We ignore exceptions here and catch them below
+        }
+
+        try {
+            final var f = new File(resourceName);
+            // Check if the file exists
+            if (f.exists()) {
+                final var urlret = f.toURI().toURL();
+                LOGGER.debug("resource \"{}\" was found on the local file system", f.toURI().toURL());
+                return urlret;
+            } else {
+                LOGGER.debug("resource \"{}\" does not exist on the local file system", resourceName);
+                return null;
+            }
+        } catch (final Exception e) {
+            LOGGER.debug("error finding resource {}", resourceName, e);
+            return null;
+        }
+    }
+
+    /**
+     * Gets the file path for a resource on the local file system or on the class path.
+     *
+     * @param resource the resource to the get the file path for
+     * @return the resource file path
+     */
+    public static String getFilePath4Resource(final String resource) {
+        if (resource == null) {
+            return null;
+        }
+
+        var modelFileUrl = getUrl4Resource(resource);
+        if (modelFileUrl != null) {
+            return modelFileUrl.getPath();
+        } else {
+            return resource;
+        }
+    }
+
+    /**
+     * Read the list of entries in a resource directory.
+     *
+     * @param resourceDirectoryName the name of the resource directory
+     * @return a set of entries
+     */
+    public static Set<String> getDirectoryContents(final String resourceDirectoryName) {
+        // Find the location of the resource, is it in a Jar or on the local file system?
+        var directoryUrl = ResourceUtils.getUrl4Resource(resourceDirectoryName);
+
+        if (directoryUrl == null) {
+            LOGGER.debug("resource \"{}\" was not found", resourceDirectoryName);
+            return Collections.emptySet();
+        }
+
+        if (FILE_PROTOCOL.equals(directoryUrl.getProtocol())) {
+            return getDirectoryContentsLocal(directoryUrl, resourceDirectoryName);
+        } else if (JAR_PROTOCOL.equals(directoryUrl.getProtocol())) {
+            // Examine the Jar
+            return getDirectoryContentsJar(directoryUrl, resourceDirectoryName);
+        } else {
+            LOGGER.debug("resource \"{}\" has an unsupported protocol {}", resourceDirectoryName,
+                    directoryUrl.getProtocol());
+            return Collections.emptySet();
+        }
+    }
+
+    /**
+     * Get a list of the contents of a local resource directory.
+     *
+     * @param localResourceDirectoryUrl the local resource file URL
+     * @param resourceDirectoryName the name of the resource directory
+     * @return a set of the directory contents
+     */
+    public static Set<String> getDirectoryContentsLocal(final URL localResourceDirectoryUrl,
+            final String resourceDirectoryName) {
+        var localDirectory = new File(localResourceDirectoryUrl.getFile());
+
+        if (!localDirectory.isDirectory()) {
+            LOGGER.debug("resource \"{}\" is not a directory", resourceDirectoryName);
+            return Collections.emptySet();
+        }
+
+        Set<String> localDirectorySet = new TreeSet<>();
+        for (File localDirectoryEntry : Objects.requireNonNull(localDirectory.listFiles())) {
+            if (localDirectoryEntry.isDirectory()) {
+                localDirectorySet
+                        .add(resourceDirectoryName + File.separator + localDirectoryEntry.getName() + File.separator);
+            } else {
+                localDirectorySet.add(resourceDirectoryName + File.separator + localDirectoryEntry.getName());
+            }
+        }
+
+        return localDirectorySet;
+    }
+
+    /**
+     * Get a list of the contents of a local resource directory.
+     *
+     * @param jarResourceDirectoryUrl the name of the resource directory in the jar
+     * @param resourceDirectoryName the name of the resource directory
+     * @return a set of the directory contents
+     */
+    public static Set<String> getDirectoryContentsJar(final URL jarResourceDirectoryUrl,
+            final String resourceDirectoryName) {
+        String dirNameWithSlash = resourceDirectoryName + "/";
+        int minLength = dirNameWithSlash.length() + 1;
+        var jarResourceDirectory = new File(jarResourceDirectoryUrl.getPath());
+        String jarFileName = jarResourceDirectory.getParent().replaceFirst("^file:", "").replaceFirst("!.*$", "");
+
+        Set<String> localDirectorySet = new TreeSet<>();
+
+        try (var jarFile = new JarFile(jarFileName)) {
+            Enumeration<JarEntry> entries = jarFile.entries(); // NOSONAR
+
+            while (entries.hasMoreElements()) {
+                /*
+                 * Ignore sonar issue, as the entries are not being expanded here.
+                 */
+                JarEntry je = entries.nextElement();    // NOSONAR
+                String jeName = je.getName();
+
+                if (jeName.length() >= minLength && jeName.startsWith(dirNameWithSlash)) {
+                    localDirectorySet.add(jeName);
+                }
+            }
+        } catch (IOException ioe) {
+            LOGGER.debug("error opening jar file {}", jarResourceDirectoryUrl.getPath());
+            return Collections.emptySet();
+        }
+
+        return localDirectorySet;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/FeatureApiUtils.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/FeatureApiUtils.java
new file mode 100644 (file)
index 0000000..042ee93
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.services;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Predicate;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+/**
+ * Utilities for use with "feature APIs".
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class FeatureApiUtils {
+
+    /**
+     * Applies a function on each feature provider, stopping as soon as one returns true.
+     *
+     * @param providers list of feature providers
+     * @param predicate function to be applied to each provider
+     * @param handleEx function to handle any exception generated by the predicate
+     *        (typically, by logging the message)
+     * @return {@code true} if one of the providers returned {@code true}, {@code false}
+     *         otherwise
+     */
+    public static <T> boolean apply(List<T> providers, Predicate<T> predicate,
+                    BiConsumer<T, Exception> handleEx) {
+
+        for (T feature : providers) {
+            try {
+                if (predicate.test(feature)) {
+                    return true;
+                }
+            } catch (RuntimeException e) {
+                handleEx.accept(feature, e);
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedService.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedService.java
new file mode 100644 (file)
index 0000000..c5050c8
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * ============LICENSE_START=======================================================
+ * utils
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.services;
+
+/**
+ * This is a base interface that is used to control the order of a list
+ * of services (features) discovered via 'ServiceLoader'. See
+ * 'OrderedServiceImpl' for more details.
+ */
+@FunctionalInterface
+public interface OrderedService {
+    /**
+     * Get sequence number.
+     *
+     * @return an integer sequence number, which determines the order of a list
+     *     of objects implementing this interface
+     */
+    public int getSequenceNumber();
+
+
+    /**
+     * Get the name.
+     *
+     * @return the name of the ordered service
+     */
+    public default String getName() {
+        return this.getClass().getName();
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedServiceImpl.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/OrderedServiceImpl.java
new file mode 100644 (file)
index 0000000..3726ef8
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * ============LICENSE_START=======================================================
+ * utils
+ * ================================================================================
+ * 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.utils.services;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class is a template for building a sorted list of service instances,
+ * which are discovered and created using 'ServiceLoader'.
+ */
+public class OrderedServiceImpl<T extends OrderedService> {
+    // logger
+    private static final Logger logger = LoggerFactory.getLogger(OrderedServiceImpl.class);
+
+    // sorted list of instances implementing the service
+    private List<T> implementers = null;
+
+    // 'ServiceLoader' that is used to discover and create the services
+    private final ServiceLoader<T> serviceLoader;
+
+    // use this to ensure that we only use one unique instance of each class
+    private static final Map<Class<?>, OrderedService> classToSingleton = new HashMap<>();
+
+    /**
+     * Constructor - create the 'ServiceLoader' instance.
+     *
+     * @param clazz the class object associated with 'T' (I supposed it could
+     *              be a subclass, but I'm not sure if this is useful)
+     */
+    public OrderedServiceImpl(Class<T> clazz) {
+        // This constructor wouldn't be needed if 'T.class' was legal
+        serviceLoader = ServiceLoader.load(clazz);
+    }
+
+    /**
+     * Get List of implementers.
+     *
+     * @return the sorted list of services implementing interface 'T' discovered by 'ServiceLoader'.
+     */
+    public synchronized List<T> getList() {
+        if (implementers == null) {
+            rebuildList();
+        }
+        return implementers;
+    }
+
+    /**
+     * This method is called by 'getList', but could also be called directly if
+     * we were running with a 'ClassLoader' that supported the dynamic addition
+     * of JAR files. In this case, it could be invoked in order to discover any
+     * new services implementing interface 'T'. This is probably a relatively
+     * expensive operation in terms of CPU and elapsed time, so it is best if it
+     * isn't invoked too frequently.
+     *
+     * @return the sorted list of services implementing interface 'T' discovered by 'ServiceLoader'.
+     */
+    @SuppressWarnings("unchecked")
+    public synchronized List<T> rebuildList() {
+        // build a list of all the current implementors
+        List<T> tmp = new LinkedList<>();
+        for (T service : serviceLoader) {
+            tmp.add((T) getSingleton(service));
+        }
+
+        // Sort the list according to sequence number, and then alphabetically
+        // according to full class name.
+        tmp.sort((o1, o2) -> {
+            int s1 = o1.getSequenceNumber();
+            int s2 = o2.getSequenceNumber();
+            if (s1 < s2) {
+                return -1;
+            } else if (s1 > s2) {
+                return 1;
+            } else {
+                return o1.getClass().getName().compareTo(o2.getClass().getName());
+            }
+        });
+
+        // create an unmodifiable version of this list
+        implementers = Collections.unmodifiableList(tmp);
+        logger.info("***** OrderedServiceImpl implementers:\n {}", implementers);
+        return implementers;
+    }
+
+    /**
+     * If a service implements multiple APIs managed by 'ServiceLoader', a
+     * separate instance is created for each API. This method ensures that
+     * the first instance is used in all the lists.
+     *
+     * @param service this is the object created by ServiceLoader
+     * @return the object to use in place of 'service'. If 'service' is the first
+     *     object of this class created by ServiceLoader, it is returned. If not,
+     *     the object of this class that was initially created is returned
+     *     instead.
+     */
+    private static synchronized OrderedService getSingleton(OrderedService service) {
+        // see if we already have an instance of this class
+        OrderedService rval = classToSingleton.get(service.getClass());
+        if (rval == null) {
+            // No previous instance of this class exists -- use the supplied
+            // instance, and place it in the table.
+            rval = service;
+            classToSingleton.put(service.getClass(), service);
+        }
+        return rval;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManager.java
new file mode 100644 (file)
index 0000000..0899491
--- /dev/null
@@ -0,0 +1,223 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP PAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.services;
+
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.onap.policy.common.capabilities.Startable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages a series of services. The services are started in order, and stopped in reverse
+ * order.
+ */
+public class ServiceManager implements Startable {
+    private static final Logger logger = LoggerFactory.getLogger(ServiceManager.class);
+
+    /**
+     * Manager name.
+     */
+    @Getter
+    private final String name;
+
+    /**
+     * Services to be started/stopped.
+     */
+    private final Deque<Service> items = new LinkedList<>();
+
+    /**
+     * {@code True} if the services are currently running, {@code false} otherwise.
+     */
+    private final AtomicBoolean running = new AtomicBoolean(false);
+
+    /**
+     * Constructs the object, with a default name.
+     */
+    public ServiceManager() {
+        this("service manager");
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param name the manager's name, used for logging purposes
+     */
+    public ServiceManager(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Adds a pair of service actions to the manager.
+     *
+     * @param stepName name to be logged when the service is started/stopped
+     * @param starter function to start the service
+     * @param stopper function to stop the service
+     * @return this manager
+     */
+    public synchronized ServiceManager addAction(String stepName, RunnableWithEx starter, RunnableWithEx stopper) {
+        if (isAlive()) {
+            throw new IllegalStateException(name + " is already running; cannot add " + stepName);
+        }
+
+        items.add(new Service(stepName, starter, stopper));
+        return this;
+    }
+
+    /**
+     * Adds a service to the manager. The manager will invoke the service's
+     * {@link Startable#start()} and {@link Startable#stop()} methods.
+     *
+     * @param stepName name to be logged when the service is started/stopped
+     * @param service object to be started/stopped
+     * @return this manager
+     */
+    public synchronized ServiceManager addService(String stepName, Startable service) {
+        if (isAlive()) {
+            throw new IllegalStateException(name + " is already running; cannot add " + stepName);
+        }
+
+        items.add(new Service(stepName, service::start, service::stop));
+        return this;
+    }
+
+    @Override
+    public boolean isAlive() {
+        return running.get();
+    }
+
+    @Override
+    public synchronized boolean start() {
+        if (isAlive()) {
+            throw new IllegalStateException(name + " is already running");
+        }
+
+        logger.info("{} starting", name);
+
+        // tracks the services that have been started so far
+        Deque<Service> started = new LinkedList<>();
+        Exception ex = null;
+
+        for (Service item : items) {
+            try {
+                logger.info("{} starting {}", name, item.stepName);
+                item.starter.run();
+                started.add(item);
+
+            } catch (Exception e) {
+                logger.error("{} failed to start {}; rewinding steps", name, item.stepName);
+                ex = e;
+                break;
+            }
+        }
+
+        if (ex == null) {
+            logger.info("{} started", name);
+            running.set(true);
+            return true;
+        }
+
+        // one of the services failed to start - rewind those we've previously started
+        try {
+            rewind(started);
+
+        } catch (ServiceManagerException e) {
+            logger.error("{} rewind failed", name, e);
+        }
+
+        throw new ServiceManagerException(ex);
+    }
+
+    @Override
+    public synchronized boolean stop() {
+        if (!isAlive()) {
+            throw new IllegalStateException(name + " is not running");
+        }
+
+        running.set(false);
+        rewind(items);
+
+        return true;
+    }
+
+    @Override
+    public void shutdown() {
+        stop();
+    }
+
+    /**
+     * Rewinds a list of services, stopping them in reverse order. Stops all of the
+     * services, even if one of the "stop" functions throws an exception.
+     *
+     * @param running services that are running, in the order they were started
+     * @throws ServiceManagerException if a service fails to stop
+     */
+    private void rewind(Deque<Service> running) {
+        Exception ex = null;
+
+        logger.info("{} stopping", name);
+
+        // stop everything, in reverse order
+        Iterator<Service> it = running.descendingIterator();
+        while (it.hasNext()) {
+            Service item = it.next();
+            try {
+                logger.info("{} stopping {}", name, item.stepName);
+                item.stopper.run();
+            } catch (Exception e) {
+                logger.error("{} failed to stop {}", name, item.stepName);
+                ex = e;
+
+                // do NOT break or re-throw, as we must stop ALL remaining items
+            }
+        }
+
+        logger.info("{} stopped", name);
+
+        if (ex != null) {
+            throw new ServiceManagerException(ex);
+        }
+    }
+
+    /**
+     * Service information.
+     */
+    @AllArgsConstructor
+    private static class Service {
+        private String stepName;
+        private RunnableWithEx starter;
+        private RunnableWithEx stopper;
+    }
+
+    /*
+     * Cannot use a plain Runnable, because it can't throw exceptions. Could use a
+     * Callable, instead, but then all the lambda expressions become rather messy, thus
+     * we'll stick with RunnableWithEx, and just disable the sonar warning.
+     */
+    @FunctionalInterface
+    public static interface RunnableWithEx {
+        void run() throws Exception; // NOSONAR
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerContainer.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerContainer.java
new file mode 100644 (file)
index 0000000..6afc1ab
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.services;
+
+import org.onap.policy.common.capabilities.Startable;
+import org.onap.policy.common.utils.services.ServiceManager.RunnableWithEx;
+
+/**
+ * Container, of a service manager, that implements a {@link Startable} interface by
+ * delegating to the contained service manager. This allows subclasses to add actions to
+ * the service manager, while preventing other classes from doing so.
+ */
+public class ServiceManagerContainer implements Startable {
+
+    /**
+     * The contained manager.
+     */
+    private final ServiceManager serviceManager;
+
+    /**
+     * Constructs the object, with a default name.
+     */
+    public ServiceManagerContainer() {
+        serviceManager = new ServiceManager();
+    }
+
+    /**
+     * Constructs the object.
+     *
+     * @param name the manager's name, used for logging purposes
+     */
+    public ServiceManagerContainer(String name) {
+        serviceManager = new ServiceManager(name);
+    }
+
+    public String getName() {
+        return serviceManager.getName();
+    }
+
+    /**
+     * Adds a pair of service actions to the manager.
+     *
+     * @param stepName name to be logged when the service is started/stopped
+     * @param starter function to start the service
+     * @param stopper function to stop the service
+     */
+    protected void addAction(String stepName, RunnableWithEx starter, RunnableWithEx stopper) {
+        serviceManager.addAction(stepName, starter, stopper);
+    }
+
+    /**
+     * Adds a service to the manager. The manager will invoke the service's
+     * {@link Startable#start()} and {@link Startable#stop()} methods.
+     *
+     * @param stepName name to be logged when the service is started/stopped
+     * @param service object to be started/stopped
+     */
+    protected void addService(String stepName, Startable service) {
+        serviceManager.addService(stepName, service);
+    }
+
+    @Override
+    public boolean isAlive() {
+        return serviceManager.isAlive();
+    }
+
+    @Override
+    public boolean start() {
+        return serviceManager.start();
+    }
+
+    @Override
+    public boolean stop() {
+        return serviceManager.stop();
+    }
+
+    @Override
+    public void shutdown() {
+        serviceManager.shutdown();
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java b/policy-common/src/main/java/org/onap/policy/common/utils/services/ServiceManagerException.java
new file mode 100644 (file)
index 0000000..ac37b6b
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP PAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.services;
+
+/**
+ * Exceptions thrown by the ServiceManager.
+ */
+public class ServiceManagerException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public ServiceManagerException() {
+        super();
+    }
+
+    public ServiceManagerException(String message) {
+        super(message);
+    }
+
+    public ServiceManagerException(Throwable cause) {
+        super(cause);
+    }
+
+    public ServiceManagerException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ConstructionError.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ConstructionError.java
new file mode 100644 (file)
index 0000000..d343857
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * ============LICENSE_START====================================================
+ * Common Utils-Test
+ * =============================================================================
+ * Copyright (C) 2018 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.
+ * ============LICENSE_END======================================================
+ */
+
+package org.onap.policy.common.utils.test;
+
+/**
+ * An error that occurred while trying to construct an object for a junit test.
+ */
+public class ConstructionError extends AssertionError {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructor.
+     */
+    public ConstructionError() {
+        super();
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message denotes the error message
+     */
+    public ConstructionError(final String message) {
+        super(message);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param cause denotes the cause of the error
+     */
+    public ConstructionError(final Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message denotes the error message
+     * @param cause denotes the cause of the error
+     */
+    public ConstructionError(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ErrorsTester.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ErrorsTester.java
new file mode 100644 (file)
index 0000000..ab876b6
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * ============LICENSE_START====================================================
+ * Common Utils-Test
+ * =============================================================================
+ * Copyright (C) 2018 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.
+ * ============LICENSE_END======================================================
+ */
+
+package org.onap.policy.common.utils.test;
+
+/**
+ * Used to test various Error subclasses. Uses reflection to identify the
+ * constructors that the subclass supports.
+ */
+public class ErrorsTester extends ThrowablesTester {
+
+    /**
+     * Runs tests, on an Error subclass, for all of the standard
+     * constructors.If the Error subclass does not support a given
+     * type of constructor, then it skips that test.
+     * Does <i>not</i> throw an exception if no standard constructors
+     * are found.
+     *
+     * @param claz subclass to be tested
+     * @param <T> this needs to be declared
+     *
+     * @return the number of constructors that were found/tested
+     * @throws ConstructionError
+     *             if the Error subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public <T extends Error> int testAllError(final Class<T> claz) {
+        return testAllThrowable(claz);
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ExceptionsTester.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ExceptionsTester.java
new file mode 100644 (file)
index 0000000..34dc568
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * ============LICENSE_START====================================================
+ * Common Utils-Test
+ * =============================================================================
+ * 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.utils.test;
+
+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 java.lang.reflect.Constructor;
+
+/**
+ * Used to test various Exception subclasses. Uses reflection to identify the
+ * constructors that the subclass supports.
+ */
+public class ExceptionsTester extends ThrowablesTester {
+
+    /**
+     * Runs tests, on an Exception subclass, for all of the standard
+     * constructors. If the Exception subclass does not support a given
+     * type of constructor, then it skips that test.
+     *
+     * @param claz subclass to be tested
+     * @param <T> Type of the class
+     *
+     * @return the number of constructors that were found/tested
+     * @throws ConstructionError
+     *             if the Exception subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public <T extends Exception> int test(final Class<T> claz) {
+        int ncons = testAllException(claz);
+
+        assertTrue(ncons > 0);
+
+        return ncons;
+    }
+
+    /**
+     * Runs tests, on an Exception subclass, for all of the standard
+     * constructors. If the Exception subclass does not support a given
+     * type of constructor, then it skips that test. Does <i>not</i> throw
+     * an exception if no standard constructors are found.
+     *
+     * @param claz subclass to be tested
+     * @param <T> type of the class
+     *
+     * @return the number of constructors that were found/tested
+     * @throws ConstructionError
+     *             if the Exception subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public <T extends Exception> int testAllException(final Class<T> claz) {
+        var ncons = 0;
+
+        ncons += testAllThrowable(claz);
+        ncons += testException(claz);
+        ncons += testStringException(claz);
+        ncons += testStringExceptionBooleanBoolean(claz);
+
+        return ncons;
+    }
+
+    /**
+     * Tests exceptions created via the constructor that takes just an
+     * Exception. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the cause's message</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * </ul>
+     *
+     * <p>If the Exception subclass does not support this type of constructor,
+     * then this method simply returns.
+     *
+     * @param claz subclass to be tested
+     * @param <T> Type of the class
+     *
+     * @return {@code 1}, if the subclass supports this type of constructor,
+     *         {@code 0} otherwise
+     * @throws ConstructionError
+     *             if the Exception subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public <T extends Exception> int testException(final Class<T> claz) {
+        Constructor<T> cons = getConstructor(claz, "exception",
+                        Exception.class);
+
+        if (cons == null) {
+            return 0;
+        }
+
+        var cause = new Exception(EXPECTED_EXCEPTION_MSG);
+        var ex = newInstance(cons, cause);
+
+        assertNotNull(ex.toString());
+        assertEquals(ex.getMessage(), ex.getMessage());
+        assertEquals(cause, ex.getCause());
+
+        return 1;
+    }
+
+    /**
+     * Tests exceptions created via the constructor that takes a String
+     * and an Exception. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the original message passed to
+     * the constructor</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * </ul>
+     *
+     * <p>If the Exception subclass does not support this type of
+     * constructor, then this method simply returns.
+     *
+     * @param claz subclass to be tested
+     * @param <T> Type of the class
+     *
+     * @return {@code 1}, if the subclass supports this type of constructor,
+     *         {@code 0} otherwise
+     * @throws ConstructionError
+     *             if the Exception subclass cannot be constructed
+     */
+    public <T extends Exception> int testStringException(
+                    final Class<T> claz) {
+        Constructor<T> cons = getConstructor(claz, "string-exception",
+                        String.class, Exception.class);
+        if (cons == null) {
+            return 0;
+        }
+
+        var cause = new Exception(EXPECTED_EXCEPTION_MSG);
+        var ex = newInstance(cons, "world", cause);
+
+        assertNotNull(ex.toString());
+        assertEquals("world", ex.getMessage());
+        assertEquals(cause, ex.getCause());
+
+        return 1;
+    }
+
+    /**
+     * Tests exceptions created via the constructor that takes a String, an
+     * Exception, and two booleans. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the original message passed to the
+     * constructor</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * <li>suppressed exceptions can be added, if enabled</li>
+     * <li>the stack trace can be added, if enabled</li>
+     * </ul>
+     *
+     * <p>If the Exception subclass does not support this type of constructor,
+     * then this method simply returns.
+     *
+     * @param claz subclass to be tested
+     * @param <T> Type of the class
+     *
+     * @return {@code 1}, if the subclass supports this type of constructor,
+     *         {@code 0} otherwise
+     * @throws ConstructionError
+     *             if the Exception subclass cannot be constructed
+     */
+    public <T extends Exception> int testStringExceptionBooleanBoolean(
+                    final Class<T> claz) {
+
+        Constructor<T> cons = getConstructor(claz,
+                        "string-exception-flags", String.class, Exception.class,
+                        Boolean.TYPE, Boolean.TYPE);
+
+        if (cons == null) {
+            return 0;
+        }
+
+        // test each combination of "message" and "cause"
+        testMessageCauseCombos(cons);
+
+        // test each combination of the boolean flags
+        testFlagCombos(cons);
+
+        return 1;
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ThrowablesTester.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ThrowablesTester.java
new file mode 100644 (file)
index 0000000..62584c7
--- /dev/null
@@ -0,0 +1,564 @@
+/*
+ * ============LICENSE_START====================================================
+ * Common Utils-Test
+ * =============================================================================
+ * Copyright (C) 2018, 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.utils.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Used to test various Throwable subclasses. Uses reflection to identify the
+ * constructors that the subclass supports.
+ */
+public class ThrowablesTester {
+
+    private static Logger logger =
+                    LoggerFactory.getLogger(ThrowablesTester.class);
+
+    public static final String EXPECTED_EXCEPTION_MSG =
+                    "expected exception";
+    private static final String EXPECTED_SUPPRESSED_EXCEPTION_MSG =
+                    "expected suppressed exception";
+
+    /**
+     * Passed as a "cause" to constructors.
+     */
+    public static final Exception CAUSE =
+                    new Exception(EXPECTED_EXCEPTION_MSG);
+
+    /**
+     * Passed to new objects via the <i>addSuppressed()</i> method..
+     */
+    public static final Throwable SUPPRESSED =
+                    new Throwable(EXPECTED_SUPPRESSED_EXCEPTION_MSG);
+
+    /**
+     * Runs tests, on an Throwable subclass, for all of the
+     * standard constructors. If the Throwable subclass does
+     * not support a given type of constructor, then it skips
+     * that test. Does <i>not</i> throw an exception if no
+     * standard constructors are found.
+     *
+     * @param claz subclass to be tested
+     * @param <T> To be defined
+     * @return the number of constructors that were found/tested
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> int testAllThrowable(
+                    final Class<T> claz) {
+        var ncons = 0;
+
+        ncons += testDefault(claz);
+        ncons += testString(claz);
+        ncons += testThrowable(claz);
+        ncons += testStringThrowable(claz);
+        ncons += testStringThrowableBooleanBoolean(claz);
+
+        return ncons;
+    }
+
+    /**
+     * Tests Throwable objects created via the default constructor. Verifies
+     * that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns null</li>
+     * <li><i>getCause()</i> returns null</li>
+     * </ul>
+     *
+     * <p>If the Throwable subclass does not support this type of
+     * constructor, then this method simply returns.
+     *
+     * @param claz subclass to be tested
+     * @param <T> to be defined
+     * @return {@code 1}, if the subclass supports this type of constructor,
+     *         {@code 0} otherwise
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> int testDefault(
+                    final Class<T> claz) {
+        Constructor<T> cons = getConstructor(claz, "default");
+        if (cons == null) {
+            return 0;
+        }
+
+        var ex = newInstance(cons);
+
+        assertNotNull(ex.toString());
+        assertNull(ex.getMessage());
+        assertNull(ex.getCause());
+
+        return 1;
+    }
+
+    /**
+     * Tests Throwable objects created via the constructor that takes just a
+     * String. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the original message passed to the
+     * constructor</li>
+     * <li><i>getCause()</i> returns null</li>
+     * </ul>
+     *
+     * <p>If the Throwable subclass does not support this type of constructor,
+     * then this method simply returns.
+     *
+     * @param claz
+     *            subclass to be tested
+     * @param <T> to be defined
+     * @return {@code 1}, if the subclass supports this type of constructor,
+     *         {@code 0} otherwise
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> int testString(final Class<T> claz) {
+        Constructor<T> cons = getConstructor(claz, "string",
+                        String.class);
+        if (cons == null) {
+            return 0;
+        }
+
+        var ex = newInstance(cons, "hello");
+
+        assertNotNull(ex.toString());
+        assertEquals("hello", ex.getMessage());
+        assertNull(ex.getCause());
+
+        return 1;
+    }
+
+    /**
+     * Tests Throwable objects created via the constructor that takes just a
+     * Throwable. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the cause's message</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * </ul>
+     *
+     * <p>If the Throwable subclass does not support this type of constructor,
+     * then this method simply returns.
+     *
+     * @param claz
+     *            subclass to be tested
+     * @param <T> to be defined
+     * @return {@code 1}, if the subclass supports this type of constructor,
+     *         {@code 0} otherwise
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> int testThrowable(
+                    final Class<T> claz) {
+        Constructor<T> cons = getConstructor(claz, "throwable",
+                        Throwable.class);
+        if (cons == null) {
+            return 0;
+        }
+
+        var ex = newInstance(cons, CAUSE);
+
+        assertEquals(ex.getMessage(), ex.getMessage());
+        assertNotNull(ex.toString());
+        assertEquals(CAUSE, ex.getCause());
+
+        return 1;
+    }
+
+    /**
+     * Tests Throwable objects created via the constructor that takes
+     * a String and a Throwable. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the original message passed to the
+     * constructor</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * </ul>
+     *
+     * <p>If the Throwable subclass does not support this type of constructor,
+     * then this method simply returns.
+     *
+     * @param claz subclass to be tested
+     * @param <T> to be defined
+     * @return {@code 1}, if the subclass supports this type of constructor,
+     *         {@code 0} otherwise
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> int testStringThrowable(
+                    final Class<T> claz) {
+        Constructor<T> cons = getConstructor(claz, "string-throwable",
+                        String.class, Throwable.class);
+        if (cons == null) {
+            return 0;
+        }
+
+        var ex = newInstance(cons, "world", CAUSE);
+
+        assertNotNull(ex.toString());
+        assertEquals("world", ex.getMessage());
+        assertEquals(CAUSE, ex.getCause());
+
+        return 1;
+    }
+
+    /**
+     * Tests Throwable objects created via the constructor that takes
+     * a String, a Throwable, and two booleans. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the original message passed to the
+     * constructor</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * <li>suppressed exceptions can be added, if enabled</li>
+     * <li>the stack trace can be added, if enabled</li>
+     * </ul>
+     *
+     * <p>If the Throwable subclass does not support this type of constructor,
+     * then this method simply returns.
+     *
+     * @param claz
+     *            subclass to be tested
+     * @param <T> to be defined
+     * @return {@code 1}, if the subclass supports this type of constructor,
+     *         {@code 0} otherwise
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> int
+        testStringThrowableBooleanBoolean(
+                    final Class<T> claz) {
+        Constructor<T> cons = getConstructor(claz,
+                        "string-throwable-flags",
+                        String.class, Throwable.class,
+                        Boolean.TYPE, Boolean.TYPE);
+        if (cons == null) {
+            return 0;
+        }
+
+        // test each combination of "message" and "cause"
+        testMessageCauseCombos(cons);
+
+        // test each combination of the boolean flags
+        testSuppressStack(cons);
+        testSuppressNoStack(cons);
+        testNoSuppressStack(cons);
+        testNoSuppressNoStack(cons);
+
+        return 1;
+    }
+
+    /**
+     * Tests each combination of values for the "message" and the "cause"
+     * when using the constructor that takes a String,
+     * a Throwable/Exception, and two booleans. Verifies that expected
+     * values are returned
+     * <ul>
+     * <i>toString()</i>,
+     * <i>getMessage()</i>, and <i>getCause()</i>.
+     * </ul>
+     *
+     * @param cons
+     *            constructor to be invoked
+     * @param <T> to be defined
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> void testMessageCauseCombos(
+                    final Constructor<T> cons) {
+        T ex;
+
+        ex = newInstance(cons, null, null, true, true);
+        assertNotNull(ex.toString());
+        assertNull(ex.getMessage());
+        assertNull(ex.getCause());
+
+        ex = newInstance(cons, "abc", null, true, true);
+        assertNotNull(ex.toString());
+        assertEquals("abc", ex.getMessage());
+        assertNull(ex.getCause());
+
+        ex = newInstance(cons, null, CAUSE, true, true);
+        assertNotNull(ex.toString());
+        assertNull(ex.getMessage());
+        assertEquals(CAUSE, ex.getCause());
+
+        ex = newInstance(cons, "xyz", CAUSE, true, true);
+        assertNotNull(ex.toString());
+        assertEquals("xyz", ex.getMessage());
+        assertEquals(CAUSE, ex.getCause());
+    }
+
+    /**
+     * Tests each combination of values for the "message" and the
+     * "cause" when using the constructor that takes a String,
+     * a Throwable/Exception, and two booleans. Verifies that
+     * expected values are returned by
+     * <ul>
+     * <i>toString()</i>,
+     * <i>getMessage()</i>, and <i>getCause()</i>.
+     * </ul>
+     *
+     * @param cons
+     *            constructor to be invoked
+     * @param <T> to be defined
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> void testFlagCombos(
+                    final Constructor<T> cons) {
+        testSuppressStack(cons);
+        testSuppressNoStack(cons);
+        testNoSuppressStack(cons);
+        testNoSuppressNoStack(cons);
+    }
+
+    /**
+     * Tests Throwable objects constructed with
+     * {@code enableSuppression=true} and
+     * {@code writableStackTrace=true}. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the original message passed to the
+     * constructor</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * <li>suppressed exceptions are added</li>
+     * <li>the stack trace is added</li>
+     * </ul>
+     *
+     * @param cons
+     *            the throwable's class constructor
+     * @param <T> to be defined
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> void testSuppressStack(
+                    final Constructor<T> cons) {
+        var ex = newInstance(cons, "yes,yes", CAUSE, true, true);
+
+        ex.addSuppressed(SUPPRESSED);
+
+        assertNotNull(ex.toString());
+        assertEquals("yes,yes", ex.getMessage());
+        assertEquals(CAUSE, ex.getCause());
+
+        assertEquals(1, ex.getSuppressed().length);
+        assertEquals(SUPPRESSED, ex.getSuppressed()[0]);
+
+        assertTrue(ex.getStackTrace().length > 0);
+    }
+
+    /**
+     * Tests Throwable objects constructed with
+     * {@code enableSuppression=true} and
+     * {@code writableStackTrace=false}. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the original message passed to the
+     * constructor</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * <li>suppressed exceptions are added</li>
+     * <li>the stack trace is <i>not</i> added</li>
+     * </ul>
+     *
+     * @param cons
+     *            the throwable's class constructor
+     * @param <T> to be defined
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> void testSuppressNoStack(
+                    final Constructor<T> cons) {
+        var ex = newInstance(cons, "yes,no", CAUSE, true, false);
+
+        ex.addSuppressed(SUPPRESSED);
+
+        assertNotNull(ex.toString());
+        assertEquals("yes,no", ex.getMessage());
+        assertEquals(CAUSE, ex.getCause());
+
+        assertEquals(1, ex.getSuppressed().length);
+        assertEquals(SUPPRESSED, ex.getSuppressed()[0]);
+
+        assertEquals(0, ex.getStackTrace().length);
+    }
+
+    /**
+     * Tests Throwable objects constructed with
+     * {@code enableSuppression=false} and
+     * {@code writableStackTrace=true}. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the original message passed to the
+     * constructor</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * <li>suppressed exceptions are <i>not</i> added</li>
+     * <li>the stack trace is added</li>
+     * </ul>
+     *
+     * @param cons
+     *            the throwable's class constructor
+     * @param <T> to be defined
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> void testNoSuppressStack(
+                    final Constructor<T> cons) {
+        var ex = newInstance(cons, "no,yes", CAUSE, false, true);
+
+        ex.addSuppressed(SUPPRESSED);
+
+        assertNotNull(ex.toString());
+        assertEquals("no,yes", ex.getMessage());
+        assertEquals(CAUSE, ex.getCause());
+
+        assertEquals(0, ex.getSuppressed().length);
+
+        assertTrue(ex.getStackTrace().length > 0);
+    }
+
+    /**
+     * Tests Throwable objects constructed with
+     * {@code enableSuppression=false} and
+     * {@code writableStackTrace=false}. Verifies that:
+     * <ul>
+     * <li><i>toString()</i> returns a non-null value</li>
+     * <li><i>getMessage()</i> returns the original message passed to the
+     * constructor</li>
+     * <li><i>getCause()</i> returns the original cause passed to the
+     * constructor</li>
+     * <li>suppressed exceptions are <i>not</i> added</li>
+     * <li>the stack trace is <i>not</i> added</li>
+     * </ul>
+     * @param cons
+     *            the throwable's class constructor
+     * @param <T> to be defined
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     * @throws AssertionError
+     *             if the constructed objects fail to pass various tests
+     */
+    public final <T extends Throwable> void testNoSuppressNoStack(
+                    final Constructor<T> cons) {
+        var ex = newInstance(cons, "no,no", CAUSE, false, false);
+
+        ex.addSuppressed(SUPPRESSED);
+
+        assertNotNull(ex.toString());
+        assertEquals("no,no", ex.getMessage());
+        assertEquals(CAUSE, ex.getCause());
+
+        assertEquals(0, ex.getSuppressed().length);
+        assertEquals(0, ex.getStackTrace().length);
+    }
+
+    /**
+     * Attempts to get a constructor for objects of a given type.
+     *
+     * @param claz
+     *            class of objects whose constructor is to be gotten
+     * @param <T> to be defined
+     * @param testType
+     *            type of test being run
+     * @param argTypes
+     *            argument types to be passed to the constructor
+     * @return the desired constructor, or {@code null} if the desired
+     *         constructor is not available
+     */
+    protected <T extends Throwable> Constructor<T> getConstructor(
+                    final Class<T> claz,
+                    final String testType,
+                    final Class<?>... argTypes) {
+
+        try {
+            return claz.getConstructor(argTypes);
+
+        } catch (NoSuchMethodException | SecurityException e) {
+            // this constructor is not defined so nothing to test
+            logger.debug("skipped test, no constructor for: {}", claz, e);
+            return null;
+        }
+    }
+
+    /**
+     * Creates a new instance of an Throwable subclass.
+     *
+     * @param cons
+     *            subclass constructor
+     * @param <T> to be defined
+     * @param args
+     *            arguments to be passed to the constructor
+     * @return a new instance of the Throwable subclass
+     * @throws ConstructionError
+     *             if the Throwable subclass cannot be constructed
+     */
+    protected <T extends Throwable> T newInstance(
+                    final Constructor<T> cons,
+                    final Object... args) {
+        try {
+            return cons.newInstance(args);
+
+        } catch (InstantiationException | IllegalAccessException
+                        | IllegalArgumentException
+                        | InvocationTargetException e) {
+
+            throw new ConstructionError(e);
+        }
+
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/ToStringTester.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/ToStringTester.java
new file mode 100644 (file)
index 0000000..ab09291
--- /dev/null
@@ -0,0 +1,63 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2018 Ericsson. All rights reserved.
+ *  Modifications Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved.
+ *  Modifications Copyright (C) 2019 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.test;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.anything;
+
+import com.openpojo.reflection.PojoClass;
+import com.openpojo.validation.affirm.Affirm;
+import com.openpojo.validation.test.Tester;
+import com.openpojo.validation.utils.ValidationHelper;
+import lombok.AllArgsConstructor;
+import org.hamcrest.Matcher;
+
+
+/**
+ * Class to provide toString testing utility for testing pojo classes.
+ *
+ * @author Ram Krishna Verma (ram.krishna.verma@est.tech)
+ */
+@AllArgsConstructor
+public class ToStringTester implements Tester {
+
+    private final Matcher<?> matcher;
+
+    public ToStringTester() {
+        matcher = anything();
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void run(final PojoClass pojoClass) {
+        final Class<?> clazz = pojoClass.getClazz();
+        if (anyOf(matcher).matches(clazz)) {
+            final Object classInstance = ValidationHelper.getBasicInstance(pojoClass);
+
+            Affirm.affirmFalse("Found default toString output",
+                    classInstance.toString().matches(Object.class.getName() + "@" + "\\w+"));
+        }
+
+    }
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/test/log/logback/ExtractAppender.java b/policy-common/src/main/java/org/onap/policy/common/utils/test/log/logback/ExtractAppender.java
new file mode 100644 (file)
index 0000000..27d9fcc
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+ * ============LICENSE_START====================================================
+ * Common Utils-Test
+ * =============================================================================
+ * Copyright (C) 2018-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.
+ * ============LICENSE_END======================================================
+ */
+
+package org.onap.policy.common.utils.test.log.logback;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import com.google.re2j.Matcher;
+import com.google.re2j.Pattern;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * This is an appender that is intended for use by JUnit tests that wish to
+ * capture logged messages. The appender takes an optional list of regular
+ * expressions that are used to identify and extract data of interest.
+ * <p/>
+ * If no patterns are provided, then every logged message is recorded. However,
+ * if patterns are provided, then only messages that match one of the patterns
+ * are recorded. In addition, if the pattern contains a capture group that is
+ * non-null, only the captured group is recorded. Otherwise, the entire portion
+ * of the message that matches the pattern is recorded.
+ * <p/>
+ * All operations are thread-safe.
+ */
+public class ExtractAppender extends AppenderBase<ILoggingEvent> {
+
+    /**
+     * Extracted text is placed here.
+     */
+    private final Queue<String> extracted;
+
+    /**
+     * Regular expressions/Patterns to be used to extract text. Uses a
+     * LinkedHashMap so that order is preserved.
+     */
+    private final LinkedHashMap<String, Pattern> patterns;
+
+    /**
+     * Records every message that is logged.
+     */
+    public ExtractAppender() {
+        this(new LinkedList<>());
+    }
+
+    /**
+     * Records portions of messages that match one of the regular
+     * expressions.
+     *
+     * @param regex
+     *            regular expression (i.e., {@link Pattern}) to match
+     */
+    public ExtractAppender(final String... regex) {
+        this(new LinkedList<>(), regex);
+    }
+
+    /**
+     * Rather than allocating an internal queue to store matched messages,
+     * messages are recorded in the specified target queue using the
+     * {@link Queue#offer(Object)} method. Note: whenever the queue is used,
+     * it will be synchronized to prevent simultaneous accesses.
+     *
+     * @param target - queue into which the matched text should be placed
+     * @param regex regular expression (i.e., {@link Pattern}) to match
+     */
+    public ExtractAppender(final Queue<String> target,
+                    final String... regex) {
+        extracted = target;
+        patterns = new LinkedHashMap<>(regex.length);
+
+        for (String re : regex) {
+            patterns.put(re, Pattern.compile(re));
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see ch.qos.logback.core.AppenderBase#append(Object)
+     */
+    @Override
+    protected void append(final ILoggingEvent event) {
+
+        String msg = event.getFormattedMessage();
+
+        synchronized (patterns) {
+            if (patterns.isEmpty()) {
+                addExtraction(msg);
+                return;
+            }
+
+            for (Pattern p : patterns.values()) {
+                var matcher = p.matcher(msg);
+
+                if (matcher.find()) {
+                    addGroupMatch(matcher);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds the first match group to {@link #extracted}.
+     *
+     * @param mat the matcher containing the groups
+     *
+     */
+    private void addGroupMatch(final Matcher mat) {
+        int ngroups = mat.groupCount();
+
+        for (var x = 1; x <= ngroups; ++x) {
+            String txt = mat.group(x);
+
+            if (txt != null) {
+                addExtraction(txt);
+                return;
+            }
+        }
+
+        addExtraction(mat.group());
+    }
+
+    /**
+     * Adds an item to {@link #extracted}, in a thread-safe manner.
+     * It uses the queue's <i>offer()</i> method so that the queue
+     * can discard the item if it so chooses, without generating
+     * an exception.
+     *
+     * @param txt
+     *            text to be added
+     */
+    private void addExtraction(final String txt) {
+        synchronized (extracted) {
+            extracted.offer(txt);
+        }
+    }
+
+    /**
+     * Gets the text that has been extracted.
+     *
+     * @return a copy of the text that has been extracted
+     */
+    public List<String> getExtracted() {
+        synchronized (extracted) {
+            return new ArrayList<>(extracted);
+        }
+    }
+
+    /**
+     * Clears the list of extracted text.
+     */
+    public void clearExtractions() {
+        synchronized (extracted) {
+            extracted.clear();
+        }
+    }
+
+    /**
+     * Adds a pattern to be matched by this appender.
+     *
+     * @param regex
+     *            regular expression (i.e., {@link Pattern}) to match
+     */
+    public void setPattern(final String regex) {
+        synchronized (patterns) {
+            patterns.put(regex, Pattern.compile(regex));
+        }
+    }
+
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/validation/Assertions.java b/policy-common/src/main/java/org/onap/policy/common/utils/validation/Assertions.java
new file mode 100644 (file)
index 0000000..8e47420
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2016-2018 Ericsson. All rights reserved.
+ *  Modifications Copyright (C) 2019 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.utils.validation;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The Class Assertions is a static class that is used as a shorthand for assertions in the source code.
+ * It throws runtime exceptions on assertion fails.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class Assertions {
+    // Logger for this class
+    private static final Logger LOGGER = LoggerFactory.getLogger(Assertions.class);
+
+    /**
+     * Gets the validation message for a string parameter.
+     *
+     * @param parameterName the string parameter name
+     * @param parameterValue the string parameter value
+     * @param pattern The regular expression
+     * @return null if the parameter is valid, the validation message otherwise
+     */
+    public static String getStringParameterValidationMessage(final String parameterName, final String parameterValue,
+                    final String pattern) {
+        try {
+            validateStringParameter(parameterName, parameterValue, pattern);
+        } catch (IllegalArgumentException e) {
+            String message = "parameter " + parameterName + " with value " + parameterValue
+                            + " does not match regular expression " + pattern;
+            if (LOGGER.isTraceEnabled()) {
+                LOGGER.trace(message, e);
+            }
+
+            return message;
+        }
+
+        return null;
+    }
+
+    /**
+     * Checks if a string parameter matches a regular expression.
+     *
+     * @param parameterName the string parameter name
+     * @param parameterValue the string parameter value
+     * @param pattern The regular expression
+     * @return the trimmed string
+     */
+    public static String validateStringParameter(final String parameterName, final String parameterValue,
+                    final String pattern) {
+        argumentNotNull(parameterName, "parameter name is null");
+        argumentNotNull(parameterValue, "parameter \"" + parameterName + "\" is null");
+        argumentNotNull(pattern, "parameter pattern is null");
+
+        final String trimmedValue = parameterValue.trim();
+        if (trimmedValue.matches(pattern)) {
+            return trimmedValue;
+        } else {
+            throw new IllegalArgumentException("parameter \"" + parameterName + "\": value \"" + parameterValue
+                            + "\", does not match regular expression \"" + pattern + "\"");
+        }
+    }
+
+    /**
+     * Used as a shorthand to check that method arguments are not null, throws IllegalArgumentException on error.
+     *
+     * @param <T> the generic type of the argument to check
+     * @param value the value of the type
+     * @param message the error message to issue
+     */
+    public static <T> void argumentNotNull(final T value, final String message) {
+        if (value == null) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Used as a shorthand to check that method arguments are not false, throws IllegalArgumentException on error.
+     *
+     * @param value the value to check if false
+     * @param message the error message to issue
+     */
+    public static void argumentNotFalse(final boolean value, final String message) {
+        if (!value) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * Used as a shorthand to check that method arguments are not null, throws an exception of the specified type on
+     * error.
+     *
+     * @param <T> the generic type of the argument to check
+     * @param <E> the exception to throw if incoming value is null
+     * @param value the value of the type
+     * @param exceptionClass the class of exception to return an instance of
+     * @param message the error message to issue
+     * @throws E an instance of the passed Exception Class
+     */
+    public static <T, E extends Exception> void argumentOfClassNotNull(final T value, final Class<E> exceptionClass,
+                    final String message) throws E {
+        if (value == null) {
+            // Instantiate the exception and throw it
+            try {
+                throw exceptionClass.getConstructor(String.class).newInstance(message);
+            } catch (final Exception errorException) {
+                throw new IllegalArgumentException(message, errorException);
+            }
+        }
+    }
+
+    /**
+     * Used as a shorthand to check that method argument is not false, throws an exception of the specified type on
+     * error.
+     *
+     * @param <E> the exception to throw if incoming value is false
+     * @param value the value to check if false
+     * @param exceptionClass the class of exception to return an instance of
+     * @param message the error message to issue
+     * @throws E an instance of the passed Exception Class
+     */
+    public static <E extends Exception> void argumentOfClassNotFalse(final boolean value, final Class<E> exceptionClass,
+                    final String message) throws E {
+        if (!value) {
+            // Instantiate the exception and throw it
+            try {
+                throw exceptionClass.getConstructor(String.class).newInstance(message);
+            } catch (final Exception errorException) {
+                throw new IllegalArgumentException(message, errorException);
+            }
+        }
+    }
+
+    /**
+     * Used as a shorthand to check that an object is an instance of a given class, throws IllegalArgumentException on
+     * error.
+     *
+     * @param <T> the generic type of the argument to check
+     * @param objectInstance the object instance for which to check the class
+     * @param requiredClass the class that the object should be an instance of
+     * @throws IllegalArgumentException if the incoming object is not an instance of requiredClass
+     */
+    public static <T> void instanceOf(final Object objectInstance, final Class<T> requiredClass) {
+        if (!requiredClass.isAssignableFrom(objectInstance.getClass())) {
+            throw new IllegalArgumentException(objectInstance.getClass().getName() + " is not an instance of "
+                            + requiredClass.getName());
+        }
+    }
+
+    /**
+     * Used as a shorthand to check that an instance of a class can be an instance of a given class, throws
+     * IllegalArgumentException on error.
+     *
+     * @param <T> the generic type of the argument to check
+     * @param checkClass the class to check
+     * @param requiredClass the class that the object should be an instance of
+     * @throws IllegalArgumentException if the incoming object is not an instance of requiredClass
+     */
+    public static <T> void assignableFrom(final Class<?> checkClass, final Class<T> requiredClass) {
+        if (!requiredClass.isAssignableFrom(checkClass)) {
+            throw new IllegalArgumentException(checkClass.getName() + " is not an instance of "
+                            + requiredClass.getName());
+        }
+    }
+}
diff --git a/policy-common/src/main/java/org/onap/policy/common/utils/validation/Version.java b/policy-common/src/main/java/org/onap/policy/common/utils/validation/Version.java
new file mode 100644 (file)
index 0000000..46e006b
--- /dev/null
@@ -0,0 +1,129 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP COMMON
+ * ================================================================================
+ * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2019 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.utils.validation;
+
+import com.google.re2j.Pattern;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Version of an object within the model. Versions are assumed to be of the form: major or major.minor.patch, where each
+ * component is numeric.
+ */
+@Data
+@RequiredArgsConstructor
+@NoArgsConstructor(force = true)
+public class Version implements Comparable<Version> {
+    private static final Logger logger = LoggerFactory.getLogger(Version.class);
+
+    /**
+     * Pattern to match a version of the form, major or major.minor.patch, where all components are numeric.
+     */
+    private static final Pattern VERSION_PAT = Pattern.compile("(\\d+)([.](\\d+)[.](\\d+))?");
+
+    private final int major;
+    private final int minor;
+    private final int patch;
+
+
+    /**
+     * String constructor.
+     *
+     * @param versionString the version string
+     */
+    public Version(@NonNull final String versionString) {
+        var newVersion = makeVersion("String", "constructor", versionString);
+
+        if (newVersion != null) {
+            this.major = newVersion.major;
+            this.minor = newVersion.minor;
+            this.patch = newVersion.patch;
+        } else {
+            this.major = 0;
+            this.minor = 0;
+            this.patch = 0;
+        }
+    }
+
+    /**
+     * Creates a version object.
+     *
+     * @param type type of object with which the version is associated, used when logging
+     * @param name name with which the version is associated, used when logging
+     * @param versionText the version, in textual form
+     * @return a new version, or {@code null} if the version cannot be created from the key (e.g., the key has a version
+     *         that does not match the major.minor.patch form)
+     */
+    public static Version makeVersion(String type, String name, String versionText) {
+        var matcher = VERSION_PAT.matcher(versionText);
+        if (!matcher.matches()) {
+            logger.info("invalid version for {} {}: {}", type, name, versionText);
+            return null;
+        }
+
+        try {
+            if (matcher.group(2) == null) {
+                // form: major
+                return new Version(Integer.parseInt(matcher.group(1)), 0, 0);
+
+            } else {
+                // form: major.minor.patch
+                return new Version(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(3)),
+                        Integer.parseInt(matcher.group(4)));
+            }
+
+        } catch (NumberFormatException e) {
+            logger.info("invalid version for {} {}: {}", type, name, versionText, e);
+            return null;
+        }
+    }
+
+    /**
+     * Generates a new version from a string.
+     *
+     * @return a new version, of the form major.0.0, where "major" is one more than "this" version's major number
+     */
+    public Version newVersion() {
+        return new Version(major + 1, 0, 0);
+    }
+
+    @Override
+    public int compareTo(Version other) {
+        var result = Integer.compare(major, other.major);
+        if (result != 0) {
+            return result;
+        }
+        if ((result = Integer.compare(minor, other.minor)) != 0) {
+            return result;
+        }
+        return Integer.compare(patch, other.patch);
+    }
+
+    @Override
+    public String toString() {
+        return major + "." + minor + "." + patch;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/JsonListenerTest.java b/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/JsonListenerTest.java
new file mode 100644 (file)
index 0000000..c2e5f90
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * ============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.endpoints.listeners;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+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.onap.policy.common.message.bus.event.Topic.CommInfrastructure;
+import org.onap.policy.common.utils.coder.StandardCoderObject;
+import org.onap.policy.common.utils.test.log.logback.ExtractAppender;
+import org.slf4j.LoggerFactory;
+
+class JsonListenerTest {
+
+    /**
+     * Used to attach an appender to the class' logger.
+     */
+    private static final Logger logger = (Logger) LoggerFactory.getLogger(JsonListener.class);
+    private static final ExtractAppender appender = new ExtractAppender();
+
+    /**
+     * Original logging level for the logger.
+     */
+    private static Level saveLevel;
+
+    private static final CommInfrastructure INFRA = CommInfrastructure.NOOP;
+    private static final String TOPIC = "my-topic";
+    private static final String JSON = "{'abc':'def'}".replace('\'', '"');
+
+    private JsonListener primary;
+
+    /**
+     * Initializes statics.
+     */
+    @BeforeAll
+    public static void setUpBeforeClass() {
+        saveLevel = logger.getLevel();
+        logger.setLevel(Level.INFO);
+
+        appender.setContext(logger.getLoggerContext());
+        appender.start();
+    }
+
+    @AfterAll
+    public static void tearDownAfterClass() {
+        logger.setLevel(saveLevel);
+        appender.stop();
+    }
+
+    /**
+     * Initializes mocks and a listener.
+     */
+    @BeforeEach
+    public void setUp() {
+        appender.clearExtractions();
+
+        primary = new JsonListener() {
+            @Override
+            public void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco) {
+                // do nothing
+            }
+        };
+    }
+
+    @AfterEach
+    public void tearDown() {
+        logger.detachAppender(appender);
+    }
+
+    @Test
+    void testOnTopicEvent() {
+        logger.addAppender(appender);
+
+        primary = spy(primary);
+
+        // success
+        primary.onTopicEvent(INFRA, TOPIC, JSON);
+        verify(primary).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+
+        // repeat
+        primary.onTopicEvent(INFRA, TOPIC, JSON);
+        verify(primary, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+
+        assertFalse(appender.getExtracted().toString().contains("unable to decode"));
+
+        // invalid json - decode fails
+        appender.clearExtractions();
+        primary.onTopicEvent(INFRA, TOPIC, "[");
+        assertTrue(appender.getExtracted().toString().contains("unable to decode"));
+        verify(primary, times(2)).onTopicEvent(any(), any(), any(StandardCoderObject.class));
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcherTest.java b/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcherTest.java
new file mode 100644 (file)
index 0000000..e0184bd
--- /dev/null
@@ -0,0 +1,179 @@
+/*--
+ * ============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.endpoints.listeners;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+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 ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+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.onap.policy.common.message.bus.event.Topic.CommInfrastructure;
+import org.onap.policy.common.utils.coder.StandardCoderObject;
+import org.onap.policy.common.utils.test.log.logback.ExtractAppender;
+import org.slf4j.LoggerFactory;
+
+class MessageTypeDispatcherTest {
+
+    /**
+     * Used to attach an appender to the class' logger.
+     */
+    private static final Logger logger = (Logger) LoggerFactory.getLogger(MessageTypeDispatcher.class);
+    private static final ExtractAppender appender = new ExtractAppender();
+
+    /**
+     * Original logging level for the logger.
+     */
+    private static Level saveLevel;
+
+    private static final CommInfrastructure INFRA = CommInfrastructure.NOOP;
+    private static final String TYPE_FIELD = "msg-type";
+    private static final String TOPIC = "my-topic";
+    private static final String TYPE1 = "msg-type-1";
+    private static final String TYPE2 = "msg-type-2";
+
+    private MessageTypeDispatcher primary;
+
+    private ScoListener<String> secondary1;
+    private ScoListener<String> secondary2;
+
+    /**
+     * Initializes statics.
+     */
+    @BeforeAll
+    public static void setUpBeforeClass() {
+        saveLevel = logger.getLevel();
+        logger.setLevel(Level.INFO);
+
+        appender.setContext(logger.getLoggerContext());
+        appender.start();
+    }
+
+    @AfterAll
+    public static void tearDownAfterClass() {
+        logger.setLevel(saveLevel);
+        appender.stop();
+    }
+
+    /**
+     * Initializes mocks and a listener.
+     */
+    @BeforeEach
+    @SuppressWarnings("unchecked")
+    public void setUp() {
+        appender.clearExtractions();
+
+        secondary1 = mock(ScoListener.class);
+        secondary2 = mock(ScoListener.class);
+
+        primary = new MessageTypeDispatcher(TYPE_FIELD);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        logger.detachAppender(appender);
+    }
+
+    @Test
+    void testRegister_testUnregister() {
+        primary.register(TYPE1, secondary1);
+        primary.register(TYPE2, secondary2);
+
+        primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1));
+        verify(secondary1).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+        verify(secondary2, never()).onTopicEvent(any(), any(), any());
+
+        primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1));
+        verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+        verify(secondary2, never()).onTopicEvent(any(), any(), any());
+
+        primary.unregister(TYPE1);
+        primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1));
+        verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+        verify(secondary2, never()).onTopicEvent(any(), any(), any());
+
+        primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE2));
+        verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+        verify(secondary2).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+
+        // unregister again
+        primary.unregister(TYPE1);
+
+        // unregister second type
+        primary.unregister(TYPE2);
+        primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1));
+        primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE2));
+        verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+        verify(secondary2, times(1)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+    }
+
+    @Test
+    void testOnTopicEvent() {
+        primary.register(TYPE1, secondary1);
+
+        logger.addAppender(appender);
+
+        // success
+        primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1));
+        verify(secondary1).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+
+        // repeat
+        primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE1));
+        verify(secondary1, times(2)).onTopicEvent(eq(INFRA), eq(TOPIC), any(StandardCoderObject.class));
+
+        assertFalse(appender.getExtracted().toString().contains("unable to extract"));
+        assertFalse(appender.getExtracted().toString().contains("discarding event of type"));
+
+        // no message type
+        appender.clearExtractions();
+        primary.onTopicEvent(INFRA, TOPIC, "{}");
+        assertTrue(appender.getExtracted().toString().contains("unable to extract"));
+        verify(secondary1, times(2)).onTopicEvent(any(), any(), any());
+
+        // unknown type
+        appender.clearExtractions();
+        primary.onTopicEvent(INFRA, TOPIC, makeMessage(TYPE2));
+        assertTrue(appender.getExtracted().toString().contains("discarding event of type"));
+        verify(secondary1, times(2)).onTopicEvent(any(), any(), any());
+    }
+
+    /**
+     * Makes a JSON message of the given type.
+     *
+     * @param msgType the message type
+     * @return a JSON message of the given type
+     */
+    private String makeMessage(String msgType) {
+        String json = "{'" + TYPE_FIELD + "':'" + msgType + "', 'abc':'def'}";
+        return json.replace('\'', '"');
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/ScoListenerTest.java b/policy-common/src/test/java/org/onap/policy/common/endpoints/listeners/ScoListenerTest.java
new file mode 100644 (file)
index 0000000..cb8fb70
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * ============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.endpoints.listeners;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+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.onap.policy.common.message.bus.event.Topic.CommInfrastructure;
+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.onap.policy.common.utils.coder.StandardCoderObject;
+import org.onap.policy.common.utils.test.log.logback.ExtractAppender;
+import org.slf4j.LoggerFactory;
+
+class ScoListenerTest {
+
+    /**
+     * Used to attach an appender to the class' logger.
+     */
+    private static final Logger logger = (Logger) LoggerFactory.getLogger(ScoListener.class);
+    private static final ExtractAppender appender = new ExtractAppender();
+
+    /**
+     * Original logging level for the logger.
+     */
+    private static Level saveLevel;
+
+    private static final CommInfrastructure INFRA = CommInfrastructure.NOOP;
+    private static final String TOPIC = "my-topic";
+    private static final String NAME = "pdp_1";
+
+    private static final Coder coder = new StandardCoder();
+
+    private ScoListener<MyMessage> primary;
+
+    /**
+     * Initializes statics.
+     */
+    @BeforeAll
+    public static void setUpBeforeClass() {
+        saveLevel = logger.getLevel();
+        logger.setLevel(Level.INFO);
+
+        appender.setContext(logger.getLoggerContext());
+        appender.start();
+    }
+
+    @AfterAll
+    public static void tearDownAfterClass() {
+        logger.setLevel(saveLevel);
+        appender.stop();
+    }
+
+    /**
+     * Create various mocks and primary handler.
+     */
+    @BeforeEach
+    public void setUp() {
+        appender.clearExtractions();
+
+        primary = new ScoListener<MyMessage>(MyMessage.class) {
+            @Override
+            public void onTopicEvent(CommInfrastructure infra, String topic, StandardCoderObject sco,
+                            MyMessage message) {
+                // do nothing
+            }
+        };
+    }
+
+    @AfterEach
+    public void tearDown() {
+        logger.detachAppender(appender);
+    }
+
+    @Test
+    void testOnTopicEvent() {
+        primary = spy(primary);
+
+        MyMessage status = new MyMessage(NAME);
+        StandardCoderObject sco = makeSco(status);
+        primary.onTopicEvent(INFRA, TOPIC, sco);
+        verify(primary).onTopicEvent(INFRA, TOPIC, sco, status);
+
+        assertFalse(appender.getExtracted().toString().contains("unable to decode"));
+
+        // undecodable message
+        logger.addAppender(appender);
+        primary.onTopicEvent(INFRA, TOPIC, makeSco());
+        verify(primary, times(1)).onTopicEvent(INFRA, TOPIC, sco, status);
+        assertTrue(appender.getExtracted().toString().contains("unable to decode"));
+    }
+
+    /**
+     * Makes a standard object from a JSON string.
+     *
+     * @return a standard object representing the message
+     */
+    private StandardCoderObject makeSco() {
+        try {
+            return coder.decode("[]", StandardCoderObject.class);
+
+        } catch (CoderException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Makes a standard object from a status message.
+     *
+     * @param source message to be converted
+     * @return a standard object representing the message
+     */
+    private StandardCoderObject makeSco(MyMessage source) {
+        try {
+            return coder.toStandard(source);
+
+        } catch (CoderException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    protected static class MyMessage {
+        private String name;
+
+        public MyMessage() {
+            super();
+        }
+
+        public MyMessage(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((name == null) ? 0 : name.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            MyMessage other = (MyMessage) obj;
+            if (name == null) {
+                return other.name == null;
+            } else {
+                return name.equals(other.name);
+            }
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/DoubleConverterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/DoubleConverterTest.java
new file mode 100644 (file)
index 0000000..72cb41d
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * ============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.gson;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class DoubleConverterTest {
+
+    @Test
+    void testConvertFromDoubleObject() {
+        // these should be unchanged
+        assertNull(DoubleConverter.convertFromDouble((Object) null));
+        assertEquals("hello", DoubleConverter.convertFromDouble("hello"));
+        assertEquals("10.0", DoubleConverter.convertFromDouble("10.0"));
+        assertEquals(12.5, DoubleConverter.convertFromDouble(12.5));
+        assertEquals(12, DoubleConverter.convertFromDouble(12));
+        assertEquals(12L, DoubleConverter.convertFromDouble(12L));
+
+        // positive and negative int
+        assertEquals(10, DoubleConverter.convertFromDouble(10.0));
+        assertEquals(-10, DoubleConverter.convertFromDouble(-10.0));
+
+        // positive and negative long
+        assertEquals(100000000000L, DoubleConverter.convertFromDouble(100000000000.0));
+        assertEquals(-100000000000L, DoubleConverter.convertFromDouble(-100000000000.0));
+
+        // list
+        List<Object> list = new ArrayList<>();
+        list.add("list");
+        list.add(null);
+        list.add(21.0);
+        list = (List<Object>) DoubleConverter.convertFromDouble((Object) list);
+        assertEquals("[list, null, 21]", list.toString());
+
+        // map
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("map-A", "map-value");
+        map.put("map-B", null);
+        map.put("map-C", 22.0);
+        map = (Map<String, Object>) DoubleConverter.convertFromDouble((Object) map);
+        assertEquals("{map-A=map-value, map-B=null, map-C=22}", map.toString());
+    }
+
+    @Test
+    void testConvertFromDoubleList() {
+        // null is ok
+        DoubleConverter.convertFromDouble((List<Object>) null);
+
+        List<Object> list = new ArrayList<>();
+        list.add("world");
+        list.add(20.0);
+
+        List<Object> nested = new ArrayList<>();
+        list.add(nested);
+        nested.add(30.0);
+
+        DoubleConverter.convertFromDouble(list);
+
+        assertEquals("[world, 20, [30]]", list.toString());
+    }
+
+    @Test
+    void testConvertFromDoubleMap() {
+        // null is ok
+        DoubleConverter.convertFromDouble((Map<String, Object>) null);
+
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("keyA", "valueA");
+        map.put("keyB", 200.0);
+
+        Map<String, Object> nested = new LinkedHashMap<>();
+        map.put("keyC", nested);
+        nested.put("nested-key", 201.0);
+
+        DoubleConverter.convertFromDouble(map);
+        assertEquals("{keyA=valueA, keyB=200, keyC={nested-key=201}}", map.toString());
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/GsonMessageBodyHandlerTest.java
new file mode 100644 (file)
index 0000000..579b9fd
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2023-2025 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.gson;
+
+import static org.assertj.core.api.Assertions.assertThat;
+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 jakarta.ws.rs.core.MediaType;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import lombok.ToString;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class GsonMessageBodyHandlerTest {
+    private static final String GEN_TYPE = "some-type";
+    private static final String[] subtypes = {"json", "jSoN", "hello+json", "javascript", "x-javascript", "x-json"};
+
+    @SuppressWarnings("rawtypes")
+    private static final Class GEN_CLASS = MyObject.class;
+
+    @SuppressWarnings("unchecked")
+    private static final Class<Object> CLASS_OBJ = GEN_CLASS;
+
+    private GsonMessageBodyHandler hdlr;
+
+    @BeforeEach
+    void setUp() {
+        hdlr = new GsonMessageBodyHandler();
+    }
+
+    @Test
+    void testIsWriteable() {
+        // null media type
+        assertTrue(hdlr.isWriteable(null, null, null, null));
+
+        for (String subtype : subtypes) {
+            assertTrue(hdlr.isWriteable(null, null, null, new MediaType(GEN_TYPE, subtype)), "writeable " + subtype);
+
+        }
+
+        // the remaining should be FALSE
+
+        // null subtype
+        assertFalse(hdlr.isWriteable(null, null, null, new MediaType(GEN_TYPE, null)));
+
+        // text subtype
+        assertFalse(hdlr.isWriteable(null, null, null, MediaType.TEXT_HTML_TYPE));
+    }
+
+    @Test
+    void testGetSize() {
+        assertEquals(-1, hdlr.getSize(null, null, null, null, null));
+    }
+
+    @Test
+    void testWriteTo_testReadFrom() throws Exception {
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        MyObject obj1 = new MyObject(10);
+        hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr);
+
+        Object obj2 = hdlr.readFrom(CLASS_OBJ, CLASS_OBJ, null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+        assertEquals(obj1.toString(), obj2.toString());
+    }
+
+    @Test
+    void testWriteTo_DifferentTypes() throws Exception {
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+
+        // use a derived type, but specify the base type when writing
+        MyObject obj1 = new MyObject(10) {};
+        hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr);
+
+        Object obj2 = hdlr.readFrom(CLASS_OBJ, CLASS_OBJ, null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+        assertEquals(obj1.toString(), obj2.toString());
+    }
+
+    @Test
+    void testIsReadable() {
+        // null media type
+        assertTrue(hdlr.isReadable(null, null, null, null));
+
+        // null subtype
+        assertFalse(hdlr.isReadable(null, null, null, new MediaType(GEN_TYPE, null)));
+
+        for (String subtype : subtypes) {
+            assertTrue(hdlr.isReadable(null, null, null, new MediaType(GEN_TYPE, subtype)), "readable " + subtype);
+
+        }
+
+        // the remaining should be FALSE
+
+        // null subtype
+        assertFalse(hdlr.isReadable(null, null, null, new MediaType(GEN_TYPE, null)));
+
+        // text subtype
+        assertFalse(hdlr.isReadable(null, null, null, MediaType.TEXT_HTML_TYPE));
+    }
+
+    @Test
+    void testReadFrom_DifferentTypes() throws Exception {
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        MyObject obj1 = new MyObject(10);
+        hdlr.writeTo(obj1, obj1.getClass(), CLASS_OBJ, null, null, null, outstr);
+
+        // use a derived type, but specify the base type when reading
+        @SuppressWarnings("rawtypes")
+        Class clazz = MyObject.class;
+
+        @SuppressWarnings("unchecked")
+        Class<Object> objclazz = clazz;
+
+        Object obj2 = hdlr.readFrom(objclazz, CLASS_OBJ, null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+        assertEquals(obj1.toString(), obj2.toString());
+    }
+
+    @Test
+    void testMapDouble() throws Exception {
+        MyMap map = new MyMap();
+        map.props = new HashMap<>();
+        map.props.put("plainString", "def");
+        map.props.put("negInt", -10);
+        map.props.put("doubleVal", 12.5);
+        map.props.put("posLong", 100000000000L);
+
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        hdlr.writeTo(map, map.getClass(), map.getClass(), null, null, null, outstr);
+
+        Object obj2 = hdlr.readFrom(Object.class, map.getClass(), null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+        assertEquals(map.toString(), obj2.toString());
+
+        map = (MyMap) obj2;
+
+        assertEquals(-10, map.props.get("negInt"));
+        assertEquals(100000000000L, map.props.get("posLong"));
+        assertEquals(12.5, map.props.get("doubleVal"));
+    }
+
+    @Test
+    void testInterestingFields() throws IOException {
+        InterestingFields data = new InterestingFields();
+        data.instant = Instant.ofEpochMilli(1583249713500L);
+        data.uuid = UUID.fromString("a850cb9f-3c5e-417c-abfd-0679cdcd1ab0");
+        data.localDate = LocalDateTime.of(2020, 2, 3, 4, 5, 6, 789000000);
+        data.zonedDate = ZonedDateTime.of(2020, 2, 3, 4, 5, 6, 789000000, ZoneId.of("US/Eastern"));
+
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        hdlr.writeTo(data, data.getClass(), data.getClass(), null, null, null, outstr);
+
+        // ensure fields are encoded as expected
+
+        // @formatter:off
+        assertThat(outstr.toString(StandardCharsets.UTF_8))
+                            .contains("\"2020-03-03T15:35:13.500Z\"")
+                            .contains("\"2020-02-03T04:05:06.789\"")
+                            .contains("\"2020-02-03T04:05:06.789-05:00[US/Eastern]\"")
+                            .contains("a850cb9f-3c5e-417c-abfd-0679cdcd1ab0");
+        // @formatter:on
+
+        Object obj2 = hdlr.readFrom(Object.class, data.getClass(), null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+        assertEquals(data.toString(), obj2.toString());
+    }
+
+
+    @ToString
+    public static class MyObject {
+        private int id;
+
+        public MyObject() {
+            super();
+        }
+
+        public MyObject(int id) {
+            this.id = id;
+        }
+    }
+
+    private static class MyMap {
+        private Map<String, Object> props;
+
+        @Override
+        public String toString() {
+            return props.toString();
+        }
+    }
+
+    @ToString
+    private static class InterestingFields {
+        private LocalDateTime localDate;
+        private Instant instant;
+        private UUID uuid;
+        private ZonedDateTime zonedDate;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/InstantAsMillisTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..f2ddf06
--- /dev/null
@@ -0,0 +1,67 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.gson;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.time.Instant;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+
+class InstantAsMillisTypeAdapterTest {
+    private static Gson gson =
+                    new GsonBuilder().registerTypeAdapter(Instant.class, new InstantAsMillisTypeAdapter()).create();
+
+    @Test
+    void test() {
+        InterestingFields data = new InterestingFields();
+        data.instant = Instant.ofEpochMilli(1583249713500L);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("nanos").contains("1583249713500").doesNotContain("\"1583249713500\"")
+                        .doesNotContain("T");
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null output
+        data.instant = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"instant\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+
+    @ToString
+    private static class InterestingFields {
+        private Instant instant;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/InstantTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/InstantTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..2e27d1d
--- /dev/null
@@ -0,0 +1,64 @@
+/*-
+ * ============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.gson;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.Instant;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+
+class InstantTypeAdapterTest {
+    private static Gson gson =
+                    new GsonBuilder().registerTypeAdapter(Instant.class, new InstantTypeAdapter()).create();
+
+    @Test
+    void test() {
+        InterestingFields data = new InterestingFields();
+        data.instant = Instant.ofEpochMilli(1583249713500L);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("nanos").contains("\"2020-03-03T15:35:13.500Z\"")
+                        .doesNotContain("1583249713500");
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("2020", "invalid-date");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid date");
+    }
+
+
+    @ToString
+    private static class InterestingFields {
+        private Instant instant;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/JacksonExclusionStrategyTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonExclusionStrategyTest.java
new file mode 100644 (file)
index 0000000..fc3c667
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved.
+ * Modificaitons Copyright (C) 2023-2025 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.gson;
+
+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 com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import java.io.Serial;
+import java.lang.reflect.GenericArrayType;
+import java.util.LinkedList;
+import java.util.TreeMap;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class JacksonExclusionStrategyTest {
+
+    private static JacksonExclusionStrategy strategy;
+    private static Gson gson;
+
+    @BeforeAll
+    public static void setUpBeforeClass() {
+        strategy = new JacksonExclusionStrategy();
+        gson = new GsonBuilder().setExclusionStrategies(strategy).create();
+    }
+
+    @Test
+    void testWithGson() {
+        Derived data = new Derived();
+        data.setId(10);
+        data.setText("some text");
+        data.setValue("some value");
+
+        // no fields should be serialized
+        String result = gson.toJson(data);
+        assertEquals("{}", result);
+
+        // no fields should be deserialized
+        result = "{'id':20, 'text':'my text', 'value':'my value'}".replace('\'', '"');
+        Derived data2 = gson.fromJson(result, Derived.class);
+        assertEquals(new Derived().toString(), data2.toString());
+    }
+
+    @Test
+    void testShouldSkipField() throws Exception {
+        // should skip every field of Data
+        assertTrue(strategy.shouldSkipField(new FieldAttributes(Data.class.getDeclaredField("id"))));
+        assertTrue(strategy.shouldSkipField(new FieldAttributes(Data.class.getDeclaredField("text"))));
+
+        // should not skip fields in Map
+        assertFalse(strategy.shouldSkipField(new FieldAttributes(MyMap.class.getDeclaredField("mapId"))));
+    }
+
+    @Test
+    void testShouldSkipClass() {
+        assertFalse(strategy.shouldSkipClass(null));
+        assertFalse(strategy.shouldSkipClass(Object.class));
+    }
+
+    @Test
+    void testIsManaged() {
+        // these classes SHOULD be managed
+        Class<?>[] managed = {Data.class, Intfc.class, com.google.gson.TypeAdapter.class};
+
+        for (Class<?> clazz : managed) {
+            assertTrue(JacksonExclusionStrategy.isManaged(clazz), clazz.getName());
+        }
+
+        // generic classes should NOT be managed
+        Class<?>[] unmanaged = {
+            Data[].class, Enum.class, boolean.class, byte.class, short.class, int.class,
+            long.class, float.class, double.class, char.class, Boolean.class, Byte.class, Short.class,
+            Integer.class, Long.class, Float.class, Double.class, Character.class, String.class,
+            MyMap.class, MyList.class, MyJson.class, GenericArrayType.class};
+
+        for (Class<?> clazz : unmanaged) {
+            assertFalse(JacksonExclusionStrategy.isManaged(clazz), clazz.getName());
+        }
+    }
+
+    /**
+     * Used to verify that no fields are exposed.
+     */
+    @Getter
+    @Setter
+    @ToString
+    public static class Data {
+        private int id;
+        public String text;
+
+        void setId(int id) {
+            this.id = id;
+        }
+
+        void setText(String text) {
+            this.text = text;
+        }
+    }
+
+    @Getter
+    @ToString(callSuper = true)
+    public static class Derived extends Data {
+        protected String value;
+
+        void setValue(String value) {
+            this.value = value;
+        }
+    }
+
+    /**
+     * Used to verify that enums are not managed.
+     */
+    public enum Enum {
+        UP, DOWN,
+    }
+
+    /**
+     * Used to verify that interfaces <i>are</i> managed.
+     */
+    public interface Intfc {
+        int getId();
+    }
+
+    /**
+     * Used to verify that Maps are not managed.
+     */
+    @Getter
+    public static class MyMap extends TreeMap<String, Data> {
+        @Serial
+        private static final long serialVersionUID = 1L;
+
+        private int mapId;
+
+    }
+
+    /**
+     * Used to verify that Collections are not managed.
+     */
+    public static class MyList extends LinkedList<Data> {
+        @Serial
+        private static final long serialVersionUID = 1L;
+    }
+
+    /**
+     * Used to verify that JsonElements are not managed.
+     */
+    @SuppressWarnings("deprecation")
+    public static class MyJson extends JsonElement {
+        @Override
+        public JsonElement deepCopy() {
+            return null;
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/JacksonFieldAdapterFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonFieldAdapterFactoryTest.java
new file mode 100644 (file)
index 0000000..65bf90f
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+ * ============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.gson;
+
+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.assertNull;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.annotation.GsonJsonIgnore;
+import org.onap.policy.common.gson.annotation.GsonJsonProperty;
+
+class JacksonFieldAdapterFactoryTest {
+
+    private static JacksonFieldAdapterFactory factory = new JacksonFieldAdapterFactory();
+
+    private static Gson gson = new GsonBuilder().setExclusionStrategies(new JacksonExclusionStrategy())
+                    .registerTypeAdapterFactory(factory).create();
+
+    @Test
+    void testCreate() {
+        // unhandled types
+        assertNull(factory.create(gson, TypeToken.get(JsonElement.class)));
+        assertNull(factory.create(gson, TypeToken.get(NothingToSerialize.class)));
+
+        assertNotNull(factory.create(gson, TypeToken.get(Data.class)));
+        assertNotNull(factory.create(gson, TypeToken.get(Derived.class)));
+
+        Data data = new Data();
+
+        // deserialize using fields that aren't in the Data object
+        Data data2 = gson.fromJson("{\"abc\":100}", Data.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // now work with valid fields
+        data.id = 10;
+        data.text = "hello";
+
+        String result = gson.toJson(data);
+        data2 = gson.fromJson(result, Data.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // should also work with derived types
+        Derived der = new Derived();
+        der.setId(20);
+        der.text = "world";
+        der.unserialized = "abc";
+
+        result = gson.toJson(der);
+
+        // should not contain the unserialized field
+        assertFalse(result.contains("abc"));
+
+        Derived der2 = gson.fromJson(result, Derived.class);
+        der.unserialized = null;
+        assertEquals(der.toString(), der2.toString());
+    }
+
+    @Test
+    void testCreate_Lists() {
+        DataList lst = new DataList();
+        lst.theList = new ArrayList<>();
+        lst.theList.add(new Data(200, "text 20"));
+        lst.theList.add(new Data(210, "text 21"));
+
+        String result = gson.toJson(lst);
+        assertEquals("{'theList':[{'my-id':200,'text':'text 20'},{'my-id':210,'text':'text 21'}]}".replace('\'', '"'),
+                        result);
+
+        DataList lst2 = gson.fromJson(result, DataList.class);
+        assertEquals(stripIdent(lst.toString()), stripIdent(lst2.toString()));
+        assertEquals(Data.class, lst2.theList.get(0).getClass());
+    }
+
+    @Test
+    void testCreate_OnlyOutProps() {
+        InFieldIgnored data = new InFieldIgnored();
+        data.value = "out only";
+
+        // field should be serialized
+        String result = gson.toJson(data);
+        assertEquals("{'value':'out only'}".replace('\'', '"'), result);
+
+        // field should NOT be deserialized
+        data = gson.fromJson(result, InFieldIgnored.class);
+        assertNull(data.value);
+    }
+
+    @Test
+    void testCreate_OnlyInProps() {
+        OutFieldIgnored data = new OutFieldIgnored();
+        data.value = "in only";
+
+        // field should NOT be serialized
+        String result = gson.toJson(data);
+        assertEquals("{}", result);
+
+        // field should NOT be deserialized
+        data = gson.fromJson("{'value':'in only'}".replace('\'', '"'), OutFieldIgnored.class);
+        assertEquals("in only", data.value);
+    }
+
+    /**
+     * Object identifiers may change with each execution, so this method is used to strip
+     * the identifier from the text string so that the strings will still match across
+     * different runs.
+     *
+     * @param text text from which to strip the identifier
+     * @return the text, without the identifier
+     */
+    private String stripIdent(String text) {
+        return text.replaceFirst("@\\w+", "@");
+    }
+
+    @ToString
+    private static class Data {
+        @GsonJsonProperty("my-id")
+        private int id;
+
+        public String text;
+
+        public Data() {
+            super();
+        }
+
+        public Data(int id, String text) {
+            this.id = id;
+            this.text = text;
+        }
+
+        void setId(int id) {
+            this.id = id;
+        }
+    }
+
+    @ToString(callSuper = true)
+    private static class Derived extends Data {
+        // not serialized
+        private String unserialized;
+    }
+
+    private static class DataList {
+        @GsonJsonProperty
+        private List<Data> theList;
+    }
+
+    protected static class NothingToSerialize {
+        // not serialized
+        protected String unserialized;
+    }
+
+    /**
+     * This has a field that should show up in the "output" list, but not in the "input"
+     * list, because the method will override it.
+     */
+    private static class InFieldIgnored {
+        @GsonJsonProperty("value")
+        private String value;
+
+        @GsonJsonIgnore
+        public void setValue(String value) {
+            this.value = value;
+        }
+    }
+
+    /**
+     * This has a field that should show up in the "input" list, but not in the "output"
+     * list, because the method will override it.
+     */
+    private static class OutFieldIgnored {
+        @GsonJsonProperty("value")
+        private String value;
+
+        @GsonJsonIgnore
+        public String getValue() {
+            return value;
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/JacksonHandlerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonHandlerTest.java
new file mode 100644 (file)
index 0000000..316104c
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019, 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.gson;
+
+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 com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import jakarta.ws.rs.core.MediaType;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter;
+import org.onap.policy.common.gson.annotation.GsonJsonAnySetter;
+
+class JacksonHandlerTest {
+
+    @Test
+    void test() throws Exception {
+        JacksonHandler hdlr = new JacksonHandler();
+
+        assertTrue(hdlr.isReadable(null, null, null, MediaType.APPLICATION_JSON_TYPE));
+        assertFalse(hdlr.isReadable(null, null, null, MediaType.TEXT_PLAIN_TYPE));
+
+        JsonObject expected = new JsonObject();
+        expected.addProperty("myId", 100);
+        expected.addProperty("value", "a value");
+        expected.addProperty("abc", "def");
+        expected.addProperty("hello", "world");
+
+        Data data = new Data();
+        data.id = 10;
+        data.value = "a value";
+        data.props = new HashMap<>();
+        data.props.put("abc", "def");
+        data.props.put("hello", "world");
+
+        /*
+         * Ensure everything serializes as expected.
+         */
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        hdlr.writeTo(data, Data.class, Data.class, null, null, null, outstr);
+
+        StringReader rdr = new StringReader(outstr.toString("UTF-8"));
+        JsonObject json = new Gson().fromJson(rdr, JsonObject.class);
+
+        assertEquals(expected, json);
+
+        /*
+         * Ensure everything deserializes as expected.
+         */
+        Data data2 = (Data) hdlr.readFrom(Object.class, Data.class, null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+
+        // id is not serialized, so we must copy it manually before comparing
+        data2.id = data.id;
+
+        assertEquals(data.toString(), data2.toString());
+    }
+
+    @Test
+    void testMapDouble() throws Exception {
+        MyMap map = new MyMap();
+        map.props = new HashMap<>();
+        map.props.put("plainString", "def");
+        map.props.put("negInt", -10);
+        map.props.put("doubleVal", 12.5);
+        map.props.put("posLong", 100000000000L);
+
+        JacksonHandler hdlr = new JacksonHandler();
+        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
+        hdlr.writeTo(map, map.getClass(), map.getClass(), null, null, null, outstr);
+
+        Object obj2 = hdlr.readFrom(Object.class, map.getClass(), null, null, null,
+                        new ByteArrayInputStream(outstr.toByteArray()));
+        assertEquals(map.toString(), obj2.toString());
+
+        map = (MyMap) obj2;
+
+        assertEquals(-10, map.props.get("negInt"));
+        assertEquals(100000000000L, map.props.get("posLong"));
+        assertEquals(12.5, map.props.get("doubleVal"));
+    }
+
+    /**
+     * This class includes all policy-specific gson annotations.
+     */
+    @ToString
+    public static class Data {
+        protected int id;
+
+        protected String value;
+
+        protected Map<String, String> props;
+
+        public int getMyId() {
+            return 100;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public void setValue(String value) {
+            this.value = value;
+        }
+
+        @GsonJsonAnyGetter
+        public Map<String, String> getProps() {
+            return props;
+        }
+
+        /**
+         * Sets a property.
+         *
+         * @param name property name
+         * @param value new value
+         */
+        @GsonJsonAnySetter
+        public void setProperty(String name, String value) {
+            if (props == null) {
+                props = new TreeMap<>();
+            }
+
+            props.put(name, value);
+        }
+    }
+
+    private static class MyMap {
+        private Map<String, Object> props;
+
+        @Override
+        public String toString() {
+            return props.toString();
+        }
+
+        @SuppressWarnings("unused")
+        public Map<String, Object> getProps() {
+            return props;
+        }
+
+        @SuppressWarnings("unused")
+        public void setProps(Map<String, Object> props) {
+            this.props = props;
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/JacksonMethodAdapterFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/JacksonMethodAdapterFactoryTest.java
new file mode 100644 (file)
index 0000000..4199694
--- /dev/null
@@ -0,0 +1,295 @@
+/*
+ * ============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.gson;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+import java.util.Map;
+import java.util.TreeMap;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter;
+import org.onap.policy.common.gson.annotation.GsonJsonAnySetter;
+import org.onap.policy.common.gson.annotation.GsonJsonIgnore;
+import org.onap.policy.common.gson.annotation.GsonJsonProperty;
+
+class JacksonMethodAdapterFactoryTest {
+
+    private static JacksonMethodAdapterFactory factory = new JacksonMethodAdapterFactory();
+
+    private static Gson gson = new GsonBuilder().setExclusionStrategies(new JacksonExclusionStrategy())
+                    .registerTypeAdapterFactory(factory).create();
+
+    @Test
+    void testCreate() {
+        // unhandled types
+        assertNull(factory.create(gson, TypeToken.get(JsonElement.class)));
+        assertNull(factory.create(gson, TypeToken.get(NothingToSerialize.class)));
+
+        assertNotNull(factory.create(gson, TypeToken.get(Data.class)));
+        assertNotNull(factory.create(gson, TypeToken.get(Derived.class)));
+        assertNotNull(factory.create(gson, TypeToken.get(OnlyGetters.class)));
+        assertNotNull(factory.create(gson, TypeToken.get(OnlySetters.class)));
+        assertNotNull(factory.create(gson, TypeToken.get(OnlyAnyGetter.class)));
+        assertNotNull(factory.create(gson, TypeToken.get(OnlyAnySetter.class)));
+
+        // unhandled type
+
+        Data data = new Data();
+        data.id = 10;
+        data.text = "some text";
+
+        String result = gson.toJson(data);
+        Data data2 = gson.fromJson(result, Data.class);
+        assertEquals(data.toString(), data2.toString());
+
+        Derived der = new Derived();
+        der.setId(20);
+        der.setText("hello");
+        der.text = "world";
+        der.map = new TreeMap<>();
+        der.map.put("mapA", "valA");
+        der.map.put("mapB", "valB");
+
+        result = gson.toJson(der);
+
+        // should not contain the unserialized fields
+        assertFalse(result.contains("hello"));
+        assertFalse(result.contains("world"));
+
+        // null out unserialized fields
+        der.text = null;
+
+        // null out overridden field
+        der.setText(null);
+
+        Derived der2 = gson.fromJson(result, Derived.class);
+
+        assertEquals(der.toString(), der2.toString());
+
+        // override of AnyGetter
+        AnyGetterOverride dblget = new AnyGetterOverride();
+        dblget.setMap(der.map);
+        dblget.overMap = new TreeMap<>();
+        dblget.overMap.put("getA", 100);
+        dblget.overMap.put("getB", 110);
+
+        String result2 = gson.toJson(dblget);
+        dblget.overMap.keySet().forEach(key -> assertTrue(result2.contains(key), "over contains " + key));
+        der.map.keySet().forEach(key -> assertFalse(result2.contains(key), "sub contains " + key));
+
+        // override of AnySetter
+        Map<String, Integer> map = new TreeMap<>();
+        map.put("setA", 200);
+        map.put("setB", 210);
+        AnySetterOverride dblset = gson.fromJson(gson.toJson(map), AnySetterOverride.class);
+        assertEquals(map.toString(), dblset.overMap.toString());
+        assertNull(dblset.getTheMap());
+
+        // non-static nested class - can serialize, but not de-serialize
+        Container cont = new Container(500, "bye bye");
+        result = gson.toJson(cont);
+        assertEquals("{'id':500,'nested':{'value':'bye bye'}}".replace('\'', '"'), result);
+    }
+
+    @ToString
+    protected static class Data {
+        private int id;
+        private String text;
+
+        public int getId() {
+            return id;
+        }
+
+        public void setId(int id) {
+            this.id = id;
+        }
+
+        // not public, but property provided
+        @GsonJsonProperty("text")
+        protected String getText() {
+            return text;
+        }
+
+        public void setText(String text) {
+            this.text = text;
+        }
+
+        public void unused(String text) {
+            // do nothing
+        }
+    }
+
+    @ToString(callSuper = true)
+    protected static class Derived extends Data {
+
+        // overrides private field from Data
+        public String text;
+
+        private Map<String, String> map;
+
+        @GsonJsonAnyGetter
+        public Map<String, String> getTheMap() {
+            return map;
+        }
+
+        @GsonJsonIgnore
+        public void setMap(Map<String, String> map) {
+            this.map = map;
+        }
+
+        @GsonJsonAnySetter
+        public void setMapValue(String key, String value) {
+            if (map == null) {
+                map = new TreeMap<>();
+            }
+
+            map.put(key, value);
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnyGetter} method that overrides the super class' method.
+     */
+    private static class AnyGetterOverride extends Derived {
+        private Map<String, Integer> overMap;
+
+        @GsonJsonAnyGetter
+        private Map<String, Integer> getOverride() {
+            return overMap;
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnySetter} method that overrides the super class' method.
+     */
+    private static class AnySetterOverride extends Derived {
+        private Map<String, Integer> overMap;
+
+        @GsonJsonAnySetter
+        private void setOverride(String key, int value) {
+            if (overMap == null) {
+                overMap = new TreeMap<>();
+            }
+
+            overMap.put(key, value);
+        }
+    }
+
+    /**
+     * Has nothing to serialize.
+     */
+    protected static class NothingToSerialize {
+        // not serialized
+        protected String unserialized;
+    }
+
+    /**
+     * Only has getters.
+     */
+    protected static class OnlyGetters {
+        public int getId() {
+            return 1010;
+        }
+    }
+
+    /**
+     * Only has setters.
+     */
+    protected static class OnlySetters {
+        public void setId(int id) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Only has {@link GsonJsonAnyGetter}.
+     */
+    private static class OnlyAnyGetter {
+        @GsonJsonAnyGetter
+        public Map<String, Integer> getOverride() {
+            return null;
+        }
+    }
+
+    /**
+     * Only has {@link GsonJsonAnySetter}.
+     */
+    private static class OnlyAnySetter {
+        @GsonJsonAnySetter
+        public void setOverride(String key, int value) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Used to test serialization of non-static nested classes.
+     */
+    @ToString
+    protected static class Container {
+        private int id;
+        private Nested nested;
+
+        public Container() {
+            super();
+        }
+
+        public Container(int id, String value) {
+            this.id = id;
+            this.nested = new Nested(value);
+        }
+
+        public int getId() {
+            return id;
+        }
+
+        public void setId(int id) {
+            this.id = id;
+        }
+
+        public Nested getNested() {
+            return nested;
+        }
+
+
+        @ToString
+        protected class Nested {
+            private String value;
+
+            public Nested(String val) {
+                value = val;
+            }
+
+            public String getValue() {
+                return value;
+            }
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTimeTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..87f3c46
--- /dev/null
@@ -0,0 +1,73 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.gson;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.LocalDateTime;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+
+class LocalDateTimeTypeAdapterTest {
+    private static Gson gson =
+                    new GsonBuilder().registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter()).create();
+
+    @Test
+    void test() {
+        InterestingFields data = new InterestingFields();
+        data.date = LocalDateTime.of(2020, 2, 3, 4, 5, 6, 789000000);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("year").contains("\"2020-02-03T04:05:06.789\"");
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("2020", "invalid-date");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid date");
+
+        // null output
+        data.date = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"date\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+
+    @ToString
+    private static class InterestingFields {
+        private LocalDateTime date;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/LocalDateTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..b62a19b
--- /dev/null
@@ -0,0 +1,74 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.gson;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.LocalDate;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+
+class LocalDateTypeAdapterTest {
+    private static Gson gson =
+            new GsonBuilder().registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter()).create();
+    private static final String TEST_DATE = "2020-01-01";
+
+    @Test
+    void test() {
+        InterestingFields data = new InterestingFields();
+        data.date = LocalDate.parse(TEST_DATE);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("year").contains(TEST_DATE);
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("2020", "invalid-date");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid date");
+
+        // null output
+        data.date = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"date\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+    @ToString
+    private static class InterestingFields {
+        private LocalDate date;
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/MapDoubleAdapterFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/MapDoubleAdapterFactoryTest.java
new file mode 100644 (file)
index 0000000..048e09e
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+ * ============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.gson;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class MapDoubleAdapterFactoryTest {
+    private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(new MapDoubleAdapterFactory()).create();
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void testMap() {
+        MyMap map = new MyMap();
+        map.data = new HashMap<>();
+        map.data.put("plainString", "def");
+        map.data.put("posInt", 10);
+        map.data.put("negInt", -10);
+        map.data.put("doubleVal", 12.5);
+        map.data.put("posLong", 100000000000L);
+        map.data.put("negLong", -100000000000L);
+
+        Map<String, Object> nested = new LinkedHashMap<>();
+        map.data.put("nestedMap", nested);
+        nested.put("nestedString", "world");
+        nested.put("nestedInt", 50);
+
+        String json = gson.toJson(map);
+
+        map.data.clear();
+        map = gson.fromJson(json, MyMap.class);
+
+        assertEquals(json, gson.toJson(map));
+
+        assertEquals(10, map.data.get("posInt"));
+        assertEquals(-10, map.data.get("negInt"));
+        assertEquals(100000000000L, map.data.get("posLong"));
+        assertEquals(-100000000000L, map.data.get("negLong"));
+        assertEquals(12.5, map.data.get("doubleVal"));
+        assertEquals(nested, map.data.get("nestedMap"));
+
+        nested = (Map<String, Object>) map.data.get("nestedMap");
+        assertEquals(50, nested.get("nestedInt"));
+    }
+
+    @Test
+    void testList() {
+        MyList list = new MyList();
+        list.data = new ArrayList<>();
+        list.data.add("ghi");
+        list.data.add(100);
+
+        List<Object> nested = new ArrayList<>();
+        list.data.add(nested);
+        nested.add("world2");
+        nested.add(500);
+
+        String json = gson.toJson(list);
+
+        list.data.clear();
+        list = gson.fromJson(json, MyList.class);
+
+        assertEquals(json, gson.toJson(list));
+
+        assertEquals("[ghi, 100, [world2, 500]]", list.data.toString());
+    }
+
+    @Test
+    void test_ValueIsNotObject() {
+        MyDoubleMap map = new MyDoubleMap();
+        map.data = new LinkedHashMap<>();
+        map.data.put("plainDouble", 13.5);
+        map.data.put("doubleAsInt", 100.0);
+
+        String json = gson.toJson(map);
+
+        map.data.clear();
+        map = gson.fromJson(json, MyDoubleMap.class);
+
+        // everything should still be Double - check by simply accessing
+        assertNotNull(map.data.get("plainDouble"));
+        assertNotNull(map.data.get("doubleAsInt"));
+    }
+
+    @Test
+    void test_KeyIsNotString() {
+        MyObjectMap map = new MyObjectMap();
+
+        map.data = new LinkedHashMap<>();
+        map.data.put("plainDouble2", 14.5);
+        map.data.put("doubleAsInt2", 200.0);
+
+        String json = gson.toJson(map);
+
+        map.data.clear();
+        map = gson.fromJson(json, MyObjectMap.class);
+
+        // everything should still be Double
+        assertEquals(14.5, map.data.get("plainDouble2"));
+        assertEquals(200.0, map.data.get("doubleAsInt2"));
+    }
+
+    @Test
+    void test_ListValueIsNotObject() {
+        MyDoubleList list = new MyDoubleList();
+        list.data = new ArrayList<>();
+        list.data.add(13.5);
+        list.data.add(100.0);
+
+        String json = gson.toJson(list);
+
+        list.data.clear();
+        list = gson.fromJson(json, MyDoubleList.class);
+
+        // everything should still be Double - check by simply accessing
+        assertEquals("[13.5, 100.0]", list.data.toString());
+    }
+
+    private static class MyMap {
+        private Map<String, Object> data;
+    }
+
+    private static class MyDoubleMap {
+        private Map<String, Double> data;
+    }
+
+    private static class MyObjectMap {
+        private Map<Object, Object> data;
+    }
+
+    private static class MyList {
+        private List<Object> data;
+    }
+
+    private static class MyDoubleList {
+        private List<Double> data;
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/OffsetDateTimeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/OffsetDateTimeAdapterTest.java
new file mode 100644 (file)
index 0000000..33212b8
--- /dev/null
@@ -0,0 +1,73 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.gson;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.OffsetDateTime;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+
+class OffsetDateTimeAdapterTest {
+    private static Gson gson =
+            new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter()).create();
+    private static final String TEST_DATE = "2020-01-01T12:00:00.999+05:00";
+
+    @Test
+    void test() {
+        InterestingFields data = new InterestingFields();
+        data.date = OffsetDateTime.parse(TEST_DATE);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("year").contains(TEST_DATE);
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("2020", "invalid-date");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid date");
+
+        // null output
+        data.date = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"date\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+    @ToString
+    private static class InterestingFields {
+        private OffsetDateTime date;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/OffsetTimeTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/OffsetTimeTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..27f179f
--- /dev/null
@@ -0,0 +1,73 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.gson;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.OffsetTime;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+
+class OffsetTimeTypeAdapterTest {
+    private static Gson gson =
+            new GsonBuilder().registerTypeAdapter(OffsetTime.class, new OffsetTimeTypeAdapter()).create();
+    private static final String TEST_TIME = "12:00:00.999+05:00";
+
+    @Test
+    void test() {
+        InterestingFields data = new InterestingFields();
+        data.time = OffsetTime.parse(TEST_TIME);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("foo").contains(TEST_TIME);
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("12", "invalid-time");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid time");
+
+        // null output
+        data.time = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"time\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+    @ToString
+    private static class InterestingFields {
+        private OffsetTime time;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/StringTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/StringTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..a202751
--- /dev/null
@@ -0,0 +1,95 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.gson;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+
+class StringTypeAdapterTest {
+    private static Gson gson = new GsonBuilder().registerTypeAdapter(MyData.class, new MyAdapter()).create();
+    private static final int TEST_NUM1 = 10;
+    private static final int TEST_NUM3 = 30;
+
+    @Test
+    void test() {
+        InterestingFields data = new InterestingFields();
+        data.data1 = new MyData(TEST_NUM1);
+        data.data2 = null;
+        data.data3 = new MyData(TEST_NUM3);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).contains("10", "30");
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the string is invalid
+        String json2 = json.replace("30", "invalid-value");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid data");
+
+        // null output
+        data = new InterestingFields();
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"data1\":null, \"data1\":null, \"data1\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // empty input
+        data2 = gson.fromJson("{}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+    @Getter
+    @ToString
+    @AllArgsConstructor
+    private static class MyData {
+        private int num;
+    }
+
+    @ToString
+    private static class InterestingFields {
+        private MyData data1;
+        private MyData data2;
+        private MyData data3;
+    }
+
+    private static class MyAdapter extends StringTypeAdapter<MyData> {
+        public MyAdapter() {
+            super("data", string -> new MyData(Integer.parseInt(string)), data -> String.valueOf(data.num));
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/ZoneOffsetTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..0e4e958
--- /dev/null
@@ -0,0 +1,73 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.gson;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.ZoneOffset;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+
+class ZoneOffsetTypeAdapterTest {
+    private static final Gson gson =
+            new GsonBuilder().registerTypeAdapter(ZoneOffset.class, new ZoneOffsetTypeAdapter()).create();
+    private static final String TEST_ZONE = "+05:00";
+
+    @Test
+    void test() {
+        InterestingFields data = new InterestingFields();
+        data.zone = ZoneOffset.of(TEST_ZONE);
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("foo").contains(TEST_ZONE);
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("05", "invalid-zone");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid zone");
+
+        // null output
+        data.zone = null;
+        json = gson.toJson(data);
+        data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // null input
+        data2 = gson.fromJson("{\"zone\":null}", InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+    }
+
+    @ToString
+    private static class InterestingFields {
+        private ZoneOffset zone;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/ZonedDateTimeTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..bf534a5
--- /dev/null
@@ -0,0 +1,64 @@
+/*-
+ * ============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.gson;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+
+class ZonedDateTimeTypeAdapterTest {
+    private static Gson gson =
+                    new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter()).create();
+
+    @Test
+    void test() {
+        InterestingFields data = new InterestingFields();
+        data.date = ZonedDateTime.of(2020, 2, 3, 4, 5, 6, 789000000, ZoneId.of("US/Eastern"));
+
+        String json = gson.toJson(data);
+
+        // instant should be encoded as a number, without quotes
+        assertThat(json).doesNotContain("year").contains("\"2020-02-03T04:05:06.789-05:00[US/Eastern]\"");
+
+        InterestingFields data2 = gson.fromJson(json, InterestingFields.class);
+        assertEquals(data.toString(), data2.toString());
+
+        // try when the date-time string is invalid
+        String json2 = json.replace("2020", "invalid-date");
+        assertThatThrownBy(() -> gson.fromJson(json2, InterestingFields.class)).isInstanceOf(JsonParseException.class)
+                        .hasMessageContaining("invalid date");
+    }
+
+
+    @ToString
+    private static class InterestingFields {
+        private ZonedDateTime date;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/AdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/AdapterTest.java
new file mode 100644 (file)
index 0000000..ebe2b72
--- /dev/null
@@ -0,0 +1,427 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019-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.gson.internal;
+
+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.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.JacksonExclusionStrategy;
+import org.onap.policy.common.gson.annotation.GsonJsonProperty;
+import org.onap.policy.common.gson.internal.Adapter.Factory;
+import org.onap.policy.common.gson.internal.DataAdapterFactory.Data;
+import org.onap.policy.common.gson.internal.DataAdapterFactory.DerivedData;
+import org.springframework.test.util.ReflectionTestUtils;
+
+class AdapterTest {
+    private static final String GET_INVALID_NAME = "get$InvalidName";
+    private static final String SET_INVALID_NAME = "set$InvalidName";
+    private static final String EMPTY_ALIAS = "emptyAlias";
+    private static final String GET_VALUE = ".getValue";
+    private static final String GET_VALUE_NAME = "getValue";
+    private static final String SET_VALUE_NAME = "setValue";
+    private static final String VALUE_NAME = "value";
+    private static final String MY_NAME = AdapterTest.class.getName();
+    private static final String FACTORY_FIELD = "factory";
+
+    private static DataAdapterFactory dataAdapter = new DataAdapterFactory();
+
+    private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter)
+                    .setExclusionStrategies(new JacksonExclusionStrategy()).create();
+
+    private static Factory saveFactory;
+
+    /*
+     * The remaining fields are just used within the tests.
+     */
+
+    private String value;
+
+    // empty alias - should use field name
+    @GsonJsonProperty("")
+    protected String emptyAlias;
+
+    @GsonJsonProperty("name-with-alias")
+    protected String nameWithAlias;
+
+    protected String unaliased;
+
+    private List<Data> listField;
+
+    private Data dataField;
+
+    @BeforeAll
+    public static void setUpBeforeClass() {
+        saveFactory = (Factory) ReflectionTestUtils.getField(Adapter.class, FACTORY_FIELD);
+    }
+
+    @AfterEach
+    void tearDown() {
+        ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, saveFactory);
+    }
+
+    @Test
+    void testIsManagedField() {
+        assertTrue(Adapter.isManaged(field(VALUE_NAME)));
+
+        // return an invalid field name
+        Factory factory = mock(Factory.class);
+        when(factory.getName(any(Field.class))).thenReturn("$invalidFieldName");
+        ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory);
+        assertFalse(Adapter.isManaged(field(VALUE_NAME)));
+    }
+
+    @Test
+    void testIsManagedMethod() {
+        assertTrue(Adapter.isManaged(mget(GET_VALUE_NAME)));
+
+        // return an invalid method name
+        Factory factory = mock(Factory.class);
+        ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory);
+
+        when(factory.getName(any(Method.class))).thenReturn(GET_INVALID_NAME);
+        assertFalse(Adapter.isManaged(mget(GET_VALUE_NAME)));
+
+        when(factory.getName(any(Method.class))).thenReturn(SET_INVALID_NAME);
+        assertFalse(Adapter.isManaged(mset(SET_VALUE_NAME)));
+    }
+
+    @Test
+    void testAdapterField_Converter() {
+        Adapter adapter = new Adapter(gson, field("dataField"));
+
+        // first, write something of type Data
+        dataAdapter.reset();
+        dataField = new Data(300);
+        JsonElement tree = adapter.toJsonTree(dataField);
+        assertEquals("{'id':300}".replace('\'', '"'), tree.toString());
+
+        // now try a subclass
+        dataAdapter.reset();
+        dataField = new DerivedData(300, "three");
+        tree = adapter.toJsonTree(dataField);
+        assertEquals("{'id':300,'text':'three'}".replace('\'', '"'), tree.toString());
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void testAdapterField_Converter_List() {
+        listField = DataAdapterFactory.makeList();
+
+        Adapter adapter = new Adapter(gson, field("listField"));
+
+        dataAdapter.reset();
+        JsonElement tree = adapter.toJsonTree(listField);
+        assertTrue(dataAdapter.isDataWritten());
+        assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString());
+
+        // encode it twice so it uses the cached converter
+        dataAdapter.reset();
+        tree = adapter.toJsonTree(listField);
+        assertTrue(dataAdapter.isDataWritten());
+        assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString());
+
+        dataAdapter.reset();
+        List<Data> lst2 = (List<Data>) adapter.fromJsonTree(tree);
+        assertTrue(dataAdapter.isDataRead());
+
+        assertEquals(listField.toString(), lst2.toString());
+
+        // decode it twice so it uses the cached converter
+        dataAdapter.reset();
+        lst2 = (List<Data>) adapter.fromJsonTree(tree);
+        assertTrue(dataAdapter.isDataRead());
+
+        assertEquals(listField.toString(), lst2.toString());
+    }
+
+    @Test
+    void testAdapterMethod_Converter() throws Exception {
+        listField = DataAdapterFactory.makeList();
+
+        Method getter = mget("getMyList");
+
+        Adapter aget = new Adapter(gson, getter, getter.getReturnType());
+
+        dataAdapter.reset();
+        JsonElement tree = aget.toJsonTree(listField);
+        assertTrue(dataAdapter.isDataWritten());
+        assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString());
+
+        Method setter = AdapterTest.class.getDeclaredMethod("setMyList", List.class);
+        Adapter aset = new Adapter(gson, setter, setter.getGenericParameterTypes()[0]);
+
+        dataAdapter.reset();
+        @SuppressWarnings("unchecked")
+        List<Data> lst2 = (List<Data>) aset.fromJsonTree(tree);
+        assertTrue(dataAdapter.isDataRead());
+
+        assertEquals(listField.toString(), lst2.toString());
+    }
+
+    @Test
+    void testGetPropName_testGetFullName_testMakeError() {
+        // test field
+        Adapter adapter = new Adapter(gson, field(VALUE_NAME));
+
+        assertEquals(VALUE_NAME, adapter.getPropName());
+        assertEquals(MY_NAME + ".value", adapter.getFullName());
+
+
+        // test getter
+        adapter = new Adapter(gson, mget(GET_VALUE_NAME), String.class);
+
+        assertEquals(VALUE_NAME, adapter.getPropName());
+        assertEquals(MY_NAME + GET_VALUE, adapter.getFullName());
+
+        assertEquals("hello: " + MY_NAME + GET_VALUE, adapter.makeError("hello: "));
+
+
+        // test setter
+        adapter = new Adapter(gson, mset(SET_VALUE_NAME), String.class);
+
+        assertEquals(VALUE_NAME, adapter.getPropName());
+        assertEquals(MY_NAME + ".setValue", adapter.getFullName());
+    }
+
+    @Test
+    void testToJsonTree() {
+        Adapter adapter = new Adapter(gson, field(VALUE_NAME));
+
+        JsonElement tree = adapter.toJsonTree("hello");
+        assertTrue(tree.isJsonPrimitive());
+        assertEquals("hello", tree.getAsString());
+    }
+
+    @Test
+    void testFromJsonTree() {
+        Adapter adapter = new Adapter(gson, field(VALUE_NAME));
+
+        assertEquals("world", adapter.fromJsonTree(new JsonPrimitive("world")));
+    }
+
+    @Test
+    void testDetmPropName() {
+        assertEquals(EMPTY_ALIAS, Adapter.detmPropName(field(EMPTY_ALIAS)));
+        assertEquals("name-with-alias", Adapter.detmPropName(field("nameWithAlias")));
+        assertEquals("unaliased", Adapter.detmPropName(field("unaliased")));
+
+        // return an invalid field name
+        Factory factory = mock(Factory.class);
+        when(factory.getName(any(Field.class))).thenReturn("$invalidFieldName");
+        ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory);
+        assertEquals(null, Adapter.detmPropName(field(VALUE_NAME)));
+    }
+
+    @Test
+    void testDetmGetterPropName() {
+        assertEquals(EMPTY_ALIAS, Adapter.detmGetterPropName(mget("getEmptyAlias")));
+        assertEquals("get-with-alias", Adapter.detmGetterPropName(mget("getWithAlias")));
+        assertEquals("plain", Adapter.detmGetterPropName(mget("getPlain")));
+        assertEquals("primBool", Adapter.detmGetterPropName(mget("isPrimBool")));
+        assertEquals("boxedBool", Adapter.detmGetterPropName(mget("isBoxedBool")));
+        assertEquals(null, Adapter.detmGetterPropName(mget("isString")));
+        assertEquals(null, Adapter.detmGetterPropName(mget("noGet")));
+        assertEquals(null, Adapter.detmGetterPropName(mget("get")));
+
+        // return an invalid method name
+        Factory factory = mock(Factory.class);
+        ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory);
+
+        when(factory.getName(any(Method.class))).thenReturn(GET_INVALID_NAME);
+        assertEquals(null, Adapter.detmGetterPropName(mget(GET_VALUE_NAME)));
+    }
+
+    @Test
+    void testDetmSetterPropName() {
+        assertEquals(EMPTY_ALIAS, Adapter.detmSetterPropName(mset("setEmptyAlias")));
+        assertEquals("set-with-alias", Adapter.detmSetterPropName(mset("setWithAlias")));
+        assertEquals("plain", Adapter.detmSetterPropName(mset("setPlain")));
+        assertEquals(null, Adapter.detmSetterPropName(mset("noSet")));
+        assertEquals(null, Adapter.detmSetterPropName(mset("set")));
+
+        // return an invalid method name
+        Factory factory = mock(Factory.class);
+        ReflectionTestUtils.setField(Adapter.class, FACTORY_FIELD, factory);
+
+        when(factory.getName(any(Method.class))).thenReturn(SET_INVALID_NAME);
+        assertEquals(null, Adapter.detmSetterPropName(mset(SET_VALUE_NAME)));
+    }
+
+    @Test
+    void testGetQualifiedNameField() throws Exception {
+        assertEquals(MY_NAME + ".value", Adapter.getQualifiedName(AdapterTest.class.getDeclaredField(VALUE_NAME)));
+    }
+
+    @Test
+    void testGetQualifiedNameMethod() {
+        assertEquals(MY_NAME + GET_VALUE, Adapter.getQualifiedName(mget(GET_VALUE_NAME)));
+    }
+
+    /**
+     * Gets a field from this class, by name.
+     *
+     * @param name name of the field to get
+     * @return the field
+     */
+    private Field field(String name) {
+        try {
+            return AdapterTest.class.getDeclaredField(name);
+
+        } catch (SecurityException | NoSuchFieldException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Gets a "getter" method from this class, by name.
+     *
+     * @param name name of the method to get
+     * @return the method
+     */
+    private Method mget(String name) {
+        try {
+            return AdapterTest.class.getDeclaredMethod(name);
+
+        } catch (NoSuchMethodException | SecurityException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Gets a "setter" method from this class, by name.
+     *
+     * @param name name of the method to get
+     * @return the method
+     */
+    private Method mset(String name) {
+        try {
+            return AdapterTest.class.getDeclaredMethod(name, String.class);
+
+        } catch (NoSuchMethodException | SecurityException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /*
+     * The remaining methods are just used within the tests.
+     */
+
+    protected String getValue() {
+        return value;
+    }
+
+    // empty alias - should use method name
+    @GsonJsonProperty("")
+    protected String getEmptyAlias() {
+        return "";
+    }
+
+    @GsonJsonProperty("get-with-alias")
+    protected String getWithAlias() {
+        return "";
+    }
+
+    // no alias, begins with "get"
+    protected String getPlain() {
+        return "";
+    }
+
+    // begins with "is", returns primitive boolean
+    protected boolean isPrimBool() {
+        return true;
+    }
+
+    // begins with "is", returns boxed Boolean
+    protected Boolean isBoxedBool() {
+        return true;
+    }
+
+    // begins with "is", but doesn't return a boolean
+    protected String isString() {
+        return "";
+    }
+
+    // doesn't begin with "get"
+    protected String noGet() {
+        return "";
+    }
+
+    // nothing after "get"
+    protected String get() {
+        return "";
+    }
+
+
+    protected void setValue(String text) {
+        // do nothing
+    }
+
+    // empty alias - should use method name
+    @GsonJsonProperty("")
+    protected void setEmptyAlias(String text) {
+        // do nothing
+    }
+
+    @GsonJsonProperty("set-with-alias")
+    protected void setWithAlias(String text) {
+        // do nothing
+    }
+
+    // no alias, begins with "set"
+    protected void setPlain(String text) {
+        // do nothing
+    }
+
+    // doesn't begin with "set"
+    protected void noSet(String text) {
+        // do nothing
+    }
+
+    // nothing after "get"
+    protected void set(String text) {
+        // do nothing
+    }
+
+    // returns a list
+    protected List<Data> getMyList() {
+        return listField;
+    }
+
+    // accepts a list
+    protected void setMyList(List<Data> newList) {
+        listField = newList;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnyGetterSerializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnyGetterSerializerTest.java
new file mode 100644 (file)
index 0000000..1e206cf
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * ============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.gson.internal;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.JacksonExclusionStrategy;
+import org.onap.policy.common.gson.internal.DataAdapterFactory.Data;
+
+class AnyGetterSerializerTest {
+
+    private static DataAdapterFactory dataAdapter = new DataAdapterFactory();
+
+    private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter)
+                    .setExclusionStrategies(new JacksonExclusionStrategy()).create();
+
+    private Set<String> set;
+    private AnyGetterSerializer ser;
+
+    /**
+     * Set up.
+     *
+     * @throws Exception if an error occurs
+     */
+    @BeforeEach
+    void setUp() throws Exception {
+        set = new HashSet<>(Arrays.asList("id", "value"));
+        ser = new AnyGetterSerializer(gson, set, MapData.class.getDeclaredMethod("getTheMap"));
+    }
+
+    @Test
+    void testAddToTree_testCopyLiftedItems() {
+        JsonObject tree = new JsonObject();
+        tree.addProperty("hello", "world");
+
+        MapData data = new MapData();
+
+        data.map = DataAdapterFactory.makeMap();
+
+        // this should not be copied because it is in the "set"
+        data.map.put("value", Arrays.asList(new Data(1000)));
+
+        dataAdapter.reset();
+        JsonObject tree2 = tree.deepCopy();
+        ser.addToTree(data, tree2);
+
+        assertTrue(dataAdapter.isDataWritten());
+
+        DataAdapterFactory.addToObject(tree);
+
+        assertEquals(tree.toString(), tree2.toString());
+    }
+
+    @Test
+    void testAddToTree_NullMap() {
+        JsonObject tree = new JsonObject();
+        tree.addProperty("hello", "world");
+
+        MapData data = new MapData();
+
+        // leave "map" unset
+
+        JsonObject tree2 = tree.deepCopy();
+        ser.addToTree(data, tree2);
+
+        assertEquals(tree.toString(), tree2.toString());
+    }
+
+    @Test
+    void testAddToTree_NotAnObject() throws Exception {
+        ser = new AnyGetterSerializer(gson, set, NotAnObject.class.getDeclaredMethod("getNonMap"));
+
+        JsonObject tree = new JsonObject();
+
+        NotAnObject data = new NotAnObject();
+        data.text = "bye bye";
+
+        assertThatThrownBy(() -> ser.addToTree(data, tree)).isInstanceOf(JsonParseException.class)
+                        .hasMessage(AnyGetterSerializer.NOT_AN_OBJECT_ERR + NotAnObject.class.getName() + ".getNonMap");
+    }
+
+    public static class MapData {
+        protected int id;
+        protected String value;
+        protected Map<String, List<Data>> map;
+
+        protected Map<String, List<Data>> getTheMap() {
+            return map;
+        }
+    }
+
+    /**
+     * The "lifted" property is not a JsonObject so it should throw an exception.
+     */
+    public static class NotAnObject {
+        protected String text;
+
+        public String getNonMap() {
+            return text;
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnySetterDeserializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/AnySetterDeserializerTest.java
new file mode 100644 (file)
index 0000000..d833925
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * ============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.gson.internal;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.JacksonExclusionStrategy;
+import org.onap.policy.common.gson.internal.DataAdapterFactory.Data;
+
+class AnySetterDeserializerTest {
+
+    private static DataAdapterFactory dataAdapter = new DataAdapterFactory();
+
+    private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter)
+                    .setExclusionStrategies(new JacksonExclusionStrategy()).create();
+
+    private Set<String> set;
+    private AnySetterDeserializer deser;
+
+    /**
+     * Set up.
+     *
+     * @throws Exception if an error occurs
+     */
+    @BeforeEach
+    void setUp() throws Exception {
+        set = new HashSet<>(Arrays.asList("id", "value"));
+        deser = new AnySetterDeserializer(gson, set,
+                        MapData.class.getDeclaredMethod("setItem", String.class, List.class));
+    }
+
+    @Test
+    void testAnySetterDeserializer() {
+        JsonObject json = new JsonObject();
+
+        // these should not be copied
+        json.addProperty("id", 10);
+        json.addProperty("value", "the-value");
+
+        // these should be copied
+        DataAdapterFactory.addToObject(json);
+
+        MapData data = new MapData();
+        data.map = new TreeMap<>();
+
+        dataAdapter.reset();
+        deser.getFromTree(json, data);
+
+        assertTrue(dataAdapter.isDataRead());
+        assertNotNull(data.map);
+        assertEquals(DataAdapterFactory.makeMap().toString(), data.map.toString());
+    }
+
+    public static class MapData {
+        protected Map<String, List<Data>> map;
+
+        protected void setItem(String key, List<Data> value) {
+            map.put(key, value);
+        }
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/ClassWalkerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/ClassWalkerTest.java
new file mode 100644 (file)
index 0000000..007724a
--- /dev/null
@@ -0,0 +1,505 @@
+/*
+ * ============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.gson.internal;
+
+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.assertNull;
+
+import com.google.gson.JsonParseException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import lombok.Getter;
+import lombok.Setter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.annotation.GsonJsonAnyGetter;
+import org.onap.policy.common.gson.annotation.GsonJsonAnySetter;
+import org.onap.policy.common.gson.annotation.GsonJsonIgnore;
+import org.onap.policy.common.gson.annotation.GsonJsonProperty;
+
+class ClassWalkerTest {
+
+    private static final String SET_OVERRIDE = ".setOverride";
+    private static final String INVALID_FIELD_NAME = "invalidFieldName";
+
+    private MyWalker walker;
+
+    /**
+     * Set up.
+     */
+    @BeforeEach
+    void setUp() {
+        walker = new MyWalker();
+    }
+
+    @Test
+    void testExamineClassOfQ_testExamineField_testExamineInField_testExamineOutField() {
+        walker.walkClassHierarchy(DerivedFromBottom.class);
+
+        assertEquals("[InterfaceOne, InterfaceTwo, InterfaceOne, InterfaceThree, Bottom, DerivedFromBottom]",
+            walker.classes.toString());
+
+        List<String> inFields = walker.getInProps(Field.class).stream().map(Field::getName).sorted().toList();
+        assertEquals("[exposedField, overriddenValue, transField]", inFields.toString());
+
+        List<String> outFields = walker.getInProps(Field.class).stream().map(Field::getName).sorted().toList();
+        assertEquals("[exposedField, overriddenValue, transField]", outFields.toString());
+
+        // should work with interfaces without throwing an NPE
+        walker.walkClassHierarchy(InterfaceOne.class);
+    }
+
+    @Test
+    void testHasAnyGetter() {
+        walker.walkClassHierarchy(Object.class);
+        assertNull(walker.getAnyGetter());
+        assertNull(walker.getAnySetter());
+
+        walker.walkClassHierarchy(AnyGetterIgnored.class);
+        assertNull(walker.getAnyGetter());
+        assertNull(walker.getAnySetter());
+
+        walker.walkClassHierarchy(AnyGetterOnly.class);
+        assertNotNull(walker.getAnyGetter());
+        assertNull(walker.getAnySetter());
+    }
+
+    @Test
+    void testHasAnySetter() {
+        walker.walkClassHierarchy(Object.class);
+        assertNull(walker.getAnySetter());
+        assertNull(walker.getAnyGetter());
+
+        walker.walkClassHierarchy(AnySetterIgnored.class);
+        assertNull(walker.getAnySetter());
+        assertNull(walker.getAnyGetter());
+
+        walker.walkClassHierarchy(AnySetterOnly.class);
+        assertNotNull(walker.getAnySetter());
+        assertNull(walker.getAnyGetter());
+    }
+
+    @Test
+    void testExamineMethod() {
+        walker.walkClassHierarchy(DerivedFromData.class);
+
+        assertEquals("[Data, DerivedFromData]", walker.classes.toString());
+
+        // ensure all methods were examined
+        Collections.sort(walker.methods);
+        List<String> lst = Arrays.asList("getId", "getValue", "getOnlyOut", "getStatic", "getText", "getTheMap",
+                        "getUnserialized", "getValue", "getWithParams", "setExtraParams", "setId", "setMap",
+                        "setMapValue", "setMissingParams", "setNonPublic", "setOnlyIn", "setText", "setUnserialized",
+                        "setValue", "setValue", "wrongGetPrefix", "wrongSetPrefix");
+        Collections.sort(lst);
+        assertEquals(lst.toString(), walker.methods.toString());
+
+        assertNotNull(walker.getAnyGetter());
+        assertEquals("getTheMap", walker.getAnyGetter().getName());
+
+        List<String> getters = walker.getOutProps(Method.class).stream().map(Method::getName).sorted().toList();
+        assertEquals("[getId, getOnlyOut, getValue]", getters.toString());
+
+        assertNotNull(walker.getAnySetter());
+        assertEquals("setMapValue", walker.getAnySetter().getName());
+
+        List<String> setters = walker.getInProps(Method.class).stream().map(Method::getName).sorted().toList();
+        assertEquals("[setId, setOnlyIn, setValue]", setters.toString());
+
+        // getter with invalid parameter count
+        assertThatThrownBy(() -> walker.walkClassHierarchy(AnyGetterMismatchParams.class))
+                        .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_GETTER_MISMATCH_ERR
+                                        + AnyGetterMismatchParams.class.getName() + ".getTheMap");
+
+        // setter with too few parameters
+        assertThatThrownBy(() -> walker.walkClassHierarchy(AnySetterTooFewParams.class))
+                        .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_SETTER_MISMATCH_ERR
+                                        + AnySetterTooFewParams.class.getName() + SET_OVERRIDE);
+
+        // setter with too many parameters
+        assertThatThrownBy(() -> walker.walkClassHierarchy(AnySetterTooManyParams.class))
+                        .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_SETTER_MISMATCH_ERR
+                                        + AnySetterTooManyParams.class.getName() + SET_OVERRIDE);
+
+        // setter with invalid parameter type
+        assertThatThrownBy(() -> walker.walkClassHierarchy(AnySetterInvalidParam.class))
+                        .isInstanceOf(JsonParseException.class).hasMessage(ClassWalker.ANY_SETTER_TYPE_ERR
+                                        + AnySetterInvalidParam.class.getName() + SET_OVERRIDE);
+    }
+
+    @Test
+    void testExamineMethod_AnyGetter() {
+        walker.walkClassHierarchy(AnyGetterOverride.class);
+
+        assertNotNull(walker.getAnyGetter());
+        assertEquals("getOverride", walker.getAnyGetter().getName());
+    }
+
+    @Test
+    void testExamineMethod_AnySetter() {
+        walker.walkClassHierarchy(AnySetterOverride.class);
+
+        assertNotNull(walker.getAnySetter());
+        assertEquals("setOverride", walker.getAnySetter().getName());
+    }
+
+    @Test
+    void testGetInNotIgnored_testGetOutNotIgnored() {
+        walker.walkClassHierarchy(DerivedFromData.class);
+
+        assertEquals("[id, onlyIn, text, value]", new TreeSet<>(walker.getInNotIgnored()).toString());
+        assertEquals("[id, onlyOut, text, value]", new TreeSet<>(walker.getOutNotIgnored()).toString());
+    }
+
+    /**
+     * Walker subclass that records items that are examined.
+     */
+    private static class MyWalker extends ClassWalker {
+        private final List<String> classes = new ArrayList<>();
+        private final List<String> methods = new ArrayList<>();
+
+        @Override
+        protected void examine(Class<?> clazz) {
+            classes.add(clazz.getSimpleName());
+
+            super.examine(clazz);
+        }
+
+        @Override
+        protected void examine(Method method) {
+            if (Adapter.isManaged(method)) {
+                methods.add(method.getName());
+            }
+
+            super.examine(method);
+        }
+
+        @Override
+        protected String detmPropName(Field field) {
+            if (INVALID_FIELD_NAME.equals(field.getName())) {
+                return null;
+            }
+
+            return super.detmPropName(field);
+        }
+    }
+
+    protected interface InterfaceOne {
+        int id = 1000; // NOSONAR I think this is meant to be accessible as fields, not constants
+    }
+
+    protected interface InterfaceTwo {
+        String text = "intfc2-text"; // NOSONAR I think this is meant to be accessible as fields, not constants
+    }
+
+    private interface InterfaceThree {
+
+    }
+
+    protected static class Bottom implements InterfaceOne, InterfaceThree {
+        private int id;
+        public String value;
+
+        // this is not actually invalid, but will be treated as if it were
+        public String invalidFieldName;
+
+        @GsonJsonProperty("exposed")
+        private String exposedField;
+
+        @GsonJsonIgnore
+        public int ignored;
+
+        public transient int ignoredTransField;
+
+        @GsonJsonProperty("trans")
+        public transient int transField;
+
+        @GsonJsonIgnore
+        public int getId() {
+            return id;
+        }
+
+        @GsonJsonIgnore
+        public void setId(int id) {
+            this.id = id;
+        }
+    }
+
+    protected static class DerivedFromBottom extends Bottom implements InterfaceOne, InterfaceTwo {
+        private String text;
+        protected String anotherValue;
+
+        @GsonJsonProperty("value")
+        public String overriddenValue;
+
+        @GsonJsonIgnore
+        public String getText() {
+            return text;
+        }
+
+        @GsonJsonIgnore
+        public void setText(String text) {
+            this.text = text;
+        }
+    }
+
+    @Setter
+    protected static class Data {
+        @Getter
+        private int id;
+        // this will be ignored, because there's already a field by this name
+        private String text;
+
+        // not public, but property provided
+        @GsonJsonProperty("text")
+        protected String getText() {
+            return text;
+        }
+
+        // should only show up in the output list
+        public int getOnlyOut() {
+            return 1100;
+        }
+
+        // will be overridden by subclass
+        @GsonJsonProperty("super-value-getter")
+        public String getValue() {
+            return null;
+        }
+
+        // will be overridden by subclass
+        @GsonJsonProperty("super-value-setter")
+        public void setValue(String value) {
+            // do nothing
+        }
+    }
+
+    protected static class DerivedFromData extends Data {
+        // not serialized
+        private String unserialized;
+
+        // overrides private field and public method from Data
+        public String text;
+
+        private Map<String, String> map;
+
+        private String value;
+
+        @Override
+        public String getValue() {
+            return value;
+        }
+
+        @Override
+        public void setValue(String value) {
+            this.value = value;
+        }
+
+        @GsonJsonAnyGetter
+        public Map<String, String> getTheMap() {
+            return map;
+        }
+
+        @GsonJsonIgnore
+        public void setMap(Map<String, String> map) {
+            this.map = map;
+        }
+
+        @GsonJsonAnySetter
+        public void setMapValue(String key, String value) {
+            if (map == null) {
+                map = new TreeMap<>();
+            }
+
+            map.put(key, value);
+        }
+
+        @GsonJsonIgnore
+        public String getUnserialized() {
+            return unserialized;
+        }
+
+        @GsonJsonIgnore
+        public void setUnserialized(String unserialized) {
+            this.unserialized = unserialized;
+        }
+
+        // should only show up in the input list
+        public void setOnlyIn(int value) {
+            // do nothing
+        }
+
+        // has a param - shouldn't be serialized
+        public int getWithParams(String text) {
+            return 1000;
+        }
+
+        // too few params - shouldn't be serialized
+        public void setMissingParams() {
+            // do nothing
+        }
+
+        // too many params - shouldn't be serialized
+        public void setExtraParams(String text, String moreText) {
+            // do nothing
+        }
+
+        // not public - shouldn't be serialized
+        protected void setNonPublic(String text) {
+            // do nothing
+        }
+
+        // doesn't start with "get"
+        public String wrongGetPrefix() {
+            return null;
+        }
+
+        // doesn't start with "set"
+        public void wrongSetPrefix(String text) {
+            // do nothing
+        }
+
+        // static
+        public static String getStatic() {
+            return null;
+        }
+    }
+
+    /**
+     * The "get" method has an incorrect argument count.
+     */
+    private static class AnyGetterMismatchParams {
+        @GsonJsonAnyGetter
+        public Map<String, String> getTheMap(String arg) {
+            return new TreeMap<>();
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnyGetter} method.
+     */
+    private static class AnyGetterOnly {
+        @GsonJsonAnyGetter
+        private Map<String, Integer> getOverride() {
+            return null;
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnyGetter} method, but it's ignored.
+     */
+    private static class AnyGetterIgnored {
+        @GsonJsonAnyGetter
+        @GsonJsonIgnore
+        private Map<String, Integer> getOverride() {
+            return null;
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnySetter} method.
+     */
+    private static class AnySetterOnly {
+        @GsonJsonAnySetter
+        private void setOverride(String key, int value) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnySetter} method, but it's ignored.
+     */
+    private static class AnySetterIgnored {
+        @GsonJsonAnySetter
+        @GsonJsonIgnore
+        private void setOverride(String key, int value) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnyGetter} method that overrides the super class' method.
+     */
+    private static class AnyGetterOverride extends DerivedFromData {
+        private Map<String, Integer> overMap;
+
+        @GsonJsonAnyGetter
+        private Map<String, Integer> getOverride() {
+            return overMap;
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnySetter} method that overrides the super class' method.
+     */
+    private static class AnySetterOverride extends DerivedFromData {
+        private Map<String, Integer> overMap;
+
+        @GsonJsonAnySetter
+        private void setOverride(String key, int value) {
+            if (overMap == null) {
+                overMap = new TreeMap<>();
+            }
+
+            overMap.put(key, value);
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnySetter} method with too few parameters.
+     */
+    private static class AnySetterTooFewParams extends DerivedFromData {
+        @GsonJsonAnySetter
+        public void setOverride(String key) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnySetter} method with too few parameters.
+     */
+    private static class AnySetterTooManyParams extends DerivedFromData {
+        @GsonJsonAnySetter
+        public void setOverride(String key, int value, String anotherValue) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Has {@link GsonJsonAnySetter} method whose first argument type is incorrect.
+     */
+    private static class AnySetterInvalidParam extends DerivedFromData {
+        @GsonJsonAnySetter
+        public void setOverride(Integer key, String value) {
+            // do nothing
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/DataAdapterFactory.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/DataAdapterFactory.java
new file mode 100644 (file)
index 0000000..d2cdf7f
--- /dev/null
@@ -0,0 +1,304 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.gson.internal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import lombok.ToString;
+
+/**
+ * Factory used with test Data.
+ */
+public class DataAdapterFactory implements TypeAdapterFactory {
+
+    /**
+     * Output of {@link #makeList()}, encoded as json.
+     */
+    public static final String ENCODED_LIST = "[{'id':100},{'id':101}]".replace('\'', '"');
+
+    /**
+     * Output of {@link #makeMap()}, encoded as json.
+     */
+    public static final String ENCODED_MAP = "'data-100':{'id':100},'data-101':{'id':101}".replace('\'', '"');
+
+    /**
+     * Object handled by this factory.
+     */
+    @ToString
+    public static class Data {
+        private int id;
+
+        public Data() {
+            super();
+        }
+
+        public Data(int id) {
+            this.id = id;
+        }
+
+        public int getId() {
+            return id;
+        }
+
+        public void setId(int id) {
+            this.id = id;
+        }
+    }
+
+    /**
+     * Object derived from Data.
+     */
+    @ToString(callSuper = true)
+    public static class DerivedData extends Data {
+        private String text;
+
+        public DerivedData() {
+            super();
+        }
+
+        public DerivedData(int id, String text) {
+            super(id);
+            this.text = text;
+        }
+
+        public String getText() {
+            return text;
+        }
+
+        public void setText(String text) {
+            this.text = text;
+        }
+    }
+
+    /**
+     * Set to {@code true} when {@link #write(JsonWriter, Data)} has been invoked.
+     */
+    private boolean dataWritten = false;
+
+    /**
+     * Set to {@code true} when {@link #read(JsonReader)} has been invoked.
+     */
+    private boolean dataRead = false;
+
+    /**
+     * Clears the flags that indicate that "read" or "write" has been invoked.
+     */
+    public void reset() {
+        dataWritten = true;
+        dataRead = true;
+    }
+
+    public boolean isDataWritten() {
+        return dataWritten;
+    }
+
+    public boolean isDataRead() {
+        return dataRead;
+    }
+
+    /**
+     * Makes a list of Data.
+     *
+     * @return a new list of Data
+     */
+    public static List<Data> makeList() {
+        List<Data> listField = new ArrayList<>();
+
+        listField.add(new Data(100));
+        listField.add(new Data(101));
+
+        return listField;
+    }
+
+    /**
+     * Makes an array of Data.
+     *
+     * @return a new array of Data
+     */
+    public static JsonArray makeArray() {
+        JsonArray arr = new JsonArray();
+
+        for (Data data : makeList()) {
+            JsonObject json = new JsonObject();
+            json.addProperty("id", data.getId());
+            arr.add(json);
+        }
+
+        return arr;
+    }
+
+    /**
+     * Makes a map of Data.
+     *
+     * @return a new map of Data
+     */
+    public static Map<String, List<Data>> makeMap() {
+        Map<String, List<Data>> map = new TreeMap<>();
+
+        for (Data data : makeList()) {
+            map.put("data-" + data.getId(), Arrays.asList(data));
+        }
+
+        return map;
+    }
+
+    /**
+     * Adds Data objects to a tree, mirroring {@link #makeMap()}.
+     *
+     * @param tree tree into which objects are to be added
+     */
+    public static void addToObject(JsonObject tree) {
+        for (JsonElement ent : makeArray()) {
+            JsonObject obj = ent.getAsJsonObject();
+            JsonArray arr = new JsonArray();
+            arr.add(obj);
+            tree.add("data-" + obj.get("id").getAsString(), arr);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        if (type.getRawType() == Data.class) {
+            return (TypeAdapter<T>) new DataTypeAdapter(gson.getDelegateAdapter(this, TypeToken.get(Data.class)),
+                            gson.getAdapter(JsonElement.class));
+        }
+
+        if (type.getRawType() == DerivedData.class) {
+            return (TypeAdapter<T>) new DerivedDataTypeAdapter(
+                            gson.getDelegateAdapter(this, TypeToken.get(DerivedData.class)),
+                            gson.getAdapter(JsonElement.class));
+        }
+
+        return null;
+    }
+
+    /**
+     * Adapter for "Data".
+     */
+    private class DataTypeAdapter extends TypeAdapter<Data> {
+        private TypeAdapter<Data> delegate;
+        private TypeAdapter<JsonElement> elementAdapter;
+
+        /**
+         * Constructs the object.
+         *
+         * @param delegate delegate adapter
+         * @param elementAdapter element adapter
+         */
+        public DataTypeAdapter(TypeAdapter<Data> delegate, TypeAdapter<JsonElement> elementAdapter) {
+            this.delegate = delegate;
+            this.elementAdapter = elementAdapter;
+        }
+
+        @Override
+        public void write(JsonWriter out, Data data) throws IOException {
+            dataWritten = true;
+
+            JsonElement tree = delegate.toJsonTree(data);
+
+            if (tree.isJsonObject()) {
+                JsonObject jsonObj = tree.getAsJsonObject();
+                jsonObj.addProperty("id", data.getId());
+            }
+
+            elementAdapter.write(out, tree);
+        }
+
+        @Override
+        public Data read(JsonReader in) throws IOException {
+            dataRead = true;
+
+            JsonElement tree = elementAdapter.read(in);
+            Data data = delegate.fromJsonTree(tree);
+
+            if (tree.isJsonObject()) {
+                JsonObject jsonObj = tree.getAsJsonObject();
+                data.setId(jsonObj.get("id").getAsInt());
+            }
+
+            return data;
+        }
+    }
+
+    /**
+     * Adapter for "DerivedData".
+     */
+    private class DerivedDataTypeAdapter extends TypeAdapter<DerivedData> {
+        private TypeAdapter<DerivedData> delegate;
+        private TypeAdapter<JsonElement> elementAdapter;
+
+        /**
+         * Constructs the object.
+         *
+         * @param delegate delegate adapter
+         * @param elementAdapter element adapter
+         */
+        public DerivedDataTypeAdapter(TypeAdapter<DerivedData> delegate, TypeAdapter<JsonElement> elementAdapter) {
+            this.delegate = delegate;
+            this.elementAdapter = elementAdapter;
+        }
+
+        @Override
+        public void write(JsonWriter out, DerivedData data) throws IOException {
+            dataWritten = true;
+
+            JsonElement tree = delegate.toJsonTree(data);
+
+            if (tree.isJsonObject()) {
+                JsonObject jsonObj = tree.getAsJsonObject();
+                jsonObj.addProperty("id", data.getId());
+                jsonObj.addProperty("text", data.getText());
+            }
+
+            elementAdapter.write(out, tree);
+        }
+
+        @Override
+        public DerivedData read(JsonReader in) throws IOException {
+            dataRead = true;
+
+            JsonElement tree = elementAdapter.read(in);
+            DerivedData data = delegate.fromJsonTree(tree);
+
+            if (tree.isJsonObject()) {
+                JsonObject jsonObj = tree.getAsJsonObject();
+                data.setId(jsonObj.get("id").getAsInt());
+                data.setText(jsonObj.get("text").getAsString());
+            }
+
+            return data;
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldDeserializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldDeserializerTest.java
new file mode 100644 (file)
index 0000000..8f783bc
--- /dev/null
@@ -0,0 +1,109 @@
+/*-
+ * ============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.gson.internal;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.JacksonExclusionStrategy;
+import org.onap.policy.common.gson.internal.DataAdapterFactory.Data;
+
+class FieldDeserializerTest {
+    private static final String TEXT_FIELD_NAME = "text";
+    private static final String LIST_FIELD_NAME = "listField";
+    private static final String INITIAL_VALUE = "initial value";
+    private static final String NEW_VALUE = "new value";
+
+    private static DataAdapterFactory dataAdapter = new DataAdapterFactory();
+
+    private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter)
+                    .setExclusionStrategies(new JacksonExclusionStrategy()).create();
+
+    private FieldDeserializer deser;
+
+    private String text;
+
+    private List<Data> listField;
+
+    @Test
+    void testGetFromTree() throws Exception {
+        deser = new FieldDeserializer(gson, FieldDeserializerTest.class.getDeclaredField(TEXT_FIELD_NAME));
+
+        JsonObject json = new JsonObject();
+
+        // no value in tree - text remains unchanged
+        text = INITIAL_VALUE;
+        deser.getFromTree(json, this);
+        assertEquals(INITIAL_VALUE, text);
+
+        // null value in tree - text remains unchanged
+        json.add(TEXT_FIELD_NAME, JsonNull.INSTANCE);
+        deser.getFromTree(json, this);
+        assertEquals(INITIAL_VALUE, text);
+
+        // now assign a value - text should be changed now
+        json.addProperty(TEXT_FIELD_NAME, NEW_VALUE);
+
+        deser.getFromTree(json, this);
+        assertEquals(NEW_VALUE, text);
+
+        /*
+         * check list field
+         */
+        deser = new FieldDeserializer(gson, FieldDeserializerTest.class.getDeclaredField(LIST_FIELD_NAME));
+
+        json.add(LIST_FIELD_NAME, DataAdapterFactory.makeArray());
+
+        dataAdapter.reset();
+        listField = null;
+        deser.getFromTree(json, this);
+
+        assertTrue(dataAdapter.isDataRead());
+        assertEquals(DataAdapterFactory.makeList().toString(), listField.toString());
+    }
+
+    @Test
+    void testGetFromTree_SetEx() throws Exception {
+        deser = new FieldDeserializer(gson, FieldDeserializerTest.class.getDeclaredField(TEXT_FIELD_NAME)) {
+            @Override
+            public Object fromJsonTree(JsonElement tree) {
+                // return an int, which won't fit in a String - cause an exception
+                return 10;
+            }
+        };
+
+        JsonObject json = new JsonObject();
+        json.addProperty(TEXT_FIELD_NAME, NEW_VALUE);
+
+        assertThatThrownBy(() -> deser.getFromTree(json, this)).isInstanceOf(JsonParseException.class)
+                        .hasMessage(FieldDeserializer.SET_ERR + FieldDeserializerTest.class.getName() + ".text");
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldSerializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/FieldSerializerTest.java
new file mode 100644 (file)
index 0000000..1431f47
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * ============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.gson.internal;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.JacksonExclusionStrategy;
+import org.onap.policy.common.gson.internal.DataAdapterFactory.Data;
+
+class FieldSerializerTest {
+    private static final String TEXT_FIELD_NAME = "text";
+    private static final String LIST_FIELD_NAME = "listField";
+
+    private static DataAdapterFactory dataAdapter = new DataAdapterFactory();
+
+    private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter)
+                    .setExclusionStrategies(new JacksonExclusionStrategy()).create();
+
+    private FieldSerializer ser;
+
+    protected String text;
+
+    private List<Data> listField;
+
+    @Test
+    void testAddToTree() throws Exception {
+        ser = new FieldSerializer(gson, FieldSerializerTest.class.getDeclaredField(TEXT_FIELD_NAME));
+
+        // serialize null value first
+        text = null;
+
+        JsonObject json = new JsonObject();
+        ser.addToTree(this, json);
+        assertTrue(json.get(TEXT_FIELD_NAME).isJsonNull());
+
+        // serialize an actual value
+        text = "hello";
+        ser.addToTree(this, json);
+        assertEquals("hello", json.get(TEXT_FIELD_NAME).getAsString());
+
+        /*
+         * check list field
+         */
+        listField = DataAdapterFactory.makeList();
+
+        ser = new FieldSerializer(gson, FieldSerializerTest.class.getDeclaredField(LIST_FIELD_NAME));
+
+        dataAdapter.reset();
+        JsonElement tree = ser.toJsonTree(listField);
+        assertTrue(dataAdapter.isDataWritten());
+        assertEquals(DataAdapterFactory.ENCODED_LIST, tree.toString());
+    }
+
+    @Test
+    void testAddToTree_GetEx() throws Exception {
+        ser = new FieldSerializer(gson, FieldSerializerTest.class.getDeclaredField(TEXT_FIELD_NAME)) {
+            @Override
+            protected Object getFromObject(Object source) throws IllegalAccessException {
+                throw new IllegalAccessException("expected exception");
+            }
+        };
+
+        text = "world";
+
+        JsonObject obj = new JsonObject();
+
+        assertThatThrownBy(() -> ser.addToTree(this, obj)).isInstanceOf(JsonParseException.class)
+                        .hasMessage(FieldSerializer.GET_ERR + FieldSerializerTest.class.getName() + ".text");
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/JacksonTypeAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/JacksonTypeAdapterTest.java
new file mode 100644 (file)
index 0000000..a5ebc1e
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+ * ============LICENSE_START==============================================================
+ * ONAP
+ * =======================================================================================
+ * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2024-2025 OpenInfra Foundation Europe. 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.gson.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.ToString;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class JacksonTypeAdapterTest {
+    private static final String HELLO = "hello";
+    private static final String WORLD = "world";
+
+    /**
+     * Gson object that excludes fields, as we're going to process the fields ourselves.
+     */
+    private static final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+
+    private JacksonTypeAdapter<Data> adapter;
+    private List<Serializer> sers;
+    private List<Deserializer> desers;
+
+    /**
+     * Initializes the previously defined fields.
+     */
+    @BeforeEach
+    void setUp() {
+        // create list of serializers, one for "id" and one for "value"
+        sers = new ArrayList<>(2);
+        sers.add(new NamedSer(HELLO) {
+            @Override
+            protected String getValue(Data data) {
+                return data.id;
+            }
+        });
+        sers.add(new NamedSer(WORLD) {
+            @Override
+            protected String getValue(Data data) {
+                return data.value;
+            }
+        });
+
+        // create list of deserializers, one for "id" and one for "value"
+        desers = new ArrayList<>(2);
+        desers.add(new NamedDeser(HELLO) {
+            @Override
+            protected void setValue(Data data, String value) {
+                data.id = value;
+            }
+        });
+        desers.add(new NamedDeser(WORLD) {
+            @Override
+            protected void setValue(Data data, String value) {
+                data.value = value;
+            }
+        });
+
+        TypeAdapterFactory factory = new TypeAdapterFactory() {
+            @Override
+            public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+                return null;
+            }
+        };
+
+        TypeAdapter<Data> delegate = gson.getDelegateAdapter(factory, TypeToken.get(Data.class));
+
+        adapter = new JacksonTypeAdapter<>(gson, delegate, sers, desers);
+    }
+
+    @Test
+    void testWriteJsonWriterT() throws Exception {
+        Data data = new Data("abc", "def");
+
+        StringWriter wtr = new StringWriter();
+        adapter.write(new JsonWriter(wtr), data);
+
+        assertEquals("{'hello':'abc','world':'def'}".replace('\'', '"'), wtr.toString());
+    }
+
+    /**
+     * Tests the case where the delegate does not return a JsonObject.
+     *
+     * @throws Exception if an error occurs
+     */
+    @Test
+    void testWriteJsonWriterT_NotAnObject() throws Exception {
+        TypeAdapter<String> delegate = gson.getAdapter(String.class);
+        JacksonTypeAdapter<String> stringAdapter = new JacksonTypeAdapter<>(gson, delegate, sers, desers);
+
+        StringWriter wtr = new StringWriter();
+        stringAdapter.write(new JsonWriter(wtr), "write text");
+
+        assertEquals("'write text'".replace('\'', '"'), wtr.toString());
+    }
+
+    @Test
+    void testReadJsonReader() throws Exception {
+        Data data = adapter
+                        .read(new JsonReader(new StringReader("{'hello':'four','world':'score'}".replace('\'', '"'))));
+
+        assertEquals(new Data("four", "score").toString(), data.toString());
+    }
+
+    /**
+     * Tests the case where the delegate does not use a JsonObject.
+     *
+     * @throws Exception if an error occurs
+     */
+    @Test
+    void testReadJsonReader_NotAnObject() throws Exception {
+        TypeAdapter<String> delegate = gson.getAdapter(String.class);
+        JacksonTypeAdapter<String> stringAdapter = new JacksonTypeAdapter<>(gson, delegate, sers, desers);
+
+        String data = stringAdapter.read(new JsonReader(new StringReader("'read text'".replace('\'', '"'))));
+
+        assertEquals("read text", data);
+    }
+
+    @ToString
+    private static class Data {
+        private String id;
+        private String value;
+
+        /*
+         * This is invoked by gson via reflection, thus no direct invocation. Hence it has
+         * to be labeled "unused".
+         */
+        @SuppressWarnings("unused")
+        public Data() {
+            super();
+        }
+
+        public Data(String id, String value) {
+            this.id = id;
+            this.value = value;
+        }
+    }
+
+    private abstract static class NamedSer implements Serializer {
+        private final String name;
+
+        /**
+         * Constructs the object.
+         *
+         * @param name the name of the field, when stored in a JsonObject
+         */
+        public NamedSer(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public void addToTree(Object source, JsonObject target) {
+            Data data = (Data) source;
+            target.addProperty(name, getValue(data));
+        }
+
+        protected abstract String getValue(Data data);
+    }
+
+    private abstract static class NamedDeser implements Deserializer {
+        private final String name;
+
+        /**
+         * Constructs the object.
+         *
+         * @param name the name of the field, when stored in a JsonObject
+         */
+        public NamedDeser(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public void getFromTree(JsonObject source, Object target) {
+            Data data = (Data) target;
+            setValue(data, source.get(name).getAsString());
+        }
+
+        protected abstract void setValue(Data data, String value);
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/LifterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/LifterTest.java
new file mode 100644 (file)
index 0000000..e83757b
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * ============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.gson.internal;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class LifterTest {
+
+    private static Gson gson = new Gson();
+
+    @Test
+    void testLifter_testShouldLift() throws Exception {
+        Set<String> set = new HashSet<>(Arrays.asList("abc", "def"));
+        Lifter lifter = new Lifter(gson, set, LifterTest.class.getDeclaredMethod("getValue"), String.class);
+
+        // should not lift these
+        assertFalse(lifter.shouldLift("abc"));
+        assertFalse(lifter.shouldLift("def"));
+
+        // should lift anything else
+        assertTrue(lifter.shouldLift("hello"));
+        assertTrue(lifter.shouldLift("world"));
+    }
+
+    public String getValue() {
+        return "";
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodAdapterTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodAdapterTest.java
new file mode 100644 (file)
index 0000000..17a184a
--- /dev/null
@@ -0,0 +1,58 @@
+/*--
+ * ============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.gson.internal;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParseException;
+import org.junit.jupiter.api.Test;
+
+class MethodAdapterTest {
+    private static final Gson gson = new Gson();
+
+    private String saved;
+
+    @Test
+    void testMethodAdapter_testInvoke() throws Exception {
+        MethodAdapter adapter =
+                new MethodAdapter(gson, MethodAdapterTest.class.getDeclaredMethod("getValue"), String.class);
+        assertEquals("hello", adapter.invoke(this));
+
+        MethodAdapter adapter2 = new MethodAdapter(gson,
+                MethodAdapterTest.class.getDeclaredMethod("setValue", String.class), String.class);
+        adapter2.invoke(this, "world");
+        assertEquals("world", saved);
+
+        assertThatThrownBy(() -> adapter2.invoke(this, 100)).isInstanceOf(JsonParseException.class)
+                .hasMessage(MethodAdapter.INVOKE_ERR + MethodAdapterTest.class.getName() + ".setValue");
+    }
+
+    public String getValue() {
+        return "hello";
+    }
+
+    void setValue(String val) {
+        saved = val;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodDeserializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodDeserializerTest.java
new file mode 100644 (file)
index 0000000..60fe7a0
--- /dev/null
@@ -0,0 +1,100 @@
+/*--
+ * ============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.gson.internal;
+
+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 com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.JacksonExclusionStrategy;
+import org.onap.policy.common.gson.internal.DataAdapterFactory.Data;
+
+class MethodDeserializerTest {
+    private static final String PROP_NAME = "text";
+    private static final String METHOD_NAME = "setText";
+    private static final String INITIAL_VALUE = "initial value";
+    private static final String NEW_VALUE = "new value";
+
+    private static DataAdapterFactory dataAdapter = new DataAdapterFactory();
+
+    private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter)
+            .setExclusionStrategies(new JacksonExclusionStrategy()).create();
+
+    private MethodDeserializer deser;
+
+    private String text;
+
+    private List<Data> listField;
+
+    @Test
+    void testGetFromTree() throws Exception {
+        deser = new MethodDeserializer(gson, MethodDeserializerTest.class.getDeclaredMethod(METHOD_NAME, String.class));
+
+        // non-existent value - should not overwrite
+        text = INITIAL_VALUE;
+        JsonObject json = new JsonObject();
+        deser.getFromTree(json, this);
+        assertEquals(INITIAL_VALUE, text);
+
+        // null value - should not overwrite
+        text = INITIAL_VALUE;
+        json.add(PROP_NAME, JsonNull.INSTANCE);
+        deser.getFromTree(json, this);
+        assertEquals(INITIAL_VALUE, text);
+
+        // has a value - should store it
+        text = INITIAL_VALUE;
+        json.addProperty(PROP_NAME, NEW_VALUE);
+        deser.getFromTree(json, this);
+        assertEquals(NEW_VALUE, text);
+
+        /*
+         * check list field
+         */
+        deser = new MethodDeserializer(gson, MethodDeserializerTest.class.getDeclaredMethod("setTheList", List.class));
+
+        json = new JsonObject();
+        json.add("theList", DataAdapterFactory.makeArray());
+
+        dataAdapter.reset();
+        listField = null;
+        deser.getFromTree(json, this);
+
+        assertTrue(dataAdapter.isDataRead());
+        assertNotNull(listField);
+        assertEquals(DataAdapterFactory.makeList().toString(), listField.toString());
+    }
+
+    protected void setText(String text) {
+        this.text = text;
+    }
+
+    protected void setTheList(List<Data> lst) {
+        listField = lst;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodSerializerTest.java b/policy-common/src/test/java/org/onap/policy/common/gson/internal/MethodSerializerTest.java
new file mode 100644 (file)
index 0000000..ae41e08
--- /dev/null
@@ -0,0 +1,88 @@
+/*--
+ * ============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.gson.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.gson.JacksonExclusionStrategy;
+import org.onap.policy.common.gson.internal.DataAdapterFactory.Data;
+
+class MethodSerializerTest {
+    private static final String PROP_NAME = "text";
+    private static final String METHOD_NAME = "getText";
+
+    private static DataAdapterFactory dataAdapter = new DataAdapterFactory();
+
+    private static Gson gson = new GsonBuilder().registerTypeAdapterFactory(dataAdapter)
+            .setExclusionStrategies(new JacksonExclusionStrategy()).create();
+
+    private MethodSerializer ser;
+
+    private String text;
+
+    private List<Data> listField;
+
+    @Test
+    void testAddToTree() throws Exception {
+        ser = new MethodSerializer(gson, MethodSerializerTest.class.getDeclaredMethod(METHOD_NAME));
+
+        // serialize null value first
+        text = null;
+
+        JsonObject json = new JsonObject();
+        ser.addToTree(this, json);
+        assertTrue(json.get(PROP_NAME).isJsonNull());
+
+        // serialize an actual value
+        text = "hello";
+        ser.addToTree(this, json);
+        assertEquals("hello", json.get(PROP_NAME).getAsString());
+
+        /*
+         * check list field
+         */
+        listField = DataAdapterFactory.makeList();
+
+        ser = new MethodSerializer(gson, MethodSerializerTest.class.getDeclaredMethod("getTheList"));
+
+        dataAdapter.reset();
+        JsonElement tree = ser.toJsonTree(listField);
+
+        assertTrue(dataAdapter.isDataWritten());
+        assertEquals(DataAdapterFactory.ENCODED_LIST,  tree.toString());
+    }
+
+    protected String getText() {
+        return text;
+    }
+
+    protected List<Data> getTheList() {
+        return listField;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/CommonTestData.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/CommonTestData.java
new file mode 100644 (file)
index 0000000..ecd2f20
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.java
new file mode 100644 (file)
index 0000000..6f0e38d
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicParameterGroupTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/TopicParameterGroupTest.java
new file mode 100644 (file)
index 0000000..db28892
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusConsumerTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusConsumerTest.java
new file mode 100644 (file)
index 0000000..207023e
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.java
new file mode 100644 (file)
index 0000000..343a56a
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicFactoryTestBase.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/BusTopicFactoryTestBase.java
new file mode 100644 (file)
index 0000000..bd53111
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.java
new file mode 100644 (file)
index 0000000..820fc2c
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.java
new file mode 100644 (file)
index 0000000..8ad8e8f
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicBaseTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicBaseTest.java
new file mode 100644 (file)
index 0000000..5ecde25
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicFactoryTestBase.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicFactoryTestBase.java
new file mode 100644 (file)
index 0000000..8444b48
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicPropertyBuilder.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicPropertyBuilder.java
new file mode 100644 (file)
index 0000000..29c5306
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicTestBase.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/base/TopicTestBase.java
new file mode 100644 (file)
index 0000000..8d5c353
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicClientExceptionTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicClientExceptionTest.java
new file mode 100644 (file)
index 0000000..93341a9
--- /dev/null
@@ -0,0 +1,35 @@
+/*-
+ * ============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));
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/client/TopicSinkClientTest.java
new file mode 100644 (file)
index 0000000..67b15ec
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/IndexedKafkaTopicSourceFactoryTest.java
new file mode 100644 (file)
index 0000000..a901b07
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSinkTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/InlineKafkaTopicSinkTest.java
new file mode 100644 (file)
index 0000000..c8d6e21
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapperTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaPublisherWrapperTest.java
new file mode 100644 (file)
index 0000000..8e13af2
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactoryTestBase.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicFactoryTestBase.java
new file mode 100644 (file)
index 0000000..1085ee9
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicPropertyBuilder.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicPropertyBuilder.java
new file mode 100644 (file)
index 0000000..6a1be7a
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkFactoryTest.java
new file mode 100644 (file)
index 0000000..6221036
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSinkTest.java
new file mode 100644 (file)
index 0000000..8818a27
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceFactoryTest.java
new file mode 100644 (file)
index 0000000..c91e548
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/KafkaTopicSourceTest.java
new file mode 100644 (file)
index 0000000..5572bc0
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSourceTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/kafka/SingleThreadedKafkaTopicSourceTest.java
new file mode 100644 (file)
index 0000000..0c732ba
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpointTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicEndpointTest.java
new file mode 100644 (file)
index 0000000..3ebec83
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicFactoryTest.java
new file mode 100644 (file)
index 0000000..4dcba86
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicPropertyBuilder.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicPropertyBuilder.java
new file mode 100644 (file)
index 0000000..57b7843
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkFactoryTest.java
new file mode 100644 (file)
index 0000000..ecd4df1
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSinkTest.java
new file mode 100644 (file)
index 0000000..ecba379
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceFactoryTest.java
new file mode 100644 (file)
index 0000000..c903806
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/event/noop/NoopTopicSourceTest.java
new file mode 100644 (file)
index 0000000..51ff109
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApiTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/features/NetLoggerFeatureApiTest.java
new file mode 100644 (file)
index 0000000..776c5d5
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactoryTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/TopicHealthCheckFactoryTest.java
new file mode 100644 (file)
index 0000000..b71731f
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025 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.healthcheck;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.message.bus.event.Topic;
+import org.onap.policy.common.parameters.topic.TopicParameters;
+
+class TopicHealthCheckFactoryTest {
+
+    @Test
+    void testGetTopicHealthCheck() {
+        var topicHealthCheckFactory = new TopicHealthCheckFactory();
+        var param = new TopicParameters();
+        param.setTopicCommInfrastructure(Topic.CommInfrastructure.NOOP.name());
+        var topicHealthCheck = topicHealthCheckFactory.getTopicHealthCheck(param);
+        assertNotNull(topicHealthCheck);
+        param.setTopicCommInfrastructure(Topic.CommInfrastructure.KAFKA.name());
+        topicHealthCheck = topicHealthCheckFactory.getTopicHealthCheck(param);
+        assertNotNull(topicHealthCheck);
+        param.setTopicCommInfrastructure(Topic.CommInfrastructure.REST.name());
+        topicHealthCheck = topicHealthCheckFactory.getTopicHealthCheck(param);
+        assertNull(topicHealthCheck);
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheckTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/kafka/KafkaHealthCheckTest.java
new file mode 100644 (file)
index 0000000..3b65f73
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025 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.healthcheck.kafka;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import org.apache.kafka.clients.admin.AdminClient;
+import org.apache.kafka.clients.admin.DescribeClusterResult;
+import org.apache.kafka.clients.admin.ListTopicsResult;
+import org.apache.kafka.common.KafkaFuture;
+import org.apache.kafka.common.Node;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.message.bus.healthcheck.TopicHealthCheck;
+import org.onap.policy.common.parameters.topic.TopicParameters;
+
+class KafkaHealthCheckTest {
+
+    @Test
+    void testAdminClient() {
+        var param = new TopicParameters();
+        param.setServers(List.of("localhost"));
+        var healthCheck = new KafkaHealthCheck(param);
+        var result = healthCheck.healthCheck(List.of());
+        assertFalse(result);
+
+        param.setAdditionalProps(Map.of("key", "value"));
+        result = healthCheck.healthCheck(List.of());
+        assertFalse(result);
+    }
+
+    @Test
+    void testMockAdminClientWithError() throws ExecutionException, InterruptedException {
+        var param = new TopicParameters();
+        param.setServers(List.of("localhost"));
+        var adminClient = mock(AdminClient.class);
+        KafkaFuture<Collection<Node>> kafkaFuture = mock(KafkaFuture.class);
+        var describeCluster = mock(DescribeClusterResult.class);
+        when(describeCluster.nodes()).thenReturn(kafkaFuture);
+        when(adminClient.describeCluster()).thenReturn(describeCluster);
+        when(kafkaFuture.get()).thenThrow(new InterruptedException());
+        var healthCheck = createKafkaHealthCheck(adminClient, param);
+        var result = healthCheck.healthCheck(List.of());
+        Assertions.assertFalse(result);
+    }
+
+    @Test
+    void testMockAdminClient() {
+        var param = new TopicParameters();
+        var adminClient = mock(AdminClient.class);
+        // no server address
+        var healthCheck = createKafkaHealthCheck(adminClient, param);
+        var result = healthCheck.healthCheck(List.of());
+        Assertions.assertTrue(result);
+
+        param.setServers(List.of());
+        result = healthCheck.healthCheck(List.of());
+        Assertions.assertTrue(result);
+
+        // no node Kafka
+        param.setServers(List.of("localhost"));
+        healthCheck = createKafkaHealthCheck(adminClient, param);
+        var describeCluster = mock(DescribeClusterResult.class);
+        when(describeCluster.nodes()).thenReturn(KafkaFuture.completedFuture(null));
+        when(adminClient.describeCluster()).thenReturn(describeCluster);
+        result = healthCheck.healthCheck(List.of());
+        Assertions.assertFalse(result);
+
+        // Kafka is UP
+        var node = new Node(1, "localhost", 9092);
+        when(describeCluster.nodes()).thenReturn(KafkaFuture.completedFuture(List.of(node)));
+        result = healthCheck.healthCheck(List.of());
+        Assertions.assertTrue(result);
+
+        // Kafka topics not available
+        var listTopics = mock(ListTopicsResult.class);
+        when(adminClient.listTopics()).thenReturn(listTopics);
+        when(listTopics.names()).thenReturn(KafkaFuture.completedFuture(Set.of()));
+        result = healthCheck.healthCheck(List.of("topic"));
+        Assertions.assertFalse(result);
+
+        when(listTopics.names()).thenReturn(KafkaFuture.completedFuture(Set.of("topic")));
+        result = healthCheck.healthCheck(List.of("wrongTopic"));
+        Assertions.assertFalse(result);
+
+        // Kafka topics available
+        result = healthCheck.healthCheck(List.of("topic"));
+        Assertions.assertTrue(result);
+    }
+
+    private TopicHealthCheck createKafkaHealthCheck(AdminClient adminClient, TopicParameters param) {
+        return new KafkaHealthCheck(param) {
+            @Override
+            protected AdminClient createAdminClient() {
+                return adminClient;
+            }
+        };
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheckTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/healthcheck/noop/NoopHealthCheckTest.java
new file mode 100644 (file)
index 0000000..fdd3e1b
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025 OpenInfra Foundation Europe. 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.message.bus.healthcheck.noop;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.message.bus.event.CommonTestData;
+import org.onap.policy.common.message.bus.event.TopicEndpoint;
+import org.onap.policy.common.message.bus.event.TopicEndpointManager;
+
+class NoopHealthCheckTest {
+
+    @Test
+    void testBuild() {
+        TopicEndpoint topicEndpoint = TopicEndpointManager.getManager();
+        topicEndpoint.start();
+        var healthCheck = new NoopHealthCheck();
+        var result = healthCheck.healthCheck(List.of());
+        assertTrue(result);
+    }
+
+    @Test
+    void testBuild_Failure() {
+        TopicEndpoint topicEndpoint = TopicEndpointManager.getManager();
+        topicEndpoint.start();
+        var healthCheck = new NoopHealthCheck();
+        topicEndpoint.stop();
+        var result = healthCheck.healthCheck(List.of());
+        assertFalse(result);
+    }
+
+    @Test
+    void test_TopicIsAlive() {
+        TopicEndpoint topicEndpoint = TopicEndpointManager.getManager();
+
+        var topicSource = CommonTestData.getTopicParameters("topicSource", "noop", "localhost");
+        var topicSink = CommonTestData.getTopicParameters("topicSink", "noop", "localhost");
+
+        topicEndpoint.addTopicSources(List.of(topicSource));
+        topicEndpoint.addTopicSinks(List.of(topicSink));
+
+        topicEndpoint.start();
+        var healthCheck = new NoopHealthCheck();
+        var result = healthCheck.healthCheck(List.of("topicSource", "topicSink"));
+        assertTrue(result);
+    }
+
+    @Test
+    void test_TopicIsNotAlive() {
+        TopicEndpoint topicEndpoint = TopicEndpointManager.getManager();
+
+        var topicSource = CommonTestData.getTopicParameters("topicSource", "noop", "localhost");
+        var topicSink = CommonTestData.getTopicParameters("topicSink", "noop", "localhost");
+
+        topicEndpoint.addTopicSources(List.of(topicSource));
+        topicEndpoint.addTopicSinks(List.of(topicSink));
+
+        topicEndpoint.start();
+
+        var topic = topicEndpoint.getNoopTopicSource("topicsource");
+        topic.stop();
+        var healthCheck = new NoopHealthCheck();
+        var result = healthCheck.healthCheck(List.of("topicSource", "topicSink"));
+        assertFalse(result);
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/KafkaPropertyUtilsTest.java
new file mode 100644 (file)
index 0000000..55f6a69
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/NetLoggerUtilTest.java b/policy-common/src/test/java/org/onap/policy/common/message/bus/utils/NetLoggerUtilTest.java
new file mode 100644 (file)
index 0000000..df621f5
--- /dev/null
@@ -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/policy-common/src/test/java/org/onap/policy/common/parameters/ParameterGroupTest.java b/policy-common/src/test/java/org/onap/policy/common/parameters/ParameterGroupTest.java
new file mode 100644 (file)
index 0000000..d90b13d
--- /dev/null
@@ -0,0 +1,93 @@
+/*-
+ * ============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.parameters;
+
+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.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ParameterGroupTest {
+
+    private ParameterGroup parameterGroup;
+
+    @BeforeEach
+    void setUp() {
+        parameterGroup = new ParameterGroup() {
+            private String name;
+            private BeanValidationResult validationResult = new BeanValidationResult("testGroup", "testObject");
+
+            @Override
+            public String getName() {
+                return name;
+            }
+
+            @Override
+            public void setName(final String name) {
+                this.name = name;
+            }
+
+            @Override
+            public BeanValidationResult validate() {
+                return validationResult;
+            }
+        };
+    }
+
+    @Test
+    void testGetName() {
+        String testName = "TestGroupName";
+        parameterGroup.setName(testName);
+        assertEquals(testName, parameterGroup.getName(), "The group name should match the one set");
+    }
+
+    @Test
+    void testSetName() {
+        String testName = "AnotherGroupName";
+        parameterGroup.setName(testName);
+        assertEquals(testName, parameterGroup.getName(), "The group name should match the one set");
+    }
+
+    @Test
+    void testValidate() {
+        BeanValidationResult result = parameterGroup.validate();
+        assertNotNull(result, "The validation result should not be null");
+        assertEquals("testGroup", result.getName(), "The validation result should have the correct group name");
+    }
+
+    @Test
+    void testIsValid() {
+        BeanValidationResult mockValidationResult = mock(BeanValidationResult.class);
+        ValidationStatus mockStatus = mock(ValidationStatus.class);
+
+        when(mockStatus.isValid()).thenReturn(true);
+        when(mockValidationResult.getStatus()).thenReturn(mockStatus);
+
+        ParameterGroup mockedParameterGroup = spy(parameterGroup);
+        doReturn(mockValidationResult).when(mockedParameterGroup).validate();
+
+        assertTrue(mockedParameterGroup.isValid(), "The parameters should be valid");
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidationResult.java
new file mode 100644 (file)
index 0000000..ab733ae
--- /dev/null
@@ -0,0 +1,214 @@
+/*-
+ * ============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.parameters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+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.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.function.BiConsumer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestBeanValidationResult {
+    private static final String TEXT1 = "abc";
+    private static final String TEXT2 = "def";
+    private static final String MY_LIST = "my-list";
+    private static final String MY_MAP = "my-map";
+    private static final String OBJECT = "an object";
+    private static final String INITIAL_INDENT = "xx ";
+    private static final String NEXT_INDENT = "yy ";
+    private static final String MID_INDENT = "xx yy ";
+    private static final String NAME = "my-name";
+    private static final String MY_LIST_INVALID = "  'my-list' INVALID, item has status INVALID\n    ";
+    private static final String MY_MAP_INVALID = "  'my-map' INVALID, item has status INVALID\n    ";
+    private static final String BEAN_INVALID_MSG = requote("'my-name' INVALID, item has status INVALID\n");
+
+    private String cleanMsg;
+    private String invalidMsg;
+
+    private BeanValidationResult bean;
+    private ObjectValidationResult clean;
+    private ObjectValidationResult invalid;
+
+    /**
+     * Sets up.
+     */
+    @BeforeEach
+    void setUp() {
+        clean = new ObjectValidationResult(TEXT1, 10);
+        cleanMsg = clean.getResult("", "", true);
+
+        invalid = new ObjectValidationResult(TEXT2, 20);
+        invalid.setResult(ValidationStatus.INVALID, "invalid");
+        invalidMsg = invalid.getResult();
+
+        bean = new BeanValidationResult(NAME, OBJECT);
+    }
+
+    @Test
+    void testBeanValidationResult() {
+        assertTrue(bean.isValid());
+        assertNull(bean.getResult());
+    }
+
+    @Test
+    void testAddResult_testGetResult() {
+        // null should be ok
+        assertTrue(bean.addResult(null));
+
+        assertTrue(bean.addResult(clean));
+        assertTrue(bean.isValid());
+        assertNull(bean.getResult());
+
+        assertFalse(bean.addResult(invalid));
+        assertFalse(bean.isValid());
+        assertEquals(BEAN_INVALID_MSG + "  " + invalidMsg, bean.getResult());
+
+        assertEquals(INITIAL_INDENT + BEAN_INVALID_MSG + MID_INDENT + cleanMsg + MID_INDENT + invalidMsg,
+                        bean.getResult(INITIAL_INDENT, NEXT_INDENT, true));
+
+        bean = new BeanValidationResult(NAME, OBJECT);
+        assertFalse(bean.addResult(MY_LIST, "hello", ValidationStatus.INVALID, TEXT1));
+        assertThat(bean.getResult()).contains("\"" + MY_LIST + "\" value \"hello\" INVALID, " + TEXT1);
+    }
+
+    @Test
+    void testValidateNotNull() {
+        assertTrue(bean.validateNotNull("sub-name", "sub-object"));
+        assertTrue(bean.isValid());
+        assertNull(bean.getResult());
+
+        assertFalse(bean.validateNotNull("sub-name", null));
+        assertFalse(bean.isValid());
+        assertEquals(requote(BEAN_INVALID_MSG + "  item 'sub-name' value 'null' INVALID, is null\n"), bean.getResult());
+    }
+
+    @Test
+    void testValidateNotNullList() {
+        List<ValidationResult> list = List.of(clean);
+        assertTrue(bean.validateNotNullList(MY_LIST, list, item -> item));
+        assertTrue(bean.isValid());
+        assertNull(bean.getResult());
+
+        list = Arrays.asList(invalid, invalid);
+        assertFalse(bean.validateNotNullList(MY_LIST, list, item -> item));
+        assertFalse(bean.isValid());
+        assertEquals(requote(BEAN_INVALID_MSG + MY_LIST_INVALID + invalidMsg
+                        + "    " + invalidMsg), bean.getResult());
+    }
+
+    @Test
+    void testValidateNotNullList_NullList() {
+        List<ValidationResult> list = null;
+        assertFalse(bean.validateNotNullList(MY_LIST, list, item -> item));
+        assertFalse(bean.isValid());
+        assertEquals(requote(BEAN_INVALID_MSG + "  item 'my-list' value 'null' INVALID, is null\n"), bean.getResult());
+
+    }
+
+    @Test
+    void testValidateList() {
+        List<ValidationResult> list = null;
+        bean = new BeanValidationResult(NAME, OBJECT);
+        assertTrue(bean.validateList(MY_LIST, list, item -> item));
+        assertTrue(bean.isValid());
+        assertNull(bean.getResult());
+
+        list = List.of(clean);
+        bean = new BeanValidationResult(NAME, OBJECT);
+        assertTrue(bean.validateList(MY_LIST, list, item -> item));
+        assertTrue(bean.isValid());
+        assertNull(bean.getResult());
+
+        // null item in the list
+        list = Arrays.asList(clean, null);
+        bean = new BeanValidationResult(NAME, OBJECT);
+        assertFalse(bean.validateList(MY_LIST, list, item -> item));
+        assertFalse(bean.isValid());
+        assertEquals(requote(BEAN_INVALID_MSG + MY_LIST_INVALID
+                        + "item 'item' value 'null' INVALID, null\n"), bean.getResult());
+
+        list = Arrays.asList(invalid, invalid);
+        bean = new BeanValidationResult(NAME, OBJECT);
+        assertFalse(bean.validateList(MY_LIST, list, item -> item));
+        assertFalse(bean.isValid());
+        assertEquals(requote(BEAN_INVALID_MSG + MY_LIST_INVALID + invalidMsg
+                        + "    " + invalidMsg), bean.getResult());
+
+    }
+
+    @Test
+    void testValidateMap() {
+        Map<String, ValidationResult> map = null;
+        bean = new BeanValidationResult(NAME, OBJECT);
+        assertTrue(bean.validateMap(MY_MAP, map, validMapEntry()));
+        assertTrue(bean.isValid());
+        assertNull(bean.getResult());
+
+        map = Map.of(TEXT1, clean, TEXT2, clean);
+        bean = new BeanValidationResult(NAME, OBJECT);
+        assertTrue(bean.validateMap(MY_MAP, map, validMapEntry()));
+        assertTrue(bean.isValid());
+        assertNull(bean.getResult());
+
+        // null value in the map
+        map = new TreeMap<>();
+        map.put(TEXT1, clean);
+        map.put(TEXT2, null);
+        bean = new BeanValidationResult(NAME, OBJECT);
+        assertFalse(bean.validateMap(MY_MAP, map, validMapEntry()));
+        assertFalse(bean.isValid());
+        assertEquals(requote(BEAN_INVALID_MSG + MY_MAP_INVALID
+                        + "item 'def' value 'null' INVALID, is null\n"), bean.getResult());
+
+        map = Map.of(TEXT1, invalid, TEXT2, invalid);
+        bean = new BeanValidationResult(NAME, OBJECT);
+        assertFalse(bean.validateMap(MY_MAP, map, validMapEntry()));
+        assertFalse(bean.isValid());
+        assertEquals(requote(BEAN_INVALID_MSG + MY_MAP_INVALID + invalidMsg
+                        + "    " + invalidMsg), bean.getResult());
+
+    }
+
+    private BiConsumer<BeanValidationResult, Entry<String, ValidationResult>> validMapEntry() {
+        return (result, entry) -> {
+            var value = entry.getValue();
+            if (value == null) {
+                result.validateNotNull(entry.getKey(), value);
+            } else {
+                result.addResult(value);
+            }
+        };
+    }
+
+    private static String requote(String text) {
+        return text.replace('\'', '"');
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestBeanValidator.java
new file mode 100644 (file)
index 0000000..c421113
--- /dev/null
@@ -0,0 +1,661 @@
+/*-
+ * ============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.parameters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import lombok.Getter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.parameters.annotations.ClassName;
+import org.onap.policy.common.parameters.annotations.Max;
+import org.onap.policy.common.parameters.annotations.Min;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+import org.onap.policy.common.parameters.annotations.Pattern;
+import org.onap.policy.common.parameters.annotations.Size;
+import org.onap.policy.common.parameters.annotations.Valid;
+
+class TestBeanValidator {
+    private static final String TOP = "top";
+    private static final String STR_FIELD = "strValue";
+    private static final String INT_FIELD = "intValue";
+    private static final String NUM_FIELD = "numValue";
+    private static final String ITEMS_FIELD = "items";
+    private static final String STRING_VALUE = "string value";
+    private static final int INT_VALUE = 20;
+
+    private BeanValidator validator;
+
+    @BeforeEach
+    void setUp() {
+        validator = new BeanValidator();
+    }
+
+    @Test
+    void testValidateTop_testValidateFields() {
+        // validate null
+        assertTrue(validator.validateTop(TOP, null).isValid());
+
+        // validate something that has no annotations
+        assertTrue(validator.validateTop(TOP, validator).isValid());
+
+        @NotNull
+        @Getter
+        class Data {
+            String strValue;
+        }
+
+        // one failure case
+        Data data = new Data();
+        BeanValidationResult result = validator.validateTop(TOP, data);
+        assertInvalid("testValidateFields", result, STR_FIELD, "null");
+        assertTrue(result.getResult().contains(TOP));
+
+        // one success case
+        data.strValue = STRING_VALUE;
+        assertTrue(validator.validateTop(TOP, data).isValid());
+
+        @Getter
+        class Derived extends Data {
+            @Min(10)
+            int intValue;
+        }
+
+        Derived derived = new Derived();
+        derived.strValue = STRING_VALUE;
+        derived.intValue = INT_VALUE;
+
+        // success case
+        assertTrue(validator.validateTop(TOP, derived).isValid());
+
+        // failure cases
+        derived.strValue = null;
+        assertInvalid("testValidateFields", validator.validateTop(TOP, derived), STR_FIELD, "null");
+        derived.strValue = STRING_VALUE;
+
+        derived.intValue = 1;
+        assertInvalid("testValidateFields", validator.validateTop(TOP, derived), INT_FIELD, "minimum");
+        derived.intValue = INT_VALUE;
+
+        // both invalid
+        derived.strValue = null;
+        derived.intValue = 1;
+        result = validator.validateTop(TOP, derived);
+        assertInvalid("testValidateFields", result, STR_FIELD, "null");
+        assertInvalid("testValidateFields", result, INT_FIELD, "minimum");
+        derived.strValue = STRING_VALUE;
+        derived.intValue = INT_VALUE;
+    }
+
+    @Test
+    void testVerNotNull() {
+        @Getter
+        class NotNullCheck {
+            @Min(1)
+            @NotNull
+            Integer intValue;
+        }
+
+        NotNullCheck notNullCheck = new NotNullCheck();
+        assertInvalid("testVerNotNull", validator.validateTop(TOP, notNullCheck), INT_FIELD, "null");
+
+        notNullCheck.intValue = INT_VALUE;
+        assertTrue(validator.validateTop(TOP, notNullCheck).isValid());
+
+        notNullCheck.intValue = 0;
+        assertInvalid("testVerNotNull", validator.validateTop(TOP, notNullCheck), INT_FIELD, "minimum");
+    }
+
+    @Test
+    void testVerNotBlank() {
+        @Getter
+        class NotBlankCheck {
+            @NotBlank
+            String strValue;
+        }
+
+        NotBlankCheck notBlankCheck = new NotBlankCheck();
+
+        // null
+        assertTrue(validator.validateTop(TOP, notBlankCheck).isValid());
+
+        // empty
+        notBlankCheck.strValue = "";
+        assertInvalid("testVerNotNull", validator.validateTop(TOP, notBlankCheck), STR_FIELD, "blank");
+
+        // spaces
+        notBlankCheck.strValue = "  ";
+        assertInvalid("testVerNotNull", validator.validateTop(TOP, notBlankCheck), STR_FIELD, "blank");
+
+        // not blank
+        notBlankCheck.strValue = STRING_VALUE;
+        assertTrue(validator.validateTop(TOP, notBlankCheck).isValid());
+
+        /*
+         * Class with "blank" annotation on an integer.
+         */
+        @Getter
+        class NotBlankInt {
+            @NotBlank
+            int intValue;
+        }
+
+        NotBlankInt notBlankInt = new NotBlankInt();
+        notBlankInt.intValue = 0;
+        assertTrue(validator.validateTop(TOP, notBlankInt).isValid());
+    }
+
+    /**
+     * Tests verSize with a collection.
+     */
+    @Test
+    void testVerSizeCollection() {
+        @Getter
+        class CollectionSizeCheck {
+            @Size(min = 3)
+            Collection<Integer> items;
+        }
+
+        CollectionSizeCheck collCheck = new CollectionSizeCheck();
+
+        // valid length - exact
+        collCheck.items = List.of(1, 2, 3);
+        assertThat(validator.validateTop(TOP, collCheck).isValid()).isTrue();
+
+        // valid length - extra
+        collCheck.items = List.of(1, 2, 3, 4);
+        assertThat(validator.validateTop(TOP, collCheck).isValid()).isTrue();
+
+        // too few
+        collCheck.items = List.of(1, 2);
+        assertInvalid("testVerSize", validator.validateTop(TOP, collCheck), ITEMS_FIELD, "minimum", "3");
+
+        // null
+        collCheck.items = null;
+        assertThat(validator.validateTop(TOP, collCheck).isValid()).isTrue();
+    }
+
+    /**
+     * Tests verSize with a map.
+     */
+    @Test
+    void testVerSizeMap() {
+        @Getter
+        class MapSizeCheck {
+            @Size(min = 3)
+            Map<Integer, Integer> items;
+        }
+
+        MapSizeCheck mapCheck = new MapSizeCheck();
+
+        // valid length - exact
+        mapCheck.items = Map.of(1, 10, 2, 20, 3, 30);
+        assertThat(validator.validateTop(TOP, mapCheck).isValid()).isTrue();
+
+        // valid length - extra
+        mapCheck.items = Map.of(1, 10, 2, 20, 3, 30, 4, 40);
+        assertThat(validator.validateTop(TOP, mapCheck).isValid()).isTrue();
+
+        // too few
+        mapCheck.items = Map.of(1, 10, 2, 20);
+        assertInvalid("testVerSize", validator.validateTop(TOP, mapCheck), ITEMS_FIELD, "minimum", "3");
+
+        // null
+        mapCheck.items = null;
+        assertThat(validator.validateTop(TOP, mapCheck).isValid()).isTrue();
+    }
+
+    /**
+     * Tests verSize with an object for which it doesn't apply.
+     */
+    @Test
+    void testVerSizeOther() {
+        @Getter
+        class OtherSizeCheck {
+            @Size(min = 3)
+            Integer items;
+        }
+
+        OtherSizeCheck otherCheck = new OtherSizeCheck();
+
+        otherCheck.items = 10;
+        assertThat(validator.validateTop(TOP, otherCheck).isValid()).isTrue();
+    }
+
+    @Test
+    void testVerRegex() {
+        @Getter
+        class RegexCheck {
+            @Pattern(regexp = "[a-f]*")
+            String strValue;
+        }
+
+        RegexCheck regexCheck = new RegexCheck();
+
+        // does not match
+        regexCheck.strValue = "xyz";
+        assertInvalid("testVerRegex", validator.validateTop(TOP, regexCheck), STR_FIELD,
+                        "does not match regular expression [a-f]");
+
+        // matches
+        regexCheck.strValue = "abcabc";
+        assertTrue(validator.validateTop(TOP, regexCheck).isValid());
+
+        // invalid regex
+        @Getter
+        class InvalidRegexCheck {
+            @Pattern(regexp = "[a-f")
+            String strValue;
+        }
+
+        InvalidRegexCheck invalidRegexCheck = new InvalidRegexCheck();
+
+        // does not match
+        invalidRegexCheck.strValue = "abc";
+        assertInvalid("testVerRegex", validator.validateTop(TOP, invalidRegexCheck), STR_FIELD,
+                        "does not match regular expression [a-f");
+
+        // matches
+        regexCheck.strValue = "abcabc";
+        assertTrue(validator.validateTop(TOP, regexCheck).isValid());
+
+        /*
+         * Class with "regex" annotation on an integer.
+         */
+        @Getter
+        class RegexInt {
+            @Pattern(regexp = "[a-f]*")
+            int intValue;
+        }
+
+        RegexInt regexInt = new RegexInt();
+        regexInt.intValue = 0;
+        assertInvalid("testVerRegex", validator.validateTop(TOP, regexInt), INT_FIELD,
+                        "does not match regular expression [a-f]");
+    }
+
+    @Test
+    void testVerMax() {
+        /*
+         * Field is not a number.
+         */
+        @Getter
+        class NonNumeric {
+            @Max(100)
+            String strValue;
+        }
+
+        NonNumeric nonNumeric = new NonNumeric();
+        nonNumeric.strValue = STRING_VALUE;
+        assertTrue(validator.validateTop(TOP, nonNumeric).isValid());
+
+        /*
+         * Integer field.
+         */
+        @Getter
+        class IntField {
+            @Max(100)
+            Integer intValue;
+        }
+
+        // ok value
+        IntField intField = new IntField();
+        assertNumeric(intField, value -> {
+            intField.intValue = value;
+        }, INT_FIELD, "maximum", 100, 101);
+
+        /*
+         * Long field.
+         */
+        @Getter
+        class LongField {
+            @Max(100)
+            Long numValue;
+        }
+
+        // ok value
+        LongField longField = new LongField();
+        assertNumeric(longField, value -> {
+            longField.numValue = (long) value;
+        }, NUM_FIELD, "maximum", 100, 101);
+
+        /*
+         * Float field.
+         */
+        @Getter
+        class FloatField {
+            @Max(100)
+            Float numValue;
+        }
+
+        // ok value
+        FloatField floatField = new FloatField();
+        assertNumeric(floatField, value -> {
+            floatField.numValue = (float) value;
+        }, NUM_FIELD, "maximum", 100, 101);
+
+        /*
+         * Double field.
+         */
+        @Getter
+        class DoubleField {
+            @Max(100)
+            Double numValue;
+        }
+
+        // ok value
+        DoubleField doubleField = new DoubleField();
+        assertNumeric(doubleField, value -> {
+            doubleField.numValue = (double) value;
+        }, NUM_FIELD, "maximum", 100, 101);
+
+        /*
+         * Atomic Integer field (which is a subclass of Number).
+         */
+        @Getter
+        class AtomIntValue {
+            @Max(100)
+            AtomicInteger numValue;
+        }
+
+        // ok value
+        AtomIntValue atomIntField = new AtomIntValue();
+        atomIntField.numValue = new AtomicInteger(INT_VALUE);
+        assertTrue(validator.validateTop(TOP, atomIntField).isValid());
+
+        // invalid value - should be OK, because it isn't an Integer
+        atomIntField.numValue.set(101);
+        assertTrue(validator.validateTop(TOP, atomIntField).isValid());
+    }
+
+    @Test
+    void testVerMin() {
+        /*
+         * Field is not a number.
+         */
+        @Getter
+        class NonNumeric {
+            @Min(10)
+            String strValue;
+        }
+
+        NonNumeric nonNumeric = new NonNumeric();
+        nonNumeric.strValue = STRING_VALUE;
+        assertTrue(validator.validateTop(TOP, nonNumeric).isValid());
+
+        /*
+         * Integer field.
+         */
+        @Getter
+        class IntField {
+            @Min(10)
+            Integer intValue;
+        }
+
+        // ok value
+        IntField intField = new IntField();
+        assertNumeric(intField, value -> {
+            intField.intValue = value;
+        }, INT_FIELD, "minimum", 10, 1);
+
+        /*
+         * Long field.
+         */
+        @Getter
+        class LongField {
+            @Min(10)
+            Long numValue;
+        }
+
+        // ok value
+        LongField longField = new LongField();
+        assertNumeric(longField, value -> {
+            longField.numValue = (long) value;
+        }, NUM_FIELD, "minimum", 10, 1);
+
+        /*
+         * Float field.
+         */
+        @Getter
+        class FloatField {
+            @Min(10)
+            Float numValue;
+        }
+
+        // ok value
+        FloatField floatField = new FloatField();
+        assertNumeric(floatField, value -> {
+            floatField.numValue = (float) value;
+        }, NUM_FIELD, "minimum", 10, 1);
+
+        /*
+         * Double field.
+         */
+        @Getter
+        class DoubleField {
+            @Min(10)
+            Double numValue;
+        }
+
+        // ok value
+        DoubleField doubleField = new DoubleField();
+        assertNumeric(doubleField, value -> {
+            doubleField.numValue = (double) value;
+        }, NUM_FIELD, "minimum", 10, 1);
+
+        /*
+         * Atomic Integer field (which is a subclass of Number).
+         */
+        @Getter
+        class AtomIntValue {
+            @Min(10)
+            AtomicInteger numValue;
+        }
+
+        // ok value
+        AtomIntValue atomIntField = new AtomIntValue();
+        atomIntField.numValue = new AtomicInteger(INT_VALUE);
+        assertTrue(validator.validateTop(TOP, atomIntField).isValid());
+
+        // invalid value - should be OK, because it isn't an Integer
+        atomIntField.numValue.set(101);
+        assertTrue(validator.validateTop(TOP, atomIntField).isValid());
+    }
+
+    @Test
+    void testVerClassName() {
+        @Getter
+        class ClassNameCheck {
+            @ClassName
+            String strValue;
+        }
+
+        ClassNameCheck classCheck = new ClassNameCheck();
+
+        // null should be OK
+        classCheck.strValue = null;
+        assertTrue(validator.validateTop(TOP, classCheck).isValid());
+
+        // valid class name
+        classCheck.strValue = getClass().getName();
+        assertTrue(validator.validateTop(TOP, classCheck).isValid());
+
+        // invalid class name
+        classCheck.strValue = "<unknown class>";
+        assertInvalid("testVerClassName", validator.validateTop(TOP, classCheck),
+                        STR_FIELD, "class is not in the classpath");
+    }
+
+    @Test
+    void testVerCascade() {
+        @Getter
+        class Item {
+            @NotNull
+            Integer intValue;
+        }
+
+        @Getter
+        class Container {
+            @Valid
+            Item checked;
+
+            Item unchecked;
+
+            @Valid
+            List<Item> items;
+
+            @Valid
+            Map<String, Item> itemMap;
+        }
+
+        Container cont = new Container();
+        cont.unchecked = new Item();
+        cont.items = List.of(new Item());
+        cont.itemMap = Map.of(STRING_VALUE, new Item());
+
+        cont.checked = null;
+        assertTrue(validator.validateTop(TOP, cont).isValid());
+
+        cont.checked = new Item();
+
+        assertInvalid("testVerCascade", validator.validateTop(TOP, cont), INT_FIELD, "null");
+
+        cont.checked.intValue = INT_VALUE;
+        assertTrue(validator.validateTop(TOP, cont).isValid());
+    }
+
+    @Test
+    void testVerCollection() {
+        @Getter
+        class Container {
+            List<@Min(5) Integer> items;
+
+            // not a collection - should not be checked
+            @Valid
+            String strValue;
+
+            String noAnnotations;
+        }
+
+        Container cont = new Container();
+        cont.strValue = STRING_VALUE;
+        cont.noAnnotations = STRING_VALUE;
+
+        // null collection - always valid
+        assertTrue(validator.validateTop(TOP, cont).isValid());
+
+        // empty collection - always valid
+        cont.items = List.of();
+        assertTrue(validator.validateTop(TOP, cont).isValid());
+
+        cont.items = List.of(-10, -20);
+        assertThat(validator.validateTop(TOP, cont).getResult()).contains("\"0\"", "-10", "\"1\"", "-20", "minimum");
+
+        cont.items = List.of(10, -30);
+        assertThat(validator.validateTop(TOP, cont).getResult()).contains("\"1\"", "-30", "minimum")
+                        .doesNotContain("\"0\"");
+
+        cont.items = List.of(10, 20);
+        assertTrue(validator.validateTop(TOP, cont).isValid());
+    }
+
+    @Test
+    void testVerMap() {
+        @Getter
+        class Container {
+            Map<String, @Min(5) Integer> items;
+
+            // not a map
+            @NotBlank
+            String strValue;
+
+            String noAnnotations;
+        }
+
+        Container cont = new Container();
+        cont.strValue = STRING_VALUE;
+        cont.noAnnotations = STRING_VALUE;
+
+        // null map - always valid
+        assertTrue(validator.validateTop(TOP, cont).isValid());
+
+        // empty map - always valid
+        cont.items = Map.of();
+        assertTrue(validator.validateTop(TOP, cont).isValid());
+
+        cont.items = Map.of("abc", -10, "def", -20);
+        assertThat(validator.validateTop(TOP, cont).getResult()).contains("abc", "-10", "def", "-20", "minimum");
+
+        cont.items = Map.of("abc", 10, "def", -30);
+        assertThat(validator.validateTop(TOP, cont).getResult()).contains("def", "-30", "minimum")
+                        .doesNotContain("abc");
+
+        cont.items = Map.of("abc", 10, "def", 20);
+        assertTrue(validator.validateTop(TOP, cont).isValid());
+    }
+
+    @Test
+    void testGetEntryName() {
+        assertThat(validator.getEntryName(makeEntry(null))).isEmpty();
+        assertThat(validator.getEntryName(makeEntry(""))).isEmpty();
+        assertThat(validator.getEntryName(makeEntry(STRING_VALUE))).isEqualTo(STRING_VALUE);
+    }
+
+    /**
+     * Makes a Map entry with the given key and value.
+     *
+     * @param key desired key
+     * @return a new Map entry
+     */
+    private Map.Entry<String, Integer> makeEntry(String key) {
+        HashMap<String, Integer> map = new HashMap<>();
+        map.put(key, 0);
+        return map.entrySet().iterator().next();
+    }
+
+    private <T> void assertNumeric(T object, Consumer<Integer> setter, String fieldName,
+                                   String expectedText, int edge, int outside) {
+        setter.accept(TestBeanValidator.INT_VALUE);
+        assertTrue(validator.validateTop(TOP, object).isValid());
+
+        // on the edge
+        setter.accept(edge);
+        assertTrue(validator.validateTop(TOP, object).isValid());
+
+        // invalid
+        setter.accept(outside);
+        assertInvalid("testVerNotNull", validator.validateTop(TOP, object), fieldName, expectedText);
+    }
+
+
+    private void assertInvalid(String testName, BeanValidationResult result, String... text) {
+        assertThat(result.getResult()).describedAs(testName).contains(text);
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestFieldValidator.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestFieldValidator.java
new file mode 100644 (file)
index 0000000..0659ad8
--- /dev/null
@@ -0,0 +1,379 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.parameters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.google.gson.annotations.SerializedName;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.Map;
+import lombok.Getter;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.parameters.annotations.Min;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+class TestFieldValidator extends ValidatorUtil {
+    private static final String INT_LIST_FIELD = "intList";
+    private static final String INT_MAP_FIELD = "intMap";
+    private static final String UNANNOTATED_FIELD = "unannotated";
+    private static final String INT_FIELD = "intValue";
+    private static final int VALID_INT = 10;
+    private static final int INVALID_INT = -10;
+
+    @Getter
+    private int unannotated;
+
+    @Min(0)
+    @Getter
+    private int intValue;
+
+    @Getter
+    private List<@Min(1) Integer> intList;
+
+    @Getter
+    private Map<@NotBlank String, @Min(1) Integer> intMap;
+
+    @SerializedName("annotated_key_map")
+    @Getter
+    private Map<@NotBlank String, Integer> annotatedKeyMap;
+
+    @Getter
+    private Map<String, @Min(1) Integer> annotatedValueMap;
+
+    @Getter
+    private List<Integer> unannotatedList;
+
+    @Getter
+    private Map<String, Integer> unannotatedMap;
+
+    @NotNull
+    @Getter
+    private boolean boolValue;
+
+    @NotNull
+    @Getter
+    private String notNullValue;
+
+    @Min(0)
+    @Getter
+    private static int staticField;
+
+    /**
+     * Has no accessor.
+     */
+    @Min(0)
+    private int noMethod;
+
+    /**
+     * Accessor is {@link #getStaticMethod()}, which is static.
+     */
+    @Min(0)
+    private int staticMethod;
+
+    /**
+     * Accessor is {@link #getVoidMethod()}, which returns a void.
+     */
+    @Min(0)
+    private int voidMethod;
+
+    /**
+     * Accessor is {@link #getParameterizedMethod(boolean)}, which requires a parameter.
+     */
+    @Min(0)
+    private int parameterizedMethod;
+
+    /**
+     * Accessor is {@link #getExMethod()}, which throws an exception.
+     */
+    @Min(0)
+    private int exMethod;
+
+
+    @BeforeEach
+    void setUp() {
+        bean = new BeanValidator();
+    }
+
+    @Test
+    void testGetAnnotation() {
+        // field-level annotation
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse();
+
+        // class-level annotation
+        assertThat(new FieldValidator(bean, ClassAnnot.class, getField(ClassAnnot.class, "text")).isEmpty()).isFalse();
+    }
+
+    @Test
+    void testFieldValidator() throws NoSuchFieldException, SecurityException {
+        /*
+         * Note: nested classes contain fields like "$this", thus the check for "$" in the
+         * variable name is already covered by the other tests.
+         */
+
+        /*
+         * Class with no annotations.
+         */
+        @NotNull
+        class NoAnnotations {
+            @SuppressWarnings("unused")
+            String strValue;
+        }
+
+        Field field = NoAnnotations.class.getDeclaredField("this$0");
+
+        assertThat(new FieldValidator(bean, NoAnnotations.class, field).isEmpty()).isTrue();
+
+        // unannotated
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("unannotated")).isEmpty()).isTrue();
+
+        // these are invalid for various reasons
+
+        Field staticField2 = getField("staticField");
+        assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, staticField2))
+                        .isInstanceOf(IllegalArgumentException.class);
+
+        Field noMethodField = getField("noMethod");
+        assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, noMethodField))
+                        .isInstanceOf(IllegalArgumentException.class);
+
+        // annotated
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse();
+    }
+
+    @Test
+    void testFieldValidator_SetNullAllowed() {
+        // default - null is allowed
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isNullAllowed()).isTrue();
+
+        // field-level NotNull
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("notNullValue")).isNullAllowed())
+                        .isFalse();
+
+        // class-level NotNull
+        assertThat(new FieldValidator(bean, ClassAnnot.class, getField(ClassAnnot.class, "noMethod")).isNullAllowed())
+                        .isFalse();
+    }
+
+    @Test
+    void testAddListValidator() {
+
+        // unannotated
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("unannotatedList")).isEmpty()).isTrue();
+
+        // annotated
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_LIST_FIELD)).isEmpty()).isFalse();
+    }
+
+    @Test
+    void testAddMapValidator() {
+
+        // unannotated
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("unannotatedMap")).isEmpty()).isTrue();
+
+        // annotated
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_MAP_FIELD)).isEmpty()).isFalse();
+
+        // only the key is annotated
+        FieldValidator validator = new FieldValidator(bean, TestFieldValidator.class, getField("annotatedKeyMap"));
+        assertThat(validator.isEmpty()).isFalse();
+
+        BeanValidationResult result = new BeanValidationResult(MY_NAME, this);
+        annotatedKeyMap = Map.of("abc", -10);
+        validator.validateField(result, this);
+        assertThat(result.getResult()).isNull();
+
+        annotatedKeyMap = Map.of(" ", -10);
+        validator.validateField(result, this);
+        assertThat(result.getResult()).contains("annotated_key_map", "blank").doesNotContain("-10");
+
+        // only the value is annotated
+        validator = new FieldValidator(bean, TestFieldValidator.class, getField("annotatedValueMap"));
+        assertThat(validator.isEmpty()).isFalse();
+
+        result = new BeanValidationResult(MY_NAME, this);
+        annotatedValueMap = Map.of(" ", 10);
+        validator.validateField(result, this);
+        assertThat(result.getResult()).isNull();
+
+        annotatedValueMap = Map.of(" ", -10);
+        validator.validateField(result, this);
+        assertThat(result.getResult()).doesNotContain("blank").contains("annotatedValueMap", "\" \"", "-10");
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    void testValidateField_testGetValue() {
+        // unannotated
+        BeanValidationResult result = new BeanValidationResult(MY_NAME, this);
+        new FieldValidator(bean, getClass(), getField(UNANNOTATED_FIELD)).validateField(result, this);
+        assertThat(result.getResult()).isNull();
+
+        // valid
+        intValue = VALID_INT;
+        result = new BeanValidationResult(MY_NAME, this);
+        new FieldValidator(bean, getClass(), getField(INT_FIELD)).validateField(result, this);
+        assertThat(result.getResult()).isNull();
+
+        // invalid
+        intValue = INVALID_INT;
+        result = new BeanValidationResult(MY_NAME, this);
+        new FieldValidator(bean, getClass(), getField(INT_FIELD)).validateField(result, this);
+        assertThat(result.getResult()).contains(INT_FIELD);
+
+        // throws an exception
+        FieldValidator validator = new FieldValidator(bean, TestFieldValidator.class, getField("exMethod"));
+        BeanValidationResult result2 = new BeanValidationResult(MY_NAME, this);
+        assertThatThrownBy(() -> validator.validateField(result2, this)).isInstanceOf(IllegalArgumentException.class)
+                        .getCause().isInstanceOf(InvocationTargetException.class).getCause()
+                        .hasMessage("expected exception");
+    }
+
+    @Test
+    void testValidateField_testGetValue_ListField() {
+        // valid
+        BeanValidationResult result = new BeanValidationResult(MY_NAME, this);
+        intList = List.of(10, 20, 30, 40);
+        new FieldValidator(bean, getClass(), getField(INT_LIST_FIELD)).validateField(result, this);
+        assertThat(result.getResult()).isNull();
+
+        // invalid
+        result = new BeanValidationResult(MY_NAME, this);
+        intList = List.of(9, -8, 7, -6);
+        new FieldValidator(bean, getClass(), getField(INT_LIST_FIELD)).validateField(result, this);
+        assertThat(result.getResult()).doesNotContain("0", "9").contains("1", "-8").doesNotContain("2", "7")
+                        .contains("3", "-6");
+    }
+
+    @Test
+    void testValidateField_testGetValue_MapField() {
+        // valid
+        BeanValidationResult result = new BeanValidationResult(MY_NAME, this);
+        intMap = Map.of("ten", 10, "twenty", 20, "thirty", 30, "forty", 40);
+        new FieldValidator(bean, getClass(), getField(INT_MAP_FIELD)).validateField(result, this);
+        assertThat(result.getResult()).isNull();
+
+        // invalid
+        result = new BeanValidationResult(MY_NAME, this);
+        intMap = Map.of("ten", 9, "twenty", -8, "thirty", 7, "forty", -6);
+        new FieldValidator(bean, getClass(), getField(INT_MAP_FIELD)).validateField(result, this);
+        assertThat(result.getResult()).doesNotContain("ten", "9").contains("twenty", "-8").doesNotContain("thirty", "7")
+                        .contains("forty", "-6");
+    }
+
+    @Test
+    void testClassOnly() {
+        // class-level annotation has no bearing on a static field
+        assertThat(new FieldValidator(bean, ClassAnnot.class, getField(ClassAnnot.class, "staticValue")).isEmpty())
+                        .isTrue();
+
+        // field-level annotation on a static field
+        Field staticField2 = getField("staticField");
+        assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, staticField2))
+                        .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void testGetAccessor() {
+        // uses "getXxx"
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse();
+
+        // uses "isXxx"
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField("boolValue")).isEmpty()).isFalse();
+    }
+
+    @Test
+    void testGetMethod() {
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse();
+
+        // these are invalid for various reasons
+
+        Field noMethodField = getField("noMethod");
+        assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, noMethodField))
+                        .isInstanceOf(IllegalArgumentException.class);
+
+        Field staticMethodField = getField("staticMethod");
+        assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, staticMethodField))
+                        .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void testValidMethod() {
+        assertThat(new FieldValidator(bean, TestFieldValidator.class, getField(INT_FIELD)).isEmpty()).isFalse();
+
+        // these are invalid for various reasons
+
+        Field staticMethodField = getField("staticMethod");
+        assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, staticMethodField))
+                        .isInstanceOf(IllegalArgumentException.class);
+
+        Field voidMethodField = getField("voidMethod");
+        assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, voidMethodField))
+                        .isInstanceOf(IllegalArgumentException.class);
+
+        Field paramMethodField = getField("parameterizedMethod");
+        assertThatThrownBy(() -> new FieldValidator(bean, TestFieldValidator.class, paramMethodField))
+                        .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void testIsFieldAnnotated_testSetFieldAnnotated() {
+        // annotated at the field level
+        assertThat(new FieldValidator(bean, getClass(), getField(INT_FIELD)).isFieldAnnotated()).isTrue();
+
+        // unannotated
+        assertThat(new FieldValidator(bean, getClass(), getField(UNANNOTATED_FIELD)).isFieldAnnotated()).isFalse();
+    }
+
+    public static int getStaticMethod() {
+        return -1000;
+    }
+
+    void getVoidMethod() {
+        // do nothing
+    }
+
+    public int getParameterizedMethod(boolean flag) {
+        return flag ? 0 : 1;
+    }
+
+    public int getExMethod() {
+        throw new RuntimeException("expected exception");
+    }
+
+    @NotNull
+    public static class ClassAnnot {
+        @Getter
+        private String text;
+
+        // no "get" method
+        @SuppressWarnings("unused")
+        private String noMethod;
+
+        @Getter
+        private static int staticValue;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestItemValidator.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestItemValidator.java
new file mode 100644 (file)
index 0000000..b2686fd
--- /dev/null
@@ -0,0 +1,122 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.parameters;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.parameters.annotations.Min;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+class TestItemValidator extends ValidatorUtil {
+
+    // annotated fields - each field must have exactly one annotation
+
+    /**
+     * This annotation does not contain a method returning an array.
+     */
+    @Min(value = 0)
+    private int notArray;
+
+    /**
+     * This annotation doesn't contain any annotations that the {@link BeanValidator}
+     * recognizes.
+     */
+    @Simple
+    private int mismatch;
+
+    /**
+     * No annotations.
+     */
+    @SuppressWarnings("unused")
+    private int noAnnotations;
+
+    /**
+     * One matching annotation.
+     */
+    @NotNull
+    private int match;
+
+    /**
+     * Multiple matching annotations.
+     */
+    @NotNull
+    @NotBlank
+    private String multiMatch;
+
+
+    @BeforeEach
+    void setUp() {
+        bean = new BeanValidator();
+    }
+
+    @Test
+    void testGetAnnotation() {
+        // no matches
+        assertThat(new ItemValidator(bean, getAnnotType("noAnnotations"), true).isEmpty()).isTrue();
+
+        // had a match
+        assertThat(new ItemValidator(bean, getAnnotType("match"), true).checkers).hasSize(1);
+
+        // multiple matches
+        ItemValidator validator = new ItemValidator(bean, getAnnotType("multiMatch"), true);
+        assertThat(validator.checkers).hasSize(2);
+
+        BeanValidationResult result = new BeanValidationResult(MY_NAME, this);
+        validator.validateValue(result, MY_FIELD, HELLO);
+        assertThat(result.getResult()).isNull();
+
+        result = new BeanValidationResult(MY_NAME, this);
+        validator.validateValue(result, MY_FIELD, null);
+        assertThat(result.getResult()).isNotNull();
+
+        result = new BeanValidationResult(MY_NAME, this);
+        validator.validateValue(result, MY_FIELD, "");
+        assertThat(result.getResult()).isNotNull();
+    }
+
+    @Test
+    void testItemValidatorBeanValidatorAnnotation() {
+        assertThat(new ItemValidator(bean, getAnnotType("match")).isEmpty()).isFalse();
+    }
+
+    @Test
+    void testItemValidatorBeanValidatorAnnotationBoolean() {
+        assertThat(new ItemValidator(bean, getAnnotType("match"), true).isEmpty()).isFalse();
+
+        assertThat(new ItemValidator(bean, getAnnotType("match"), false).isEmpty()).isTrue();
+    }
+
+    // these annotations are not recognized by the BeanValidator
+
+    @Retention(RUNTIME)
+    @Target(FIELD)
+    public @interface Simple {
+
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestObjectValidationResult.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestObjectValidationResult.java
new file mode 100644 (file)
index 0000000..c72d4f4
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * ============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.parameters;
+
+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 org.junit.jupiter.api.Test;
+
+class TestObjectValidationResult {
+    private static final String NAME = "my-name";
+    private static final Object OBJECT = "my-object";
+
+    private ObjectValidationResult result;
+
+    @Test
+    void testValidationResultImplStringObjectValidationStatusString() {
+        result = new ObjectValidationResult(NAME, OBJECT, ValidationStatus.INVALID, "invalid data");
+        assertEquals(NAME, result.getName());
+        assertEquals(OBJECT, result.getObject());
+        assertEquals(ValidationStatus.INVALID, result.getStatus());
+        assertEquals("invalid data", result.getMessage());
+    }
+
+    @Test
+    void testGetResult() {
+        result = new ObjectValidationResult(NAME, OBJECT);
+        assertEquals(ValidationStatus.CLEAN, result.getStatus());
+        assertNull(result.getResult());
+        assertEquals(requote("xxx item 'my-name' value 'my-object' CLEAN, item has status CLEAN\n"),
+                        result.getResult("xxx ", "yyy", true));
+
+        result.setResult(ValidationStatus.WARNING, "a warning");
+        assertEquals(ValidationStatus.WARNING, result.getStatus());
+
+        // should not override warning
+        result.setResult(ValidationStatus.OBSERVATION, "an observation");
+        assertEquals(ValidationStatus.WARNING, result.getStatus());
+
+        assertTrue(result.isValid());
+        assertEquals(requote("item 'my-name' value 'my-object' WARNING, a warning\n"), result.getResult());
+
+        result.setResult(ValidationStatus.INVALID, "is invalid");
+        assertEquals(ValidationStatus.INVALID, result.getStatus());
+
+        assertFalse(result.isValid());
+        assertEquals(requote("item 'my-name' value 'my-object' INVALID, is invalid\n"), result.getResult());
+    }
+
+    private String requote(String text) {
+        return text.replace('\'', '"');
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestValidationResultImpl.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestValidationResultImpl.java
new file mode 100644 (file)
index 0000000..f6851b5
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * ============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.parameters;
+
+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 org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestValidationResultImpl {
+    private static final String NAME = "my-name";
+    private static final Object OBJECT = "my-object";
+
+    private MyResult result;
+
+    @BeforeEach
+    void setUp() {
+        result = new MyResult(NAME, OBJECT);
+    }
+
+    @Test
+    void testValidationResultImplStringObjectValidationStatusString() {
+        result = new MyResult(NAME, OBJECT, ValidationStatus.INVALID, "invalid data");
+        assertEquals(NAME, result.getName());
+        assertEquals(OBJECT, result.getObject());
+        assertEquals(ValidationStatus.INVALID, result.getStatus());
+        assertEquals("invalid data", result.getMessage());
+    }
+
+    @Test
+    void testValidateNotNull() {
+        assertTrue(result.validateNotNull());
+        assertTrue(result.isValid());
+        assertNull(result.getResult());
+
+        // now try with null
+        result = new MyResult(NAME, null);
+        assertFalse(result.validateNotNull());
+        assertFalse(result.isValid());
+        assertEquals("INVALID is null", result.getResult());
+    }
+
+    @Test
+    void testSetResultValidationStatus() {
+        result.setResult(ValidationStatus.WARNING);
+        assertEquals(ValidationStatus.WARNING, result.getStatus());
+
+        // should not override warning
+        result.setResult(ValidationStatus.OBSERVATION);
+        assertEquals(ValidationStatus.WARNING, result.getStatus());
+
+        assertTrue(result.isValid());
+        assertEquals("WARNING item has status WARNING", result.getResult());
+    }
+
+    @Test
+    void testSetResult_testGetResult_testGetStatus() {
+        assertEquals(ValidationStatus.CLEAN, result.getStatus());
+        assertEquals("CLEAN item has status CLEAN", result.getResult("xxx ", "yyy", true));
+
+        result.setResult(ValidationStatus.WARNING, "a warning");
+        assertEquals(ValidationStatus.WARNING, result.getStatus());
+
+        // should not override warning
+        result.setResult(ValidationStatus.OBSERVATION, "an observation");
+        assertEquals(ValidationStatus.WARNING, result.getStatus());
+
+        assertTrue(result.isValid());
+        assertEquals("WARNING a warning", result.getResult());
+
+        result.setResult(ValidationStatus.INVALID, "is invalid");
+        assertEquals(ValidationStatus.INVALID, result.getStatus());
+
+        assertFalse(result.isValid());
+        assertEquals("INVALID is invalid", result.getResult());
+    }
+
+    @Test
+    void testGetName() {
+        assertEquals(NAME, result.getName());
+    }
+
+    private static class MyResult extends ValidationResultImpl {
+        public MyResult(String name, Object object) {
+            super(name, object);
+        }
+
+        public MyResult(String name, Object object, ValidationStatus status, String message) {
+            super(name, object, status, message);
+        }
+
+        @Override
+        public String getResult(String initialIndentation, String subIndentation, boolean showClean) {
+            if (!showClean && getStatus() == ValidationStatus.CLEAN) {
+                return null;
+            }
+
+            return (getStatus() + " " + getMessage());
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/TestValueValidator.java b/policy-common/src/test/java/org/onap/policy/common/parameters/TestValueValidator.java
new file mode 100644 (file)
index 0000000..1b42876
--- /dev/null
@@ -0,0 +1,140 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2024-2025 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.parameters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.lang.annotation.Annotation;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.parameters.annotations.Min;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+class TestValueValidator extends ValidatorUtil {
+
+    private ValueValidator validator;
+
+    // these fields just provide place-holders for annotations
+
+    @NotNull
+    @NotBlank
+    private final int annotField = 1;
+
+
+    @BeforeEach
+    void setUp() {
+        validator = new MyValueValidator();
+    }
+
+    @Test
+    void testIsEmpty() {
+        assertThat(validator.isEmpty()).isTrue();
+
+        validator.addAnnotation(NotNull.class, (result2, fieldName, value) -> true);
+        assertThat(validator.isEmpty()).isFalse();
+    }
+
+    @Test
+    void testValidateValue_NullValue() {
+        BeanValidationResult result = new BeanValidationResult(MY_NAME, this);
+
+        validator.validateValue(result, MY_FIELD, null);
+        assertThat(result.getResult()).isNull();
+
+        validator.addAnnotation(NotNull.class, BeanValidationResult::validateNotNull);
+        validator.validateValue(result, MY_FIELD, null);
+        assertThat(result.getResult()).contains(MY_FIELD, "null");
+    }
+
+    @Test
+    void testValidateValue_NotNullValue() {
+        BeanValidationResult result = new BeanValidationResult(MY_NAME, this);
+
+        validator.validateValue(result, MY_FIELD, HELLO);
+        assertThat(result.getResult()).isNull();
+
+        validator.addAnnotation(NotNull.class, BeanValidationResult::validateNotNull);
+        validator.validateValue(result, MY_FIELD, HELLO);
+        assertThat(result.getResult()).isNull();
+    }
+
+    @Test
+    void testAddAnnotationClassOfTChecker() {
+        // the field does not have this annotation
+        validator.addAnnotation(Min.class, (result2, fieldName, value) -> true);
+        assertThat(validator.isEmpty()).isTrue();
+
+        // "null" flag should stay true with this annotation
+        assertThat(validator.isNullAllowed()).isTrue();
+        validator.addAnnotation(NotBlank.class, (result2, fieldName, value) -> true);
+        assertThat(validator.isNullAllowed()).isTrue();
+
+        // "null" flag should become false with this annotation
+        validator.addAnnotation(NotNull.class, (result2, fieldName, value) -> true);
+        assertThat(validator.isNullAllowed()).isFalse();
+    }
+
+    @Test
+    void testAddAnnotationClassOfTCheckerWithAnnotOfT() {
+        // the field does not have this annotation
+        validator.addAnnotation(Min.class, (result2, fieldName, annot, value) -> true);
+        assertThat(validator.isEmpty()).isTrue();
+
+        // indicates the annotation value
+        AtomicBoolean wasNull = new AtomicBoolean(false);
+
+        // the field DOES have this annotation
+        validator.addAnnotation(NotNull.class, (result2, fieldName, annot, value) -> {
+            wasNull.set(annot != null);
+            return result2.validateNotNull(fieldName, value);
+        });
+        assertThat(validator.isEmpty()).isFalse();
+
+        // ensure that the checker is invoked
+        BeanValidationResult result = new BeanValidationResult(MY_NAME, this);
+        validator.validateValue(result, MY_FIELD, HELLO);
+        assertThat(result.getResult()).isNull();
+
+        assertThat(wasNull.get()).isTrue();
+    }
+
+    @Test
+    void testGetAnnotation() {
+        assertThat(new ValueValidator().getAnnotation(NotNull.class)).isNull();
+    }
+
+    /**
+     * Checks for annotations on the "annotField" field.
+     */
+    private static class MyValueValidator extends ValueValidator {
+        @Override
+        public <T extends Annotation> T getAnnotation(Class<T> annotClass) {
+            try {
+                return TestValueValidator.class.getDeclaredField("annotField").getAnnotation(annotClass);
+            } catch (NoSuchFieldException | SecurityException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/ValidatorUtil.java b/policy-common/src/test/java/org/onap/policy/common/parameters/ValidatorUtil.java
new file mode 100644 (file)
index 0000000..e39b5b8
--- /dev/null
@@ -0,0 +1,83 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.parameters;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.Field;
+
+/**
+ * Utilities for validator tests.
+ */
+public class ValidatorUtil {
+    protected static final String MY_NAME = "My-Name";
+    protected static final String MY_FIELD = "My-Field";
+    protected static final String HELLO = "hello";
+
+    protected BeanValidator bean;
+
+    /**
+     * Gets the single annotation for a given field.
+     *
+     * @param fieldName name of the field having the desired annotation
+     * @return the given field's annotation
+     */
+    protected Annotation getAnnot(String fieldName) {
+        return getField(fieldName).getAnnotations()[0];
+    }
+
+    /**
+     * Gets the annotated type for a given field.
+     *
+     * @param fieldName name of the field of interest
+     * @return the given field's annotated type
+     */
+    protected AnnotatedType getAnnotType(String fieldName) {
+        return getField(fieldName).getAnnotatedType();
+    }
+
+    /**
+     * Gets a field from this object.
+     *
+     * @param fieldName name of the field of interest
+     * @return the given field
+     */
+    protected Field getField(String fieldName) {
+        return getField(getClass(), fieldName);
+    }
+
+    /**
+     * Gets a field from a given class.
+     *
+     * @param clazz class containing the field
+     * @param fieldName name of the field of interest
+     * @return the given field
+     */
+    protected Field getField(Class<?> clazz, String fieldName) {
+        try {
+            return clazz.getDeclaredField(fieldName);
+
+        } catch (NoSuchFieldException | SecurityException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/topic/BusTopicParamsTest.java b/policy-common/src/test/java/org/onap/policy/common/parameters/topic/BusTopicParamsTest.java
new file mode 100644 (file)
index 0000000..c474e5f
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * ============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.parameters.topic;
+
+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 java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.BiConsumer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.parameters.topic.BusTopicParams.TopicParamsBuilder;
+
+class BusTopicParamsTest {
+
+    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_PARTITION = "my-partition";
+    public static final String MY_SERIALIZER = "org.apache.kafka.common.serialization.StringSerializer";
+
+    protected Map<String, String> addProps;
+    protected TopicParamsBuilder builder;
+
+    @BeforeEach
+    public void setUp() {
+        addProps = new TreeMap<>();
+        addProps.put("my-key-A", "my-value-A");
+        addProps.put("my-key-B", "my-value-B");
+
+        builder = makeBuilder();
+    }
+
+    @Test
+    void testGetters() {
+        BusTopicParams params = makeBuilder().build();
+
+        Assertions.assertEquals(addProps, params.getAdditionalProps());
+        Assertions.assertEquals(MY_AFT_ENV, params.getAftEnvironment());
+        assertTrue(params.isAllowSelfSignedCerts());
+        Assertions.assertEquals(MY_API_KEY, params.getApiKey());
+        Assertions.assertEquals(MY_API_SECRET, params.getApiSecret());
+        Assertions.assertEquals(MY_BASE_PATH, params.getBasePath());
+        Assertions.assertEquals(MY_CLIENT_NAME, params.getClientName());
+        Assertions.assertEquals(MY_CONS_GROUP, params.getConsumerGroup());
+        Assertions.assertEquals(MY_CONS_INST, params.getConsumerInstance());
+        Assertions.assertEquals(MY_ENV, params.getEnvironment());
+        Assertions.assertEquals(MY_FETCH_LIMIT, params.getFetchLimit());
+        Assertions.assertEquals(MY_FETCH_TIMEOUT, params.getFetchTimeout());
+        Assertions.assertEquals(MY_HOST, params.getHostname());
+        Assertions.assertEquals(MY_LAT, params.getLatitude());
+        Assertions.assertEquals(MY_LONG, params.getLongitude());
+        assertTrue(params.isManaged());
+        Assertions.assertEquals(MY_PARTITION, params.getPartitionId());
+        Assertions.assertEquals(MY_PARTNER, params.getPartner());
+        Assertions.assertEquals(MY_PASS, params.getPassword());
+        Assertions.assertEquals(MY_PORT, params.getPort());
+        Assertions.assertEquals(List.of("localhost"), params.getServers());
+        Assertions.assertEquals(MY_TOPIC, params.getTopic());
+        Assertions.assertEquals(MY_EFFECTIVE_TOPIC, params.getEffectiveTopic());
+        assertTrue(params.isUseHttps());
+        Assertions.assertEquals(MY_USERNAME, params.getUserName());
+    }
+
+    @Test
+    void testBooleanGetters() {
+        // ensure that booleans are independent of each other
+        testBoolean("true:false:false", TopicParamsBuilder::allowSelfSignedCerts);
+        testBoolean("false:true:false", TopicParamsBuilder::managed);
+        testBoolean("false:false:true", TopicParamsBuilder::useHttps);
+    }
+
+    @Test
+    void testValidators() {
+        BusTopicParams params = makeBuilder().build();
+
+        // test validity methods
+        assertTrue(params.isAdditionalPropsValid());
+        assertFalse(params.isAftEnvironmentInvalid());
+        assertTrue(params.isApiKeyValid());
+        assertTrue(params.isApiSecretValid());
+        assertFalse(params.isClientNameInvalid());
+        assertFalse(params.isConsumerGroupInvalid());
+        assertFalse(params.isConsumerInstanceInvalid());
+        assertFalse(params.isEnvironmentInvalid());
+        assertFalse(params.isHostnameInvalid());
+        assertFalse(params.isLatitudeInvalid());
+        assertFalse(params.isLongitudeInvalid());
+        assertFalse(params.isPartitionIdInvalid());
+        assertFalse(params.isPartnerInvalid());
+        assertTrue(params.isPasswordValid());
+        assertFalse(params.isPortInvalid());
+        assertFalse(params.isServersInvalid());
+        assertFalse(params.isTopicInvalid());
+        assertTrue(params.isUserNameValid());
+    }
+
+    @Test
+    void testInvertedValidators() {
+        Assertions.assertFalse(makeBuilder().additionalProps(null).build().isAdditionalPropsValid());
+        Assertions.assertTrue(makeBuilder().aftEnvironment("").build().isAftEnvironmentInvalid());
+        Assertions.assertFalse(makeBuilder().apiKey("").build().isApiKeyValid());
+        Assertions.assertFalse(makeBuilder().apiSecret("").build().isApiSecretValid());
+        Assertions.assertTrue(makeBuilder().clientName("").build().isClientNameInvalid());
+        Assertions.assertTrue(makeBuilder().consumerGroup("").build().isConsumerGroupInvalid());
+        Assertions.assertTrue(makeBuilder().consumerInstance("").build().isConsumerInstanceInvalid());
+        Assertions.assertTrue(makeBuilder().environment("").build().isEnvironmentInvalid());
+        Assertions.assertTrue(makeBuilder().hostname("").build().isHostnameInvalid());
+        Assertions.assertTrue(makeBuilder().latitude("").build().isLatitudeInvalid());
+        Assertions.assertTrue(makeBuilder().longitude("").build().isLongitudeInvalid());
+        Assertions.assertTrue(makeBuilder().partitionId("").build().isPartitionIdInvalid());
+        Assertions.assertTrue(makeBuilder().partner("").build().isPartnerInvalid());
+        Assertions.assertFalse(makeBuilder().password("").build().isPasswordValid());
+        Assertions.assertTrue(makeBuilder().port(-1).build().isPortInvalid());
+        Assertions.assertTrue(makeBuilder().port(65536).build().isPortInvalid());
+        Assertions.assertTrue(makeBuilder().servers(null).build().isServersInvalid());
+        Assertions.assertTrue(makeBuilder().servers(new LinkedList<>()).build().isServersInvalid());
+        Assertions.assertTrue(makeBuilder().servers(List.of("")).build().isServersInvalid());
+        Assertions.assertFalse(makeBuilder().servers(List.of("one-server")).build().isServersInvalid());
+        Assertions.assertTrue(makeBuilder().topic("").build().isTopicInvalid());
+        Assertions.assertFalse(makeBuilder().userName("").build().isUserNameValid());
+    }
+
+    /**
+     * Tests the boolean methods by applying a function, once with {@code false} and once
+     * with {@code true}. Verifies that all the boolean methods return the correct
+     * value by concatenating them.
+     *
+     * @param expectedTrue the string that is expected when {@code true} is passed to the
+     *        method
+     * @param function function to be applied to the builder
+     */
+    private void testBoolean(String expectedTrue, BiConsumer<TopicParamsBuilder, Boolean> function) {
+        TopicParamsBuilder topicParamsBuilder = BusTopicParams.builder();
+
+        // first try the "false" case
+        function.accept(topicParamsBuilder, false);
+
+        BusTopicParams params = topicParamsBuilder.build();
+        assertEquals("false:false:false",
+                        params.isAllowSelfSignedCerts() + ":" + params.isManaged() + ":" + params.isUseHttps());
+
+
+        // now try the "true" case
+        function.accept(topicParamsBuilder, true);
+
+        params = topicParamsBuilder.build();
+        assertEquals(expectedTrue,
+                        params.isAllowSelfSignedCerts() + ":" + params.isManaged() + ":" + params.isUseHttps());
+    }
+
+    public TopicParamsBuilder makeBuilder() {
+
+        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(List.of("localhost")).topic(MY_TOPIC)
+            .effectiveTopic(MY_EFFECTIVE_TOPIC).useHttps(true).allowTracing(true).userName(MY_USERNAME)
+            .serializationProvider(MY_SERIALIZER);
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/parameters/validation/ParameterGroupValidatorTest.java b/policy-common/src/test/java/org/onap/policy/common/parameters/validation/ParameterGroupValidatorTest.java
new file mode 100644 (file)
index 0000000..0c7f29b
--- /dev/null
@@ -0,0 +1,91 @@
+/*-
+ * ============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.parameters.validation;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import jakarta.validation.ConstraintValidatorContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.common.parameters.BeanValidationResult;
+import org.onap.policy.common.parameters.ParameterGroup;
+
+class ParameterGroupValidatorTest {
+
+    private ParameterGroupValidator validator;
+
+    @Mock
+    private ParameterGroup mockParameterGroup;
+
+    @Mock
+    private BeanValidationResult mockBeanValidationResult;
+
+    @Mock
+    private ConstraintValidatorContext mockContext;
+
+    @Mock
+    private ConstraintValidatorContext.ConstraintViolationBuilder mockViolationBuilder;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        validator = new ParameterGroupValidator();
+    }
+
+    @Test
+    void testIsValid_NullValue() {
+        boolean result = validator.isValid(null, mockContext);
+        assertTrue(result, "Expected isValid to return true when value is null");
+    }
+
+    @Test
+    void testIsValid_ValidParameterGroup() {
+        when(mockParameterGroup.validate()).thenReturn(mockBeanValidationResult);
+        when(mockBeanValidationResult.isValid()).thenReturn(true);
+
+        boolean result = validator.isValid(mockParameterGroup, mockContext);
+        assertTrue(result, "Expected isValid to return true when ParameterGroup is valid");
+
+        verify(mockContext, never()).buildConstraintViolationWithTemplate(anyString());
+    }
+
+    @Test
+    void testIsValid_InvalidParameterGroup() {
+        when(mockParameterGroup.validate()).thenReturn(mockBeanValidationResult);
+        when(mockBeanValidationResult.isValid()).thenReturn(false);
+        when(mockBeanValidationResult.getMessage()).thenReturn("Invalid parameters");
+        when(mockContext.buildConstraintViolationWithTemplate(anyString())).thenReturn(mockViolationBuilder);
+
+        boolean result = validator.isValid(mockParameterGroup, mockContext);
+        assertFalse(result, "Expected isValid to return false when ParameterGroup is invalid");
+
+        InOrder inOrder = inOrder(mockContext, mockViolationBuilder);
+        inOrder.verify(mockContext).buildConstraintViolationWithTemplate("Invalid parameters");
+        inOrder.verify(mockViolationBuilder).addConstraintViolation();
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategyTest.java b/policy-common/src/test/java/org/onap/policy/common/spring/utils/CustomImplicitNamingStrategyTest.java
new file mode 100644 (file)
index 0000000..e7c3ad5
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * ============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.spring.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.hibernate.boot.model.naming.Identifier;
+import org.hibernate.boot.model.naming.ImplicitJoinColumnNameSource;
+import org.hibernate.boot.model.relational.Database;
+import org.hibernate.boot.spi.InFlightMetadataCollector;
+import org.hibernate.boot.spi.MetadataBuildingContext;
+import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
+import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+class CustomImplicitNamingStrategyTest {
+
+    static CustomImplicitNamingStrategy strategy;
+
+    @Mock
+    static ImplicitJoinColumnNameSource source;
+
+    @BeforeAll
+    public static void setUpBeforeClass() {
+        strategy = new CustomImplicitNamingStrategy();
+        source = mock(ImplicitJoinColumnNameSource.class);
+    }
+
+    @Test
+    void testDetermineJoinColumnName() {
+        Identifier identifier = new Identifier("identifier", true);
+
+        MetadataBuildingContext buildingContextMock = mock(MetadataBuildingContext.class);
+        InFlightMetadataCollector flightCollectorMock = mock(InFlightMetadataCollector.class);
+        Database databaseMock = mock(Database.class);
+
+        when(flightCollectorMock.getDatabase()).thenReturn(databaseMock);
+        when(source.getReferencedColumnName()).thenReturn(identifier);
+        when(source.getBuildingContext()).thenReturn(buildingContextMock);
+        when(buildingContextMock.getMetadataCollector()).thenReturn(flightCollectorMock);
+
+        JdbcEnvironment environmentMock = mock(JdbcEnvironment.class);
+        when(databaseMock.getJdbcEnvironment()).thenReturn(environmentMock);
+
+        IdentifierHelper helperMock = mock(IdentifierHelper.class);
+        when(environmentMock.getIdentifierHelper()).thenReturn(helperMock);
+        when(helperMock.toIdentifier(anyString())).thenReturn(identifier);
+
+        Identifier result = strategy.determineJoinColumnName(source);
+        assertEquals(identifier, result);
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverterTest.java b/policy-common/src/test/java/org/onap/policy/common/spring/utils/YamlHttpMessageConverterTest.java
new file mode 100644 (file)
index 0000000..0c46741
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * ============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=========================================================
+ */
+
+package org.onap.policy.common.spring.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+
+class YamlHttpMessageConverterTest {
+
+    private YamlHttpMessageConverter converter;
+
+    @BeforeEach
+    void setUp() {
+        converter = new YamlHttpMessageConverter();
+    }
+
+    @Test
+    void testCanReadAndWriteYamlMediaType() {
+        MediaType mediaType = new MediaType("application", "yaml");
+        assertTrue(converter.canRead(Object.class, mediaType));
+        assertTrue(converter.canWrite(Object.class, mediaType));
+    }
+
+    @Test
+    void testReadInternal() throws IOException {
+        // YAML content representing a simple key-value pair as a map
+        String yamlContent = "key: value";
+
+        // Mocking HttpHeaders
+        HttpHeaders headers = mock(HttpHeaders.class);
+        when(headers.getContentType()).thenReturn(MediaType.APPLICATION_JSON);  // Return JSON media type
+
+        // Mocking HttpInputMessage
+        HttpInputMessage inputMessage = mock(HttpInputMessage.class);
+        when(inputMessage.getBody()).thenReturn(new ByteArrayInputStream(yamlContent.getBytes(StandardCharsets.UTF_8)));
+        when(inputMessage.getHeaders()).thenReturn(headers);
+
+        // Now we call the converter's read method and assert the results
+        Map<String, String> result = (Map<String, String>) converter.read(Map.class, null, inputMessage);
+
+        assertNotNull(result);
+        assertEquals("value", result.get("key"));
+    }
+
+
+    @Test
+    void testReadInternalWithException() throws IOException {
+        HttpInputMessage inputMessage = mock(HttpInputMessage.class);
+        when(inputMessage.getBody()).thenThrow(new IOException("IO Exception during reading"));
+
+        assertThrows(HttpMessageNotReadableException.class, () -> converter.read(Map.class, null, inputMessage));
+    }
+
+    @Test
+    void testWriteInternal() throws IOException {
+        // Mocking HttpHeaders
+        HttpHeaders headers = mock(HttpHeaders.class);
+        when(headers.getContentType()).thenReturn(MediaType.APPLICATION_JSON);  // Return JSON media type
+        when(headers.getAcceptCharset()).thenReturn(null);  // Return null to use default charset
+
+        // Mocking HttpOutputMessage
+        HttpOutputMessage outputMessage = mock(HttpOutputMessage.class);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        when(outputMessage.getBody()).thenReturn(outputStream);
+        when(outputMessage.getHeaders()).thenReturn(headers);
+
+        // A simple map to be serialized into YAML
+        Map<String, String> map = new HashMap<>();
+        map.put("key", "value");
+
+        // Calling the converter's write method
+        converter.write(map, null, outputMessage);
+
+        // Verifying the output
+        String result = outputStream.toString(StandardCharsets.UTF_8);
+        assertTrue(result.contains("key: value"));
+    }
+
+
+    @Test
+    void testWriteInternalWithException() throws IOException {
+        // Mocking HttpHeaders
+        HttpHeaders headers = mock(HttpHeaders.class);
+        when(headers.getContentType()).thenReturn(MediaType.APPLICATION_JSON);  // Return YAML media type
+
+        // Mocking HttpOutputMessage to throw an IOException when getBody() is called
+        HttpOutputMessage outputMessage = mock(HttpOutputMessage.class);
+        when(outputMessage.getBody()).thenThrow(new IOException("IO Exception during writing"));
+        when(outputMessage.getHeaders()).thenReturn(headers);
+
+        // A simple map to be serialized into YAML
+        Map<String, String> map = new HashMap<>();
+        map.put("key", "value");
+
+        // We expect the write method to throw a HttpMessageNotWritableException
+        assertThrows(HttpMessageNotWritableException.class, () -> converter.write(map, null, outputMessage));
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderExceptionTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderExceptionTest.java
new file mode 100644 (file)
index 0000000..9e82c5d
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * ============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.utils.coder;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class CoderExceptionTest {
+    private static final String STRING_VALUE = "My String";
+    private static final Throwable CAUSE = new Throwable();
+
+    private CoderException exc;
+
+    @Test
+    void testCoderException() {
+        exc = new CoderException();
+
+        assertNull(exc.getMessage());
+        assertSame(null, exc.getCause());
+        assertNotNull(exc.toString());
+    }
+
+    @Test
+    void testCoderExceptionString() {
+        exc = new CoderException(STRING_VALUE);
+
+        assertEquals(STRING_VALUE, exc.getMessage());
+        assertSame(null, exc.getCause());
+        assertTrue(exc.toString().contains(STRING_VALUE));
+    }
+
+    @Test
+    void testCoderExceptionThrowable() {
+        exc = new CoderException(CAUSE);
+
+        assertEquals(CAUSE.toString(), exc.getMessage());
+        assertSame(CAUSE, exc.getCause());
+        assertNotNull(exc.toString());
+    }
+
+    @Test
+    void testCoderExceptionStringThrowable() {
+        exc = new CoderException(STRING_VALUE, CAUSE);
+
+        assertEquals(STRING_VALUE, exc.getMessage());
+        assertSame(CAUSE, exc.getCause());
+        assertTrue(exc.toString().contains(STRING_VALUE));
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/CoderTest.java
new file mode 100644 (file)
index 0000000..fe72292
--- /dev/null
@@ -0,0 +1,130 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.utils.coder;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class CoderTest {
+    private static final Long LONG = 10L;
+    private static final Integer INTEGER = 10;
+    private static final String INT_TEXT = INTEGER.toString();
+    private static final String TEXT = "some text";
+    private static final String ENCODED = "encoded value";
+    private static final String DECODED = "decoded value";
+
+    private MyCoder coder;
+
+    @BeforeEach
+    void setUp() {
+        coder = new MyCoder();
+    }
+
+    @Test
+    void testConvert() throws CoderException {
+        assertNull(coder.convert(null, String.class));
+
+        // same class of object
+        assertEquals(TEXT, coder.convert(TEXT, String.class));
+        assertEquals(INTEGER, coder.convert(INTEGER, Integer.class));
+
+        // source is a string
+        assertEquals(INTEGER, coder.convert(TEXT, Integer.class));
+
+        // target is a string
+        assertEquals(INT_TEXT, coder.convert(INTEGER, String.class));
+
+        // source and target are different types, neither is a string
+        assertEquals(INTEGER, coder.convert(LONG, Integer.class));
+    }
+
+    private static class MyCoder implements Coder {
+        @Override
+        public String encode(Object object) throws CoderException {
+            return (object.getClass() == String.class ? ENCODED : INT_TEXT);
+        }
+
+        @Override
+        public String encode(Object object, boolean pretty) throws CoderException {
+            // unused
+            return null;
+        }
+
+        @Override
+        public void encode(Writer target, Object object) throws CoderException {
+            // unused
+        }
+
+        @Override
+        public void encode(OutputStream target, Object object) throws CoderException {
+            // unused
+        }
+
+        @Override
+        public void encode(File target, Object object) throws CoderException {
+            // unused
+        }
+
+        @Override
+        public <T> T decode(String json, Class<T> clazz) throws CoderException {
+            return (clazz == String.class ? clazz.cast(DECODED) : clazz.cast(INTEGER));
+        }
+
+        @Override
+        public <T> T decode(Reader source, Class<T> clazz) throws CoderException {
+            // unused
+            return null;
+        }
+
+        @Override
+        public <T> T decode(InputStream source, Class<T> clazz) throws CoderException {
+            // unused
+            return null;
+        }
+
+        @Override
+        public <T> T decode(File source, Class<T> clazz) throws CoderException {
+            // unused
+            return null;
+        }
+
+        @Override
+        public StandardCoderObject toStandard(Object object) throws CoderException {
+            // unused
+            return null;
+        }
+
+        @Override
+        public <T> T fromStandard(StandardCoderObject sco, Class<T> clazz) throws CoderException {
+            // unused
+            return null;
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderObjectTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderObjectTest.java
new file mode 100644 (file)
index 0000000..0e4967b
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2024-2025 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.utils.coder;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class StandardCoderObjectTest {
+    private static final Gson gson = new Gson();
+
+    private static final String PROP1 = "abc";
+    private static final String PROP2 = "ghi";
+    private static final Integer PROP2_INDEX = 1;
+    private static final String PROP_2_B = "jkl";
+    private static final String VAL1 = "def";
+    private static final String VAL2 = "mno";
+    private static final String JSON = "{'abc':'def','ghi':[{},{'jkl':'mno'}]}".replace('\'', '"');
+
+    private StandardCoderObject sco;
+
+    /**
+     * Creates a standard object, populated with some data.
+     *
+     * @throws Exception if an error occurs
+     */
+    @BeforeEach
+    public void setUp() throws Exception {
+        sco = new StandardCoderObject(gson.fromJson(JSON, JsonElement.class));
+    }
+
+    @Test
+    void testStandardCoderObject() {
+        assertNull(new StandardCoderObject().getData());
+    }
+
+    @Test
+    void testStandardCoderObjectJsonElement() {
+        assertNotNull(sco.getData());
+        assertEquals(JSON, gson.toJson(sco.getData()));
+    }
+
+    @Test
+    void testGetString() throws Exception {
+        // one field
+        assertEquals(VAL1, sco.getString(PROP1));
+
+        // multiple fields
+        assertEquals(VAL2, sco.getString(PROP2, PROP2_INDEX, PROP_2_B));
+
+        // not found
+        assertNull(sco.getString("xyz"));
+
+        // read from null object
+        assertNull(new StandardCoderObject().getString());
+        assertNull(new StandardCoderObject().getString(PROP1));
+
+        JsonElement obj = gson.fromJson("{'abc':[]}".replace('\'', '"'), JsonElement.class);
+        sco = new StandardCoderObject(obj);
+
+        // not a primitive
+        assertNull(sco.getString(PROP1));
+
+        // not a JSON object
+        assertNull(sco.getString(PROP1, PROP2));
+
+        // invalid subscript
+        assertThatIllegalArgumentException().isThrownBy(() -> sco.getString(10.0));
+    }
+
+    @Test
+    void testGetFieldFromObject() {
+        // not an object
+        assertNull(sco.getFieldFromObject(fromJson("[]"), PROP1));
+
+        // field doesn't exist
+        assertNull(sco.getFieldFromObject(fromJson("{}"), "non-existent"));
+
+        // field exists
+        assertEquals(4, sco.getFieldFromObject(fromJson("{\"world\":4}"), "world").getAsInt());
+    }
+
+    @Test
+    void testGetItemFromArray() {
+        // not an array
+        assertNull(sco.getItemFromArray(fromJson("{}"), 0));
+
+        // negative index
+        assertThatIllegalArgumentException().isThrownBy(() -> sco.getItemFromArray(fromJson("[]"), -1));
+
+        // index out of bounds
+        assertNull(sco.getItemFromArray(fromJson("[5]"), 1));
+        assertNull(sco.getItemFromArray(fromJson("[5]"), 2));
+
+        // index exists
+        assertEquals(6, sco.getItemFromArray(fromJson("[5,6,7]"), 1).getAsInt());
+
+        // edge case: first and last item
+        assertEquals(50, sco.getItemFromArray(fromJson("[50,60,70]"), 0).getAsInt());
+        assertEquals(700, sco.getItemFromArray(fromJson("[500,600,700]"), 2).getAsInt());
+    }
+
+    private JsonElement fromJson(String json) {
+        return gson.fromJson(json, JsonElement.class);
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardCoderTest.java
new file mode 100644 (file)
index 0000000..269893e
--- /dev/null
@@ -0,0 +1,376 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP PAP
+ * ================================================================================
+ * 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.utils.coder;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSyntaxException;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import lombok.ToString;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class StandardCoderTest {
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+
+    private static final JsonParseException jpe = new JsonParseException(EXPECTED_EXCEPTION);
+    private static final IOException ioe = new IOException(EXPECTED_EXCEPTION);
+
+    private StandardCoder coder;
+
+    @BeforeEach
+    public void setUp() {
+        coder = new StandardCoder();
+    }
+
+    @Test
+    void testConvert() throws CoderException {
+        // null source
+        assertNull(coder.convert(null, StandardCoderObject.class));
+
+        // same class of object
+        StandardCoderObject sco = new StandardCoderObject();
+        assertSame(sco, coder.convert(sco, StandardCoderObject.class));
+
+        // source is a string
+        assertEquals(Integer.valueOf(10), coder.convert("10", Integer.class));
+
+        // target is a string
+        assertEquals("10", coder.convert(10, String.class));
+
+        // source and target are different types, neither is a string
+        sco = coder.convert(Map.of("hello", "world"), StandardCoderObject.class);
+        assertEquals("world", sco.getString("hello"));
+
+        // throw an exeception
+        coder = new StandardCoder() {
+            @Override
+            protected <T> T fromJson(JsonElement json, Class<T> clazz) {
+                throw jpe;
+            }
+        };
+        assertThatThrownBy(() -> coder.convert(10, Long.class)).isInstanceOf(CoderException.class).hasCause(jpe);
+    }
+
+    @Test
+    void testEncodeObject() throws Exception {
+        List<Integer> arr = Arrays.asList(1100, 1110);
+        assertEquals("[1100,1110]", coder.encode(arr));
+
+        // test exception case
+        coder = spy(new StandardCoder());
+        when(coder.toJson(arr)).thenThrow(jpe);
+        assertThatThrownBy(() -> coder.encode(arr)).isInstanceOf(CoderException.class).hasCause(jpe);
+    }
+
+    @Test
+    void testEncodeObjectBoolean() throws Exception {
+        final List<Integer> arr = Arrays.asList(1100, 1110);
+
+        /*
+         * As plain json.
+         */
+        assertEquals("[1100,1110]", coder.encode(arr, false));
+
+        // test exception case
+        coder = spy(new StandardCoder());
+        when(coder.toJson(arr)).thenThrow(jpe);
+        assertThatThrownBy(() -> coder.encode(arr, false)).isInstanceOf(CoderException.class).hasCause(jpe);
+
+
+        /*
+         * As pretty json.
+         */
+        assertEquals("[\n  1100,\n  1110\n]", coder.encode(arr, true));
+
+        // test exception case
+        coder = spy(new StandardCoder());
+        when(coder.toPrettyJson(arr)).thenThrow(jpe);
+        assertThatThrownBy(() -> coder.encode(arr, true)).isInstanceOf(CoderException.class).hasCause(jpe);
+    }
+
+    @Test
+    void testEncodeWriterObject() throws Exception {
+        List<Integer> arr = Arrays.asList(1200, 1210);
+        StringWriter wtr = new StringWriter();
+        coder.encode(wtr, arr);
+        assertEquals("[1200,1210]", wtr.toString());
+
+        // test json exception
+        coder = spy(new StandardCoder());
+        doThrow(jpe).when(coder).toJson(wtr, arr);
+        assertThatThrownBy(() -> coder.encode(wtr, arr)).isInstanceOf(CoderException.class).hasCause(jpe);
+    }
+
+    @Test
+    void testEncodeOutputStreamObject() throws Exception {
+        List<Integer> arr = Arrays.asList(1300, 1310);
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        coder.encode(stream, arr);
+        assertEquals("[1300,1310]", stream.toString("UTF-8"));
+
+        // test json exception
+        Writer wtr = new StringWriter();
+        coder = spy(new StandardCoder());
+        when(coder.makeWriter(stream)).thenReturn(wtr);
+        doThrow(jpe).when(coder).toJson(wtr, arr);
+        assertThatThrownBy(() -> coder.encode(stream, arr)).isInstanceOf(CoderException.class).hasCause(jpe);
+
+        // test exception when flushed
+        wtr = spy(new OutputStreamWriter(stream));
+        doThrow(ioe).when(wtr).flush();
+        coder = spy(new StandardCoder());
+        when(coder.makeWriter(stream)).thenReturn(wtr);
+        assertThatThrownBy(() -> coder.encode(stream, arr)).isInstanceOf(CoderException.class).hasCause(ioe);
+    }
+
+    @Test
+    void testEncodeFileObject() throws Exception {
+        File file = new File(getClass().getResource(StandardCoder.class.getSimpleName() + ".json").getFile() + "X");
+        file.deleteOnExit();
+        List<Integer> arr = Arrays.asList(1400, 1410);
+        coder.encode(file, arr);
+        assertEquals("[1400,1410]", new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8));
+
+        // test json exception
+        StringWriter wtr = new StringWriter();
+        coder = spy(new StandardCoder());
+        when(coder.makeWriter(file)).thenReturn(wtr);
+        doThrow(jpe).when(coder).toJson(wtr, arr);
+        assertThatThrownBy(() -> coder.encode(file, arr)).isInstanceOf(CoderException.class).hasCause(jpe);
+
+        // test exception when closed
+        coder = spy(new StandardCoder());
+        wtr = spy(new StringWriter());
+        doThrow(ioe).when(wtr).close();
+        coder = spy(new StandardCoder());
+        when(coder.makeWriter(file)).thenReturn(wtr);
+        assertThatThrownBy(() -> coder.encode(file, arr)).isInstanceOf(CoderException.class).hasCause(ioe);
+    }
+
+    @Test
+    void testDecodeStringClass() throws Exception {
+        String text = "[2200,2210]";
+        assertEquals(text, coder.decode(text, JsonElement.class).toString());
+
+        // test json exception
+        coder = spy(new StandardCoder());
+        when(coder.fromJson(text, JsonElement.class)).thenThrow(jpe);
+        assertThatThrownBy(() -> coder.decode(text, JsonElement.class)).isInstanceOf(CoderException.class)
+                        .hasCause(jpe);
+    }
+
+    @Test
+    void testDecodeReaderClass() throws Exception {
+        String text = "[2300,2310]";
+        assertEquals(text, coder.decode(new StringReader(text), JsonElement.class).toString());
+
+        // test json exception
+        coder = spy(new StandardCoder());
+        StringReader rdr = new StringReader(text);
+        when(coder.fromJson(rdr, JsonElement.class)).thenThrow(jpe);
+        assertThatThrownBy(() -> coder.decode(rdr, JsonElement.class)).isInstanceOf(CoderException.class).hasCause(jpe);
+    }
+
+    @Test
+    void testDecodeInputStreamClass() throws Exception {
+        String text = "[2400,2410]";
+        assertEquals(text,
+                        coder.decode(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)), JsonElement.class)
+                                        .toString());
+
+        // test json exception
+        coder = spy(new StandardCoder());
+        ByteArrayInputStream stream = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
+        StringReader rdr = new StringReader(text);
+        when(coder.makeReader(stream)).thenReturn(rdr);
+        when(coder.fromJson(rdr, JsonElement.class)).thenThrow(jpe);
+        assertThatThrownBy(() -> coder.decode(stream, JsonElement.class)).isInstanceOf(CoderException.class)
+                        .hasCause(jpe);
+    }
+
+    @Test
+    void testDecodeFileClass() throws Exception {
+        File file = new File(getClass().getResource(StandardCoder.class.getSimpleName() + ".json").getFile());
+        String text = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
+        assertEquals(text, coder.decode(file, JsonElement.class).toString());
+
+        // test FileNotFoundException case
+        assertThatThrownBy(() -> coder.decode(new File("unknown-file"), JsonElement.class))
+                        .isInstanceOf(CoderException.class).hasCauseInstanceOf(FileNotFoundException.class);
+
+        // test json exception
+        Reader rdr = new StringReader(text);
+        coder = spy(new StandardCoder());
+        when(coder.makeReader(file)).thenReturn(rdr);
+        when(coder.fromJson(rdr, JsonElement.class)).thenThrow(jpe);
+        assertThatThrownBy(() -> coder.decode(file, JsonElement.class)).isInstanceOf(CoderException.class)
+                        .hasCause(jpe);
+
+        // test IOException case
+        rdr = spy(new FileReader(file));
+        doThrow(ioe).when(rdr).close();
+        coder = spy(new StandardCoder());
+        when(coder.makeReader(file)).thenReturn(rdr);
+        assertThatThrownBy(() -> coder.decode(file, JsonElement.class)).isInstanceOf(CoderException.class)
+                        .hasCause(ioe);
+    }
+
+    @Test
+    void testToJsonTree_testFromJsonJsonElementClassT() {
+        MyMap map = new MyMap();
+        map.props = new LinkedHashMap<>();
+        map.props.put("jel keyA", "jel valueA");
+        map.props.put("jel keyB", "jel valueB");
+
+        JsonElement json = coder.toJsonTree(map);
+        assertEquals("{'props':{'jel keyA':'jel valueA','jel keyB':'jel valueB'}}".replace('\'', '"'), json.toString());
+
+        Object result = coder.fromJson(json, MyMap.class);
+
+        assertNotNull(result);
+        assertEquals("{jel keyA=jel valueA, jel keyB=jel valueB}", result.toString());
+    }
+
+    @Test
+    void testConvertFromDouble() throws Exception {
+        String text = "[listA, {keyA=100}, 200]";
+        assertEquals(text, coder.decode(text, Object.class).toString());
+
+        text = "{keyB=200}";
+        assertEquals(text, coder.decode(text, Object.class).toString());
+    }
+
+    @Test
+    void testToStandard() throws Exception {
+        MyObject obj = new MyObject();
+        obj.abc = "xyz";
+        StandardCoderObject sco = coder.toStandard(obj);
+        assertNotNull(sco.getData());
+        assertEquals("{'abc':'xyz'}".replace('\'', '"'), sco.getData().toString());
+
+        // class instead of object -> exception
+        assertThatThrownBy(() -> coder.toStandard(String.class)).isInstanceOf(CoderException.class);
+    }
+
+    @Test
+    void testFromStandard() throws Exception {
+        MyObject obj = new MyObject();
+        obj.abc = "pdq";
+        StandardCoderObject sco = coder.toStandard(obj);
+
+        MyObject obj2 = coder.fromStandard(sco, MyObject.class);
+        assertEquals(obj.toString(), obj2.toString());
+
+        // null class -> exception
+        assertThatThrownBy(() -> coder.fromStandard(sco, null)).isInstanceOf(CoderException.class);
+    }
+
+    @Test
+    void testStandardTypeAdapter() {
+        String json = "{'abc':'def'}".replace('\'', '"');
+        StandardCoderObject sco = coder.fromJson(json, StandardCoderObject.class);
+        assertNotNull(sco.getData());
+        assertEquals(json, sco.getData().toString());
+        assertEquals(json, coder.toJson(sco));
+
+        // invalid json -> exception
+        StringReader rdr = new StringReader("[");
+        assertThatThrownBy(() -> coder.fromJson(rdr, StandardCoderObject.class))
+                        .isInstanceOf(JsonSyntaxException.class);
+    }
+
+    @Test
+    void testMapDouble() throws Exception {
+        MyMap map = new MyMap();
+        map.props = new HashMap<>();
+        map.props.put("plainString", "def");
+        map.props.put("negInt", -10);
+        map.props.put("doubleVal", 12.5);
+        map.props.put("posLong", 100000000000L);
+
+        String json = coder.encode(map);
+
+        map.props.clear();
+        map = coder.decode(json, MyMap.class);
+
+        assertEquals("def", map.props.get("plainString"));
+        assertEquals(-10, map.props.get("negInt"));
+        assertEquals(100000000000L, map.props.get("posLong"));
+        assertEquals(12.5, map.props.get("doubleVal"));
+
+        // test when decoding into a map
+        @SuppressWarnings("unchecked")
+        Map<String, Object> map2 = coder.decode("{'intValue':10, 'dblVal':20.1}", TreeMap.class);
+        assertEquals("{dblVal=20.1, intValue=10}", map2.toString());
+    }
+
+    @Test
+    void testListDouble() throws Exception {
+        @SuppressWarnings("unchecked")
+        List<Object> list = coder.decode("[10, 20.1, 30]", LinkedList.class);
+        assertEquals("[10, 20.1, 30]", list.toString());
+    }
+
+
+    @ToString
+    private static class MyObject {
+        private String abc;
+    }
+
+    public static class MyMap {
+        private Map<String, Object> props;
+
+        @Override
+        public String toString() {
+            return props.toString();
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/StandardYamlCoderTest.java
new file mode 100644 (file)
index 0000000..d504b82
--- /dev/null
@@ -0,0 +1,114 @@
+/*-
+ * ============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.utils.coder;
+
+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.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.utils.coder.YamlJsonTranslatorTest.Container;
+
+class StandardYamlCoderTest {
+    private static final File YAML_FILE =
+                    new File("src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml");
+
+    private StandardYamlCoder coder;
+    private Container cont;
+
+    @BeforeEach
+    public void setUp() throws CoderException {
+        coder = new StandardYamlCoder();
+        cont = coder.decode(YAML_FILE, Container.class);
+    }
+
+    @Test
+    void testToPrettyJson() throws CoderException {
+        String expected = coder.encode(cont);
+        assertEquals(expected, coder.encode(cont, false));
+
+        String yaml = coder.encode(cont, true);
+        assertEquals(expected, yaml);
+
+        Container cont2 = coder.decode(yaml, Container.class);
+        assertEquals(cont, cont2);
+
+        // test exception cases
+        IllegalArgumentException expex = new IllegalArgumentException("expected exception");
+        coder = spy(new StandardYamlCoder());
+        when(coder.toJson(cont)).thenThrow(expex);
+        assertThatThrownBy(() -> coder.encode(cont, false)).isInstanceOf(CoderException.class).hasCause(expex);
+        assertThatThrownBy(() -> coder.encode(cont, true)).isInstanceOf(CoderException.class).hasCause(expex);
+    }
+
+    @Test
+    void testToJsonObject() throws CoderException {
+        String yaml = coder.encode(cont);
+
+        Container cont2 = coder.decode(yaml, Container.class);
+        assertEquals(cont, cont2);
+    }
+
+    @Test
+    void testToJsonWriterObject() throws CoderException {
+        StringWriter wtr = new StringWriter();
+        coder.encode(wtr, cont);
+        String yaml = wtr.toString();
+
+        Container cont2 = coder.decode(yaml, Container.class);
+        assertEquals(cont, cont2);
+    }
+
+    @Test
+    void testFromJsonStringClassOfT() throws Exception {
+        String yaml = new String(Files.readAllBytes(YAML_FILE.toPath()), StandardCharsets.UTF_8);
+        Container cont2 = coder.decode(yaml, Container.class);
+        assertEquals(cont, cont2);
+    }
+
+    @Test
+    void testFromJsonReaderClassOfT() {
+        YamlJsonTranslatorTest.verify(cont);
+    }
+
+    @Test
+    void testFromJsonDoubleToInteger() throws Exception {
+        Object value = coder.decode("20", Object.class);
+        assertEquals(Integer.valueOf(20), value);
+    }
+
+    @Test
+    void testStandardTypeAdapter() {
+        String yaml = "abc: def\n";
+        StandardCoderObject sco = coder.fromJson(yaml, StandardCoderObject.class);
+        assertNotNull(sco.getData());
+        assertEquals("{'abc':'def'}".replace('\'', '"'), sco.getData().toString());
+        assertEquals(yaml, coder.toJson(sco));
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/coder/YamlJsonTranslatorTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/coder/YamlJsonTranslatorTest.java
new file mode 100644 (file)
index 0000000..563181c
--- /dev/null
@@ -0,0 +1,171 @@
+/*-
+ * ============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.utils.coder;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Map;
+import lombok.EqualsAndHashCode;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.yaml.snakeyaml.error.YAMLException;
+
+class YamlJsonTranslatorTest {
+    private static final File YAML_FILE =
+                    new File("src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml");
+
+    private Container cont;
+    private YamlJsonTranslator translator;
+
+    /**
+     * Creates {@link #translator} and uses it to load {@link #cont}.
+     *
+     * @throws IOException if an error occurs
+     */
+    @BeforeEach
+    public void setUp() throws IOException {
+        translator = new YamlJsonTranslator();
+
+        try (FileReader rdr = new FileReader(YAML_FILE)) {
+            cont = translator.fromYaml(rdr, Container.class);
+        }
+    }
+
+    @Test
+    void testToYamlObject() {
+        String yaml = translator.toYaml(cont);
+
+        Container cont2 = translator.fromYaml(yaml, Container.class);
+        assertEquals(cont, cont2);
+    }
+
+    @Test
+    void testToYamlWriterObject() throws IOException {
+        IOException ex = new IOException("expected exception");
+
+        // writer that throws an exception when the write() method is invoked
+        Writer wtr = new Writer() {
+            @Override
+            public void write(char[] cbuf, int off, int len) throws IOException {
+                throw ex;
+            }
+
+            @Override
+            public void flush() throws IOException {
+                // do nothing
+            }
+
+            @Override
+            public void close() throws IOException {
+                // do nothing
+            }
+        };
+
+        assertThatThrownBy(() -> translator.toYaml(wtr, cont)).isInstanceOf(YAMLException.class);
+
+        wtr.close();
+    }
+
+    @Test
+    void testFromYamlStringClassOfT() throws IOException {
+        String yaml = new String(Files.readAllBytes(YAML_FILE.toPath()), StandardCharsets.UTF_8);
+        Container cont2 = translator.fromYaml(yaml, Container.class);
+        assertEquals(cont, cont2);
+    }
+
+    @Test
+    void testFromYamlReaderClassOfT() {
+        verify(cont);
+    }
+
+    /**
+     * Verifies that the container has contents matching the yaml file.
+     *
+     * @param container container whose contents are to be verified
+     */
+    public static void verify(Container container) {
+        assertNotNull(container.item);
+        assertTrue(container.item.boolVal);
+        assertEquals(1000L, container.item.longVal);
+        assertEquals(1010.1f, container.item.floatVal, 0.00001);
+
+        assertEquals(4, container.list.size());
+        assertNull(container.list.get(1));
+
+        assertEquals(20, container.list.get(0).intVal);
+        assertEquals("string 30", container.list.get(0).stringVal);
+        assertNull(container.list.get(0).nullVal);
+
+        assertEquals(40.0, container.list.get(2).doubleVal, 0.000001);
+        assertNull(container.list.get(2).nullVal);
+        assertNotNull(container.list.get(2).another);
+        assertEquals(50, container.list.get(2).another.intVal);
+
+        assertTrue(container.list.get(3).boolVal);
+
+        assertNotNull(container.map);
+        assertEquals(3, container.map.size());
+
+        assertNotNull(container.map.get("itemA"));
+        assertEquals("stringA", container.map.get("itemA").stringVal);
+
+        assertNotNull(container.map.get("itemB"));
+        assertEquals("stringB", container.map.get("itemB").stringVal);
+
+        double dbl = 123456789012345678901234567890.0;
+        assertEquals(dbl, container.map.get("itemB").doubleVal, 1000.0);
+
+        assertNotNull(container.map.get("itemC"));
+        assertTrue(container.map.get("itemC").boolVal);
+    }
+
+
+    @EqualsAndHashCode
+    public static class Container {
+        protected Item item;
+        protected List<Item> list;
+        protected Map<String, Item> map;
+    }
+
+    @EqualsAndHashCode
+    public static class Item {
+        protected boolean boolVal;
+        protected int intVal;
+        protected long longVal;
+        protected double doubleVal;
+        protected float floatVal;
+        protected String stringVal;
+        protected Object nullVal;
+        protected Item another;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonSerializerTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonSerializerTest.java
new file mode 100644 (file)
index 0000000..aa70143
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * ============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.utils.gson;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringReader;
+import org.junit.jupiter.api.Test;
+
+class GsonSerializerTest {
+
+    @Test
+    void testReadJsonReader() {
+        JsonReader rdr = new JsonReader(new StringReader("10"));
+
+        GsonSerializer<Object> ser = new GsonSerializer<Object>() {
+            @Override
+            public void write(JsonWriter out, Object value) throws IOException {
+                // do nothing
+            }
+        };
+
+        assertThatThrownBy(() -> ser.read(rdr)).isInstanceOf(UnsupportedOperationException.class)
+                        .hasMessage("read from pseudo TypeAdapter");
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsBuilderTest.java
new file mode 100644 (file)
index 0000000..d85653d
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * ============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.utils.gson;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class GsonTestUtilsBuilderTest {
+
+    private GsonTestUtils utils;
+
+    @BeforeEach
+    public void setUp() {
+        utils = new MyBuilder().build();
+    }
+
+    @Test
+    void testBuilderAddMock() {
+        PreMock pre = mock(PreMock.class);
+        when(pre.getId()).thenReturn(2000);
+
+        assertEquals("{\"name\":2000}", utils.gsonEncode(pre));
+    }
+
+    /**
+     * Builder that provides an adapter for mock(PreMock.class).
+     */
+    private static class MyBuilder extends GsonTestUtilsBuilder {
+        public MyBuilder() {
+            TypeAdapterFactory sgson = new TypeAdapterFactory() {
+                @Override
+                public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+                    Class<? super T> clazz = type.getRawType();
+
+                    if (PreMock.class.isAssignableFrom(clazz)) {
+                        return new GsonSerializer<T>() {
+                            @Override
+                            public void write(JsonWriter out, T value) throws IOException {
+                                PreMock obj = (PreMock) value;
+                                out.beginObject().name("name").value(obj.getId()).endObject();
+                            }
+                        };
+                    }
+
+                    return null;
+                }
+            };
+
+            addMock(PreMock.class, sgson);
+        }
+    }
+
+    /**
+     * Class that will be mocked.
+     */
+    public static class PreMock {
+        protected int id = 1000;
+
+        public int getId() {
+            return id;
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/gson/GsonTestUtilsTest.java
new file mode 100644 (file)
index 0000000..d355823
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * ============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.utils.gson;
+
+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.assertNull;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class GsonTestUtilsTest {
+    private static final String HELLO = "hello";
+
+    private GsonTestUtils utils;
+
+    @BeforeEach
+    public void setUp() {
+        utils = new GsonTestUtils();
+    }
+
+    @Test
+    void testGetGson() {
+        assertNotNull(utils.getGson());
+    }
+
+    @Test
+    void testGsonRoundTrip() {
+        Data data = new Data();
+        data.setId(500);
+
+        // try with null text
+        data.setText(null);
+        assertEquals(data.toString(), utils.gsonRoundTrip(data, Data.class).toString());
+
+        // try with non-null text
+        data.setText(HELLO);
+        assertEquals(data.toString(), utils.gsonRoundTrip(data, Data.class).toString());
+    }
+
+    @Test
+    void testCompareGsonObjectClass_testCompareGsonObjectFile() {
+        Data data = new Data();
+        data.setId(500);
+        data.setText(HELLO);
+
+        utils.compareGson(data, GsonTestUtilsTest.class);
+
+        // file not found
+        File file = new File(GsonTestUtilsTest.class.getSimpleName() + "-NotFound.json");
+
+        assertThatThrownBy(() -> utils.compareGson(data, file))
+            .isInstanceOf(JsonParseException.class)
+            .hasCauseInstanceOf(FileNotFoundException.class);
+
+        // force I/O error while reading file
+        GsonTestUtils utils2 = new GsonTestUtils() {
+            @Override
+            protected String readFile(File file) throws IOException {
+                throw new IOException("expected exception");
+            }
+        };
+        assertThatThrownBy(() -> utils2.compareGson(data, GsonTestUtilsTest.class))
+                        .isInstanceOf(JsonParseException.class).hasCauseInstanceOf(IOException.class)
+                        .hasMessage("error reading: GsonTestUtilsTest.json");
+    }
+
+    @Test
+    void testCompareGsonObjectString() {
+        Data data = new Data();
+        data.setId(600);
+        data.setText(HELLO);
+
+        assertThatCode(() -> utils.compareGson(data, "{'id': ${obj.id}, 'text': '${obj.text}'}".replace('\'', '"')))
+                        .doesNotThrowAnyException();
+    }
+
+    @Test
+    void testCompareGsonObjectJsonElement() {
+        Data data = new Data();
+        data.setId(650);
+        data.setText(HELLO);
+
+        JsonObject json = new JsonObject();
+        json.addProperty("id", data.getId());
+        json.addProperty("text", data.getText());
+
+        utils.compareGson(data, json);
+
+        // mismatch
+        data.setText("world");
+        assertThatThrownBy(() -> utils.compareGson(data, json)).isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void testApplyScripts() {
+        Data data = new Data();
+        data.setId(700);
+        data.setText(HELLO);
+
+        String result = utils.applyScripts("no interpolation", data);
+        assertEquals("no interpolation", result);
+
+        result = utils.applyScripts("${obj.id} at start, ${obj.text} in middle, and end ${obj.id}", data);
+        assertEquals("700 at start, hello in middle, and end 700", result);
+
+        // try null value
+        data.setText(null);
+        result = utils.applyScripts("use ${obj.text} this", data);
+        assertEquals("use null this", result);
+        assertEquals("use null this", utils.applyScripts("use ${obj.text} this", null));
+    }
+
+    @Test
+    void testReorderJsonObject() {
+        // insert properties in a non-alphabetical order
+        JsonObject inner = new JsonObject();
+        inner.addProperty("objBint", 100);
+        inner.add("objBNull", JsonNull.INSTANCE);
+        inner.addProperty("objB", true);
+
+        JsonArray arr = new JsonArray();
+        arr.add(110);
+        arr.add(inner);
+        arr.add(false);
+
+        JsonObject outer = new JsonObject();
+        outer.add("objANull", JsonNull.INSTANCE);
+        outer.addProperty("objA", true);
+        outer.addProperty("objAStr", "obj-a-string");
+        outer.add("nested-array", arr);
+
+        outer = utils.reorder(outer);
+        assertEquals("{'nested-array':[110,{'objB':true,'objBint':100},false],'objA':true,'objAStr':'obj-a-string'}"
+                        .replace('\'', '"'), outer.toString());
+    }
+
+    @Test
+    void testReorderJsonArray() {
+        // insert properties in a non-alphabetical order
+        JsonObject inner = new JsonObject();
+        inner.add("objCNull", JsonNull.INSTANCE);
+        inner.addProperty("objCStr", "obj-c-string");
+        inner.addProperty("objC", true);
+
+        JsonArray arr = new JsonArray();
+        arr.add(200);
+        arr.add(inner);
+        arr.add(false);
+
+        arr = utils.reorder(arr);
+        assertEquals("[200,{'objC':true,'objCStr':'obj-c-string'},false]".replace('\'', '"'), arr.toString());
+    }
+
+    @Test
+    void testReorderJsonElement() {
+        // null element
+        JsonElement jsonEl = null;
+        assertNull(utils.reorder(jsonEl));
+
+        // object element
+        JsonObject obj = new JsonObject();
+        obj.add("objDNull", JsonNull.INSTANCE);
+        obj.addProperty("objDStr", "obj-d-string");
+        obj.addProperty("objD", true);
+        jsonEl = obj;
+        jsonEl = utils.reorder(jsonEl);
+        assertEquals("{'objD':true,'objDStr':'obj-d-string'}".replace('\'', '"'), jsonEl.toString());
+
+        // boolean
+        jsonEl = obj.get("objD");
+        jsonEl = utils.reorder(jsonEl);
+        assertEquals("true", jsonEl.toString());
+
+        // JsonNull
+        jsonEl = JsonNull.INSTANCE;
+        jsonEl = utils.reorder(jsonEl);
+        assertEquals("null", jsonEl.toString());
+
+        // array element
+        JsonObject inner = new JsonObject();
+        inner.add("objENull", JsonNull.INSTANCE);
+        inner.addProperty("objEStr", "obj-e-string");
+        inner.addProperty("objE", true);
+
+        JsonArray arr = new JsonArray();
+        arr.add(300);
+        arr.add(inner);
+        arr.add(false);
+        jsonEl = arr;
+        jsonEl = utils.reorder(jsonEl);
+        assertEquals("[300,{'objE':true,'objEStr':'obj-e-string'},false]".replace('\'', '"'), jsonEl.toString());
+    }
+
+    @Setter
+    @Getter
+    @ToString
+    public static class Data {
+        private int id;
+        private String text;
+
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/network/NetworkUtilTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/network/NetworkUtilTest.java
new file mode 100644 (file)
index 0000000..ccd67fc
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * ============LICENSE_START=======================================================
+ * policy-utils
+ * ================================================================================
+ * 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.utils.network;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+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 java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class NetworkUtilTest {
+    protected static Logger logger = LoggerFactory.getLogger(NetworkUtilTest.class);
+
+    private static final String LOCALHOST = "localhost";
+
+    @Test
+    void test() throws InterruptedException, IOException {
+        assertNotNull(NetworkUtil.IPV4_WILDCARD_ADDRESS);
+        assertFalse(NetworkUtil.isTcpPortOpen(LOCALHOST, NetworkUtil.allocPort(), 1, 5));
+        assertNotNull(NetworkUtil.getHostname());
+        assertNotNull(NetworkUtil.getHostIp());
+    }
+
+    @Test
+    void testAlwaysTrustManager() throws Exception {
+        TrustManager[] mgrarr = NetworkUtil.getAlwaysTrustingManager();
+        assertEquals(1, mgrarr.length);
+        assertInstanceOf(X509TrustManager.class, mgrarr[0]);
+
+        X509TrustManager mgr = (X509TrustManager) mgrarr[0];
+        assertNotNull(mgr.getAcceptedIssuers());
+        assertEquals(0, mgr.getAcceptedIssuers().length);
+
+        // these should not throw exceptions
+        mgr.checkClientTrusted(null, null);
+        mgr.checkServerTrusted(null, null);
+    }
+
+    @Test
+    void testAllocPort_testAllocPortString__testAllocPortInetSocketAddress() throws Exception {
+        // allocate wild-card port
+        int wildCardPort = NetworkUtil.allocPort();
+        assertNotEquals(0, wildCardPort);
+
+        // verify that we can listen on the port
+        try (ServerSocket wildSocket = new ServerSocket(wildCardPort)) {
+            new Accepter(wildSocket).start();
+            assertTrue(NetworkUtil.isTcpPortOpen(LOCALHOST, wildCardPort, 5, 1000L));
+        }
+
+
+        // allocate port using host name
+        int localPort = NetworkUtil.allocPort(LOCALHOST);
+        assertNotEquals(0, localPort);
+
+        // the OS should have allocated a new port, even though the first has been closed
+        assertNotEquals(wildCardPort, localPort);
+
+        try (ServerSocket localSocket = new ServerSocket()) {
+            localSocket.bind(new InetSocketAddress(LOCALHOST, localPort));
+            new Accepter(localSocket).start();
+            assertTrue(NetworkUtil.isTcpPortOpen(LOCALHOST, localPort, 5, 1000L));
+        }
+    }
+
+    @Test
+    void testGenUniqueName() {
+        String name = NetworkUtil.genUniqueName(LOCALHOST);
+        assertThat(name).isNotBlank().isNotEqualTo(LOCALHOST);
+
+        // second call should generate a different value
+        assertThat(NetworkUtil.genUniqueName(LOCALHOST)).isNotEqualTo(name);
+    }
+
+    /**
+     * Thread that accepts a connection on a socket.
+     */
+    private static class Accepter extends Thread {
+        private ServerSocket socket;
+
+        public Accepter(ServerSocket socket) {
+            this.socket = socket;
+            setDaemon(true);
+        }
+
+        @Override
+        public void run() {
+            try (Socket server = socket.accept()) { //NOSONAR
+                // do nothing
+
+            } catch (IOException e) {
+                logger.error("socket not accepted", e);
+            }
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/properties/PropertyUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/properties/PropertyUtilsTest.java
new file mode 100644 (file)
index 0000000..a8b37f5
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 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.utils.properties;
+
+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.util.Properties;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class PropertyUtilsTest {
+    private static final String DFLT_STRING = "my-default";
+    private static final int DLFT_INT = 1000;
+
+    private PropertyUtils utils;
+    private String invalidName;
+    private String invalidValue;
+    private Exception invalidEx;
+
+    /**
+     * Initializes {@link #utils}.
+     */
+    @BeforeEach
+    public void setUp() {
+        Properties properties = new Properties();
+        properties.put("myPrefix.my-string", "some text");
+        properties.put("myPrefix.empty-string", "");
+
+        properties.put("myPrefix.my-bool", "true");
+        properties.put("myPrefix.my-bool2", "false");
+        properties.put("myPrefix.empty-bool", "");
+        properties.put("myPrefix.invalid-bool", "not a bool");
+
+        properties.put("myPrefix.my-int", "100");
+        properties.put("myPrefix.my-int2", "200");
+        properties.put("myPrefix.empty-int", "");
+        properties.put("myPrefix.invalid-int", "not an int");
+
+        utils = new PropertyUtils(properties, "myPrefix", (name, value, ex) -> {
+            invalidName = name;
+            invalidValue = value;
+            invalidEx = ex;
+        });
+    }
+
+    @Test
+    void testGetString() {
+        assertEquals("some text", utils.getString(".my-string", DFLT_STRING));
+        assertEquals(DFLT_STRING, utils.getString(".empty-string", DFLT_STRING));
+        assertEquals(DFLT_STRING, utils.getString(".missing-string", DFLT_STRING));
+
+        assertNull(invalidName);
+        assertNull(invalidValue);
+        assertNull(invalidEx);
+    }
+
+    @Test
+    void testGetBoolean() {
+        assertTrue(utils.getBoolean(".my-bool", false));
+        assertFalse(utils.getBoolean(".my-bool2", true));
+        assertTrue(utils.getBoolean(".empty-bool", true));
+        assertFalse(utils.getBoolean(".invalid-bool", true));
+        assertTrue(utils.getBoolean(".missing-bool", true));
+
+        assertNull(invalidName);
+        assertNull(invalidValue);
+        assertNull(invalidEx);
+    }
+
+    @Test
+    void testGetInteger() {
+        assertEquals(100, utils.getInteger(".my-int", DLFT_INT));
+        assertEquals(200, utils.getInteger(".my-int2", DLFT_INT));
+        assertEquals(DLFT_INT, utils.getInteger(".empty-int", DLFT_INT));
+        assertEquals(DLFT_INT, utils.getInteger(".missing-int", DLFT_INT));
+
+        assertNull(invalidName);
+        assertNull(invalidValue);
+        assertNull(invalidEx);
+
+        assertEquals(DLFT_INT, utils.getInteger(".invalid-int", DLFT_INT));
+
+        assertEquals("myPrefix.invalid-int", invalidName);
+        assertEquals("not an int", invalidValue);
+        assertTrue(invalidEx instanceof NumberFormatException);
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/resources/ResourceUtilsTest.java
new file mode 100644 (file)
index 0000000..7b41d97
--- /dev/null
@@ -0,0 +1,337 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2018 Ericsson. All rights reserved.
+ *  Modifications Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved.
+ *  Modifications Copyright (C) 2020-2021, 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.common.utils.resources;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Set;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The Class ResourceUtilsTest.
+ *
+ * @author Liam Fallon (liam.fallon@ericsson.com)
+ */
+class ResourceUtilsTest {
+    private File tmpDir = null;
+    private File tmpEmptyFile = null;
+    private File tmpUsedFile = null;
+
+    private String jarDirResource = null;
+    private String jarFileResource = null;
+
+    private static final String RESOURCES_PATH = "src/test/resources/";
+    private static final String PATH_DIR_RESOURCE = "testdir";
+    private static final String PATH_FILE_RESOURCE = "testdir/testfile.xml";
+
+    private static final String NON_EXISTENT_RESOURCE = "somewhere/over/the/rainbow";
+    private static final String INVALID_RESOURCE = "@%%%\\\\_:::DESD";
+
+    /**
+     * Setup resource utils test.
+     *
+     * @throws IOException Signals that an I/O exception has occurred.
+     */
+    @BeforeEach
+    public void setupResourceUtilsTest() throws IOException {
+        tmpDir = new File(System.getProperty("java.io.tmpdir"));
+        tmpEmptyFile = File.createTempFile(this.getClass().getName(), ".tmp");
+        tmpUsedFile = File.createTempFile(this.getClass().getName(), ".tmp");
+
+        jarDirResource = "META-INF";
+        jarFileResource = "META-INF/MANIFEST.MF";
+
+        try (final FileWriter fileWriter = new FileWriter(tmpUsedFile)) {
+            fileWriter.write("Bluebirds fly over the rainbow");
+        }
+    }
+
+    /**
+     * Clean resource utils test.
+     */
+    @AfterEach
+    public void cleanDownResourceUtilsTest() {
+        assertTrue(tmpEmptyFile.delete());
+        assertTrue(tmpUsedFile.delete());
+    }
+
+    /**
+     * Test get url resource.
+     */
+    @Test
+    void testgetUrlResource() {
+        URL theUrl = ResourceUtils.getUrlResource(tmpDir.getAbsolutePath());
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource(tmpEmptyFile.getAbsolutePath());
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource(tmpUsedFile.getAbsolutePath());
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource(jarDirResource);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource(jarFileResource);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource(PATH_DIR_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource(PATH_FILE_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource("file:///" + PATH_DIR_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(RESOURCES_PATH + PATH_DIR_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(RESOURCES_PATH + PATH_FILE_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource(NON_EXISTENT_RESOURCE);
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource(INVALID_RESOURCE);
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getUrlResource(null);
+        assertNull(theUrl);
+    }
+
+    /**
+     * Test get local file.
+     */
+    @Test
+    void testGetLocalFile() {
+        URL theUrl = ResourceUtils.getLocalFile(tmpDir.getAbsolutePath());
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(tmpEmptyFile.getAbsolutePath());
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(tmpUsedFile.getAbsolutePath());
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(jarDirResource);
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(jarFileResource);
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(PATH_DIR_RESOURCE);
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(PATH_FILE_RESOURCE);
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(RESOURCES_PATH + PATH_DIR_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(RESOURCES_PATH + PATH_FILE_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(NON_EXISTENT_RESOURCE);
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(INVALID_RESOURCE);
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile("file:///");
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile("file:///testdir/testfile.xml");
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getLocalFile(null);
+        assertNull(theUrl);
+    }
+
+    /**
+     * Test get resource as stream.
+     */
+    @Test
+    void testGetResourceAsStream() throws IOException {
+        verifyStream(tmpDir.getAbsolutePath());
+        verifyStream(tmpEmptyFile.getAbsolutePath());
+        verifyStream(tmpUsedFile.getAbsolutePath());
+        verifyStream(jarDirResource);
+        verifyStream(jarFileResource);
+        verifyStream(PATH_DIR_RESOURCE);
+        verifyStream(PATH_FILE_RESOURCE);
+        verifyStream(RESOURCES_PATH + PATH_DIR_RESOURCE);
+        verifyStream(RESOURCES_PATH + PATH_FILE_RESOURCE);
+        assertNull(ResourceUtils.getResourceAsStream(NON_EXISTENT_RESOURCE));
+        assertNull(ResourceUtils.getResourceAsStream(INVALID_RESOURCE));
+        assertNull(ResourceUtils.getResourceAsStream(null));
+        verifyStream("");
+    }
+
+    private void verifyStream(String path) throws IOException {
+        try (var theStream = ResourceUtils.getResourceAsStream(path)) {
+            assertNotNull(theStream);
+        }
+    }
+
+    /**
+     * Test get resource as string.
+     */
+    @Test
+    void testGetResourceAsString() {
+        String theString = ResourceUtils.getResourceAsString(tmpDir.getAbsolutePath());
+        assertNotNull(theString);
+
+        theString = ResourceUtils.getResourceAsString(tmpEmptyFile.getAbsolutePath());
+        assertEquals("", theString);
+
+        theString = ResourceUtils.getResourceAsString(tmpUsedFile.getAbsolutePath());
+        assertEquals("Bluebirds fly over the rainbow", theString);
+
+        theString = ResourceUtils.getResourceAsString(jarFileResource);
+        assertNotNull(theString);
+
+        theString = ResourceUtils.getResourceAsString(PATH_DIR_RESOURCE);
+        assertNotNull(theString);
+
+        theString = ResourceUtils.getResourceAsString(PATH_FILE_RESOURCE);
+        assertNotNull(theString);
+
+        theString = ResourceUtils.getResourceAsString(RESOURCES_PATH + PATH_DIR_RESOURCE);
+        assertNotNull(theString);
+
+        theString = ResourceUtils.getResourceAsString(RESOURCES_PATH + PATH_FILE_RESOURCE);
+        assertNotNull(theString);
+
+        theString = ResourceUtils.getResourceAsString(NON_EXISTENT_RESOURCE);
+        assertNull(theString);
+
+        theString = ResourceUtils.getResourceAsString(INVALID_RESOURCE);
+        assertNull(theString);
+
+        theString = ResourceUtils.getResourceAsString(null);
+        assertNull(theString);
+
+        theString = ResourceUtils.getResourceAsString("");
+
+        assertEquals("keystore-test\nlogback-test.xml\nMETA-INF\norg\ntestdir\nversion.txt\nwebapps\n", theString);
+
+    }
+
+    @Test
+    void testgetUrl4Resource() {
+        URL theUrl = ResourceUtils.getUrl4Resource(tmpDir.getAbsolutePath());
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(tmpEmptyFile.getAbsolutePath());
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(tmpUsedFile.getAbsolutePath());
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(jarDirResource);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(jarFileResource);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(PATH_DIR_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(PATH_FILE_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(RESOURCES_PATH + PATH_DIR_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(RESOURCES_PATH + PATH_FILE_RESOURCE);
+        assertNotNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(NON_EXISTENT_RESOURCE);
+        assertNull(theUrl);
+
+        theUrl = ResourceUtils.getUrl4Resource(INVALID_RESOURCE);
+        assertNull(theUrl);
+    }
+
+    @Test
+    void testGetFilePath4Resource() {
+        assertNull(ResourceUtils.getFilePath4Resource(null));
+        assertEquals("/something/else", ResourceUtils.getFilePath4Resource("/something/else"));
+        assertTrue(ResourceUtils.getFilePath4Resource("xml/example.xml").endsWith("xml/example.xml"));
+        assertTrue(ResourceUtils.getFilePath4Resource("com/google").contains("com/google"));
+    }
+
+    @Test
+    void testGetDirectoryContents() throws MalformedURLException {
+        assertTrue(ResourceUtils.getDirectoryContents(null).isEmpty());
+        assertTrue(ResourceUtils.getDirectoryContents("idontexist").isEmpty());
+        assertTrue(ResourceUtils.getDirectoryContents("logback-test.xml").isEmpty());
+
+        Set<String> resultD0 = ResourceUtils.getDirectoryContents("testdir");
+        assertEquals(1, resultD0.size());
+        assertEquals("testdir/testfile.xml", normalizePath(resultD0.iterator().next()));
+
+        Set<String> resultD1 = ResourceUtils.getDirectoryContents("org/onap/policy/common/utils");
+        assertFalse(resultD1.isEmpty());
+        assertEquals("org/onap/policy/common/utils/coder/", normalizePath(resultD1.iterator().next()));
+
+        Set<String> resultD2 = ResourceUtils.getDirectoryContents("org/onap/policy/common/utils/coder");
+        assertTrue(resultD2.size() >= 15);
+        assertEquals("org/onap/policy/common/utils/coder/CoderExceptionTest.class",
+                normalizePath(resultD2.iterator().next()));
+
+        Set<String> resultJ0 = ResourceUtils.getDirectoryContents("com");
+        assertTrue(resultJ0.contains("com/google/"));
+        assertEquals("com/google/", normalizePath(resultJ0.iterator().next()));
+
+        Set<String> resultJ1 = ResourceUtils.getDirectoryContents("com/google/gson");
+        assertTrue(resultJ1.size() > 1);
+        assertTrue(resultJ1.contains("com/google/gson/JsonElement.class"));
+
+        URL dummyUrl = new URL("http://even/worse");
+        assertTrue(ResourceUtils.getDirectoryContentsJar(dummyUrl, "nonexistantdirectory").isEmpty());
+
+    }
+
+    /**
+     * Normalizes a path name, replacing OS-specific separators with "/".
+     *
+     * @param pathName path name to be normalized
+     * @return the normalized path name
+     */
+    private String normalizePath(String pathName) {
+        return pathName.replace(File.separator, "/");
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/FeatureApiUtilsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/FeatureApiUtilsTest.java
new file mode 100644 (file)
index 0000000..3de9377
--- /dev/null
@@ -0,0 +1,83 @@
+/*-
+ * ============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.utils.services;
+
+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 java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Predicate;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class FeatureApiUtilsTest {
+    private static final String HANDLED = "handled";
+
+    private MyPred pred;
+    private List<String> tried;
+    private List<String> errors;
+
+    /**
+     * Initializes fields.
+     */
+    @BeforeEach
+    public void setUp() {
+        tried = new LinkedList<>();
+        errors = new LinkedList<>();
+        pred = new MyPred();
+    }
+
+    @Test
+    void testApplyFeatureTrue() {
+        assertTrue(FeatureApiUtils.apply(Arrays.asList("exceptT0", "falseT1", HANDLED, "falseT2", HANDLED), pred,
+            (str, ex) -> errors.add(str)));
+
+        assertEquals("[exceptT0, falseT1, handled]", tried.toString());
+        assertEquals("[exceptT0]", errors.toString());
+    }
+
+    @Test
+    void testApplyFeatureFalse() {
+        List<String> lst = Arrays.asList("falseF1", "exceptF2", "falseF3");
+
+        assertFalse(FeatureApiUtils.apply(lst, pred, (str, ex) -> errors.add(str)));
+        assertEquals(lst.toString(), tried.toString());
+        assertEquals("[exceptF2]", errors.toString());
+    }
+
+    private class MyPred implements Predicate<String> {
+
+        @Override
+        public boolean test(String data) {
+            tried.add(data);
+
+            if (data.startsWith("except")) {
+                throw new IllegalArgumentException("expected exception");
+            }
+
+            return data.equals(HANDLED);
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceImplTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceImplTest.java
new file mode 100644 (file)
index 0000000..1508009
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+ * ============LICENSE_START=======================================================
+ * utils
+ * ================================================================================
+ * 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.utils.services;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class OrderedServiceImplTest {
+
+    private static final int HIGH_PRIORITY_NUM = -1000;
+    private static final int LOW_PRIORITY_NUM = 1000;
+
+    private static GenericService highPrioService;
+    private static GenericService lowPrioService;
+
+    /**
+     * Saves the original state of the ordered service list to restore after each test.
+     */
+    @BeforeAll
+    public static void setup() {
+        List<GenericService> implementers = GenericService.providers.getList();
+        highPrioService = implementers.get(0);
+        lowPrioService = implementers.get(1);
+    }
+
+    /**
+     * Restores original state after each test.
+     */
+    @BeforeEach
+    public void resetOrder() {
+        highPrioService.setSequenceNumber(HIGH_PRIORITY_NUM);
+        lowPrioService.setSequenceNumber(LOW_PRIORITY_NUM);
+    }
+
+    /**
+     * Tests obtaining a list of service implementers.
+     */
+    @Test
+    void getListTest() {
+        List<GenericService> implementers = GenericService.providers.getList();
+        assertEquals(2, implementers.size());
+
+        assertEquals(highPrioService, implementers.get(0));
+        assertEquals(HIGH_PRIORITY_NUM, highPrioService.getSequenceNumber());
+
+        assertEquals(lowPrioService, implementers.get(1));
+        assertEquals(LOW_PRIORITY_NUM, lowPrioService.getSequenceNumber());
+    }
+
+    /**
+     * Tests inverting the priority of two services to ensure the list is rebuilt
+     * with the new order.
+     */
+    @Test
+    void rebuildListInvertedPriorityTest() {
+
+        List<GenericService> implementers = GenericService.providers.getList();
+        assertEquals(2, implementers.size());
+
+        assertEquals(highPrioService, implementers.get(0));
+        assertEquals(HIGH_PRIORITY_NUM, highPrioService.getSequenceNumber());
+
+        assertEquals(lowPrioService, implementers.get(1));
+        assertEquals(LOW_PRIORITY_NUM, lowPrioService.getSequenceNumber());
+
+        highPrioService.setSequenceNumber(LOW_PRIORITY_NUM);
+        lowPrioService.setSequenceNumber(HIGH_PRIORITY_NUM);
+
+        implementers = GenericService.providers.rebuildList();
+        assertEquals(2, implementers.size());
+
+        assertEquals(lowPrioService, implementers.get(0));
+        assertEquals(HIGH_PRIORITY_NUM, lowPrioService.getSequenceNumber());
+
+        assertEquals(highPrioService, implementers.get(1));
+        assertEquals(LOW_PRIORITY_NUM, highPrioService.getSequenceNumber());
+
+    }
+
+    /**
+     * Tests that the service list is ordered alphabetically by class names
+     * if the priorities are equivalent.
+     */
+    @Test
+    void rebuildListEqualPriorityTest() {
+
+        List<GenericService> implementers = GenericService.providers.getList();
+        assertEquals(2, implementers.size());
+
+        assertEquals(highPrioService, implementers.get(0));
+        assertEquals(HIGH_PRIORITY_NUM, highPrioService.getSequenceNumber());
+
+        assertEquals(lowPrioService, implementers.get(1));
+        assertEquals(LOW_PRIORITY_NUM, lowPrioService.getSequenceNumber());
+
+        highPrioService.setSequenceNumber(LOW_PRIORITY_NUM);
+        lowPrioService.setSequenceNumber(LOW_PRIORITY_NUM);
+
+        implementers = GenericService.providers.rebuildList();
+        assertEquals(2, implementers.size());
+
+        assertEquals(highPrioService, implementers.get(0));
+        assertEquals(LOW_PRIORITY_NUM, highPrioService.getSequenceNumber());
+
+        assertEquals(lowPrioService, implementers.get(1));
+        assertEquals(LOW_PRIORITY_NUM, lowPrioService.getSequenceNumber());
+
+    }
+
+    /**
+     * Test interface that extends OrderedService to allow changing the sequence number.
+     */
+    public static interface GenericService extends OrderedService {
+
+        /**
+         * Providers of the GenericService interface.
+         */
+        OrderedServiceImpl<GenericService> providers = new OrderedServiceImpl<>(GenericService.class);
+
+        /**
+         * Sets the sequence number of the service.
+         */
+        public void setSequenceNumber(int seqNum);
+
+    }
+
+    /**
+     * A high priority service class.
+     */
+    public static class HighPriorityService implements GenericService {
+
+        /**
+         * Defaults to a high priority.
+         */
+        private int seqNum = HIGH_PRIORITY_NUM;
+
+        /**
+         * {@inheritDoc}.
+         */
+        @Override
+        public int getSequenceNumber() {
+            return this.seqNum;
+        }
+
+        /**
+         * {@inheritDoc}.
+         */
+        @Override
+        public void setSequenceNumber(int seqNum) {
+            this.seqNum = seqNum;
+        }
+
+    }
+
+    /**
+     * A low priority service class.
+     */
+    public static class LowPriorityService implements GenericService {
+
+        /**
+         * Defaults to a low priority.
+         */
+        private int seqNum = LOW_PRIORITY_NUM;
+
+        /**
+         * {@inheritDoc}.
+         */
+        @Override
+        public int getSequenceNumber() {
+            return this.seqNum;
+        }
+
+        /**
+         * {@inheritDoc}.
+         */
+        @Override
+        public void setSequenceNumber(int seqNum) {
+            this.seqNum = seqNum;
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/OrderedServiceTest.java
new file mode 100644 (file)
index 0000000..adac7cb
--- /dev/null
@@ -0,0 +1,62 @@
+/*-
+ * ============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.utils.services;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class OrderedServiceTest {
+
+    @Test
+    void testGetSequenceNumber() {
+        // Anonymous class implementation for testing
+        OrderedService service = () -> 5;  // Returns 5 as the sequence number
+
+        // Test getSequenceNumber
+        assertEquals(5, service.getSequenceNumber(), "The sequence number should be 5");
+    }
+
+    @Test
+    void testGetName() {
+        // Anonymous class implementation for testing
+        OrderedService service = () -> 5;
+
+        // Test getName
+        assertEquals(service.getClass().getName(), service.getName(), "The name should match the class name");
+    }
+
+    @Test
+    void testGetNameWithCustomImplementation() {
+        // Custom implementation of OrderedService
+        class CustomOrderedService implements OrderedService {
+            @Override
+            public int getSequenceNumber() {
+                return 10;
+            }
+        }
+
+        OrderedService service = new CustomOrderedService();
+
+        // Test getName for custom implementation
+        assertEquals(service.getClass().getName(), service.getName(),
+            "The name should match the custom implementation class name");
+    }
+}
+
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerContainerTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerContainerTest.java
new file mode 100644 (file)
index 0000000..1bae46c
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * ============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.utils.services;
+
+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.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.capabilities.Startable;
+import org.onap.policy.common.utils.services.ServiceManager.RunnableWithEx;
+
+class ServiceManagerContainerTest {
+    private static final String MY_NAME = "my-name";
+    private static final String MY_ACTION = "my-action";
+    private static final String MY_OBJECT = "my-object";
+    private RunnableWithEx starter;
+    private RunnableWithEx stopper;
+    private Startable startObj;
+    private MyCont cont;
+
+    /**
+     * Set up.
+     */
+    @BeforeEach
+    public void setUp() {
+        starter = mock(RunnableWithEx.class);
+        stopper = mock(RunnableWithEx.class);
+        startObj = mock(Startable.class);
+
+        cont = new MyCont(MY_NAME);
+    }
+
+    @Test
+    void testServiceManagerContainer() throws Exception {
+        // use no-arg constructor
+        cont = new MyCont();
+        assertEquals("service manager", cont.getName());
+
+        cont.start();
+        verify(starter).run();
+    }
+
+    @Test
+    void test() throws Exception {
+        assertEquals(MY_NAME, cont.getName());
+
+        assertFalse(cont.isAlive());
+
+        cont.start();
+        assertTrue(cont.isAlive());
+        verify(starter).run();
+        verify(startObj).start();
+        verify(stopper, never()).run();
+        verify(startObj, never()).stop();
+        verify(startObj, never()).shutdown();
+
+        cont.stop();
+        assertFalse(cont.isAlive());
+        verify(starter).run();
+        verify(startObj).start();
+        verify(stopper).run();
+        verify(startObj).stop();
+        verify(startObj, never()).shutdown();
+    }
+
+    @Test
+    void testShutdown() throws Exception {
+        cont.start();
+        cont.shutdown();
+        assertFalse(cont.isAlive());
+        verify(stopper).run();
+        verify(startObj).stop();
+        verify(startObj, never()).shutdown();
+    }
+
+    private class MyCont extends ServiceManagerContainer {
+
+        public MyCont() {
+            addServices();
+        }
+
+        public MyCont(String name) {
+            super(name);
+            addServices();
+        }
+
+        private void addServices() {
+            addAction(MY_ACTION, starter, stopper);
+            addService(MY_OBJECT, startObj);
+        }
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerExceptionTest.java
new file mode 100644 (file)
index 0000000..8d6c8a2
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * ============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.utils.services;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.Test;
+
+class ServiceManagerExceptionTest {
+    private ServiceManagerException sme;
+
+    @Test
+    void testServiceManagerException() {
+        sme = new ServiceManagerException();
+        assertNull(sme.getMessage());
+        assertNull(sme.getCause());
+    }
+
+    @Test
+    void testServiceManagerExceptionString() {
+        sme = new ServiceManagerException("hello");
+        assertEquals("hello", sme.getMessage());
+        assertNull(sme.getCause());
+    }
+
+    @Test
+    void testServiceManagerExceptionThrowable() {
+        Throwable thrown = new Throwable("expected exception");
+        sme = new ServiceManagerException(thrown);
+        assertNotNull(sme.getMessage());
+        assertSame(thrown, sme.getCause());
+    }
+
+    @Test
+    void testServiceManagerExceptionStringThrowable() {
+        Throwable thrown = new Throwable("another expected exception");
+        sme = new ServiceManagerException("world", thrown);
+        assertEquals("world", sme.getMessage());
+        assertSame(thrown, sme.getCause());
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/services/ServiceManagerTest.java
new file mode 100644 (file)
index 0000000..dc55558
--- /dev/null
@@ -0,0 +1,296 @@
+/*
+ * ============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.utils.services;
+
+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.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.onap.policy.common.capabilities.Startable;
+import org.onap.policy.common.utils.services.ServiceManager.RunnableWithEx;
+
+class ServiceManagerTest {
+    private static final String MY_NAME = "my-name";
+    private static final String ALREADY_RUNNING = MY_NAME + " is already running";
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+
+    private ServiceManager svcmgr;
+
+    /**
+     * Initializes {@link #svcmgr}.
+     */
+    @BeforeEach
+    public void setUp() {
+        svcmgr = new ServiceManager(MY_NAME);
+    }
+
+    @Test
+    void testServiceName() {
+        assertEquals("service manager", new ServiceManager().getName());
+    }
+
+    @Test
+    void testGetName() {
+        assertEquals(MY_NAME, svcmgr.getName());
+    }
+
+    @Test
+    void testAddAction() throws Exception {
+        RunnableWithEx start1 = mock(RunnableWithEx.class);
+        RunnableWithEx stop1 = mock(RunnableWithEx.class);
+        svcmgr.addAction("first action", start1, stop1);
+
+        RunnableWithEx start2 = mock(RunnableWithEx.class);
+        RunnableWithEx stop2 = mock(RunnableWithEx.class);
+        svcmgr.addAction("second action", start2, stop2);
+
+        svcmgr.start();
+        verify(start1).run();
+        verify(start2).run();
+        verify(stop1, never()).run();
+        verify(stop2, never()).run();
+
+        // cannot add while running
+        assertThatIllegalStateException().isThrownBy(() -> svcmgr.addAction("fail action", start1, stop1))
+                        .withMessage(ALREADY_RUNNING + "; cannot add fail action");
+
+        svcmgr.stop();
+        verify(start1).run();
+        verify(start2).run();
+        verify(stop1).run();
+        verify(stop2).run();
+    }
+
+    @Test
+    void testAddStartable() {
+        Startable start1 = mock(Startable.class);
+        svcmgr.addService("first startable", start1);
+
+        Startable start2 = mock(Startable.class);
+        svcmgr.addService("second startable", start2);
+
+        svcmgr.start();
+        verify(start1).start();
+        verify(start1, never()).stop();
+        verify(start2).start();
+        verify(start2, never()).stop();
+
+        // cannot add while running
+        assertThatIllegalStateException().isThrownBy(() -> svcmgr.addService("fail startable", start1))
+                        .withMessage(ALREADY_RUNNING + "; cannot add fail startable");
+
+        svcmgr.stop();
+        verify(start1).start();
+        verify(start1).stop();
+        verify(start2).start();
+        verify(start2).stop();
+    }
+
+    @Test
+    void testStart() {
+        Startable start1 = mock(Startable.class);
+        svcmgr.addService("test start", start1);
+
+        assertTrue(svcmgr.start());
+
+        assertTrue(svcmgr.isAlive());
+        verify(start1).start();
+        verify(start1, never()).stop();
+
+        // cannot re-start
+        assertThatIllegalStateException().isThrownBy(() -> svcmgr.start()).withMessage(ALREADY_RUNNING);
+
+        // verify that it didn't try to start the service again
+        verify(start1).start();
+
+        // still running
+        assertTrue(svcmgr.isAlive());
+    }
+
+    @Test
+    void testStart_Ex() {
+        Startable start1 = mock(Startable.class);
+        svcmgr.addService("test start ex", start1);
+
+        Startable start2 = mock(Startable.class);
+        svcmgr.addService("second test start ex", start2);
+
+        // this one will throw an exception
+        Startable start3 = mock(Startable.class);
+        RuntimeException exception = new RuntimeException(EXPECTED_EXCEPTION);
+        when(start3.start()).thenThrow(exception);
+        svcmgr.addService("third test start ex", start3);
+
+        Startable start4 = mock(Startable.class);
+        svcmgr.addService("fourth test start ex", start4);
+
+        Startable start5 = mock(Startable.class);
+        svcmgr.addService("fifth test start ex", start5);
+
+        assertThatThrownBy(() -> svcmgr.start()).isInstanceOf(ServiceManagerException.class).hasCause(exception);
+
+        assertFalse(svcmgr.isAlive());
+
+        verify(start1).start();
+        verify(start2).start();
+        verify(start3).start();
+        verify(start4, never()).start();
+        verify(start5, never()).start();
+
+        verify(start1).stop();
+        verify(start2).stop();
+        verify(start3, never()).stop();
+        verify(start4, never()).stop();
+        verify(start5, never()).stop();
+    }
+
+    @Test
+    void testStart_RewindEx() {
+        Startable start1 = mock(Startable.class);
+        svcmgr.addService("test start rewind", start1);
+
+        // this one will throw an exception during rewind
+        Startable start2 = mock(Startable.class);
+        RuntimeException exception2 = new RuntimeException(EXPECTED_EXCEPTION);
+        when(start2.stop()).thenThrow(exception2);
+        svcmgr.addService("second test start rewind", start2);
+
+        // this one will throw an exception
+        Startable start3 = mock(Startable.class);
+        RuntimeException exception = new RuntimeException(EXPECTED_EXCEPTION);
+        when(start3.start()).thenThrow(exception);
+        svcmgr.addService("third test start rewind", start3);
+
+        Startable start4 = mock(Startable.class);
+        svcmgr.addService("fourth test start rewind", start4);
+
+        Startable start5 = mock(Startable.class);
+        svcmgr.addService("fifth test start rewind", start5);
+
+        assertThatThrownBy(() -> svcmgr.start()).isInstanceOf(ServiceManagerException.class).hasCause(exception);
+
+        assertFalse(svcmgr.isAlive());
+    }
+
+    @Test
+    void testStop() {
+        Startable start1 = mock(Startable.class);
+        svcmgr.addService("first stop", start1);
+
+        // cannot stop until started
+        assertThatIllegalStateException().isThrownBy(() -> svcmgr.stop()).withMessage(MY_NAME + " is not running");
+
+        // verify that it didn't try to stop the service
+        verify(start1, never()).stop();
+
+        // start it
+        svcmgr.start();
+
+        assertTrue(svcmgr.stop());
+
+        assertFalse(svcmgr.isAlive());
+        verify(start1).stop();
+    }
+
+    @Test
+    void testStop_Ex() throws Exception {
+        RunnableWithEx start1 = mock(RunnableWithEx.class);
+        RunnableWithEx stop1 = mock(RunnableWithEx.class);
+        svcmgr.addAction("first stop ex", start1, stop1);
+
+        Startable start2 = mock(Startable.class);
+        svcmgr.addService("second stop ex", start2);
+
+        svcmgr.start();
+        verify(start1).run();
+        verify(stop1, never()).run();
+        verify(start2).start();
+        verify(start2, never()).stop();
+
+        svcmgr.stop();
+        verify(start1).run();
+        verify(stop1).run();
+        verify(start2).start();
+        verify(start2).stop();
+
+        assertFalse(svcmgr.isAlive());
+    }
+
+    @Test
+    void testShutdown() {
+        Startable start1 = mock(Startable.class);
+        svcmgr.addService("first stop", start1);
+
+        // cannot stop until started
+        assertThatIllegalStateException().isThrownBy(() -> svcmgr.shutdown()).withMessage(MY_NAME + " is not running");
+
+        // verify that it didn't try to stop the service
+        verify(start1, never()).stop();
+
+        // start it
+        svcmgr.start();
+
+        svcmgr.shutdown();
+
+        assertFalse(svcmgr.isAlive());
+        verify(start1).stop();
+    }
+
+    @Test
+    void testRewind() {
+        RunnableWithEx starter = mock(RunnableWithEx.class);
+        LinkedList<String> lst = new LinkedList<>();
+
+        svcmgr.addAction("first rewind", starter, () -> lst.add("rewind1"));
+        svcmgr.addAction("second rewind", starter, () -> lst.add("rewind2"));
+
+        // this one will throw an exception during rewind
+        RuntimeException exception = new RuntimeException(EXPECTED_EXCEPTION);
+        svcmgr.addAction("third rewind", starter, () -> {
+            lst.add("rewind3");
+            throw exception;
+        });
+
+        svcmgr.addAction("fourth rewind", starter, () -> lst.add("rewind4"));
+        svcmgr.addAction("fifth rewind", starter, () -> lst.add("rewind5"));
+
+        svcmgr.start();
+
+        assertThatThrownBy(() -> svcmgr.stop()).isInstanceOf(ServiceManagerException.class).hasCause(exception);
+
+        assertFalse(svcmgr.isAlive());
+
+        // all of them should have been stopped, in reverse order
+        assertEquals(Arrays.asList("rewind5", "rewind4", "rewind3", "rewind2", "rewind1").toString(), lst.toString());
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/ConstructionErrorTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/ConstructionErrorTest.java
new file mode 100644 (file)
index 0000000..95bba10
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Common Utils-Test
+ * ================================================================================
+ * 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.utils.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class ConstructionErrorTest extends ErrorsTester {
+
+    @Test
+    void test() {
+        assertEquals(4, testAllError(ConstructionError.class));
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/ErrorsTesterTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/ErrorsTesterTest.java
new file mode 100644 (file)
index 0000000..b6d91f0
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Common Utils-Test
+ * ================================================================================
+ * 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.utils.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class ErrorsTesterTest {
+
+    @Test
+    void test() {
+        assertEquals(2, new ErrorsTester().testAllError(SimpleError.class));
+        assertEquals(5, new ErrorsTester().testAllError(StaticError.class));
+    }
+
+    /**
+     * Used to test a simple success case.
+     */
+    public static class SimpleError extends Error {
+        private static final long serialVersionUID = 1L;
+
+        public SimpleError() {
+            super();
+        }
+
+        public SimpleError(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * Used to test the exhaustive success case.
+     */
+    public static class StaticError extends Error {
+        private static final long serialVersionUID = 1L;
+
+        public StaticError() {
+            super();
+        }
+
+        public StaticError(String message) {
+            super(message);
+        }
+
+        public StaticError(Throwable cause) {
+            super(cause);
+        }
+
+        public StaticError(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public StaticError(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+            super(message, cause, enableSuppression, writableStackTrace);
+        }
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/ExceptionsTesterTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/ExceptionsTesterTest.java
new file mode 100644 (file)
index 0000000..81a8503
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Common Utils-Test
+ * ================================================================================
+ * 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.utils.test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class ExceptionsTesterTest {
+
+    @Test
+    void test() {
+        assertEquals(2, new ExceptionsTester().test(SimpleException.class));
+        assertEquals(8, new ExceptionsTester().test(StaticException.class));
+    }
+
+    @Test
+    void testNoConstructorsException() {
+        ExceptionsTester tester = new ExceptionsTester();
+        assertThatThrownBy(() -> tester.test(NoConstructorsException.class))
+            .isInstanceOf(AssertionError.class);
+    }
+
+    /**
+     * Used to test a failure case - this has no standard constructors. The only constructor it has
+     * takes an "int", thus it is not one of the standard constructors.
+     */
+    public static class NoConstructorsException extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public NoConstructorsException(int value) {
+            super();
+        }
+    }
+
+    /**
+     * Used to test a simple success case.
+     */
+    public static class SimpleException extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public SimpleException() {
+            super();
+        }
+
+        public SimpleException(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * Used to test the exhaustive success case.
+     */
+    public static class StaticException extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public StaticException() {
+            super();
+        }
+
+        public StaticException(String message) {
+            super(message);
+        }
+
+        public StaticException(Throwable cause) {
+            super(cause);
+        }
+
+        public StaticException(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public StaticException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+            super(message, cause, enableSuppression, writableStackTrace);
+        }
+
+        // same as above, but with Exceptions substituted for Throwables
+
+        public StaticException(Exception cause) {
+            super(cause);
+        }
+
+        public StaticException(String message, Exception cause) {
+            super(message, cause);
+        }
+
+        public StaticException(String message, Exception cause, boolean enableSuppression, boolean writableStackTrace) {
+            super(message, cause, enableSuppression, writableStackTrace);
+        }
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/ThrowablesTesterTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/ThrowablesTesterTest.java
new file mode 100644 (file)
index 0000000..7775ef3
--- /dev/null
@@ -0,0 +1,230 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Common Utils-Test
+ * ================================================================================
+ * 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.utils.test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class ThrowablesTesterTest {
+
+    @Test
+    void test() {
+        assertEquals(2, new ThrowablesTester().testAllThrowable(SimpleThrowable.class));
+        assertEquals(5, new ThrowablesTester().testAllThrowable(StaticThrowable.class));
+    }
+
+    @Test
+    void testNoConstructorsThrowable() {
+        // this will not throw an error, but it should return 0, as there are
+        // no matching constructors
+        assertEquals(0, new ThrowablesTester().testAllThrowable(NoConstructorsThrowable.class));
+    }
+
+    @Test
+    void testIgnoreMessageThrowable() {
+        ThrowablesTester tester = new ThrowablesTester();
+        assertThatThrownBy(() -> tester.testAllThrowable(IgnoreMessageThrowable.class))
+            .isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void testIgnoreCauseThrowable() {
+        ThrowablesTester tester = new ThrowablesTester();
+        assertThatThrownBy(() -> tester.testAllThrowable(IgnoreCauseThrowable.class))
+            .isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void testAlwaysSuppressThrowable() {
+        ThrowablesTester tester = new ThrowablesTester();
+        assertThatThrownBy(() -> tester.testAllThrowable(AlwaysSuppressThrowable.class))
+            .isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void testNeverSuppressThrowable() {
+        ThrowablesTester tester = new ThrowablesTester();
+        assertThatThrownBy(() -> tester.testAllThrowable(NeverSuppressThrowable.class))
+            .isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void testAlwaysWritableThrowable() {
+        ThrowablesTester tester = new ThrowablesTester();
+        assertThatThrownBy(() -> tester.testAllThrowable(AlwaysWritableThrowable.class))
+            .isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void testNeverWritableThrowable() {
+        ThrowablesTester tester = new ThrowablesTester();
+        assertThatThrownBy(() -> tester.testAllThrowable(NeverWritableThrowable.class))
+            .isInstanceOf(AssertionError.class);
+    }
+
+    @Test
+    void testThrowInstantiationException() {
+        ThrowablesTester tester = new ThrowablesTester();
+        assertThatThrownBy(() -> tester.testAllThrowable(ThrowInstantiationThrowable.class))
+            .isInstanceOf(AssertionError.class);
+    }
+
+    /**
+     * Used to test a failure case - message text is ignored.
+     */
+    public static class IgnoreMessageThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public IgnoreMessageThrowable(String message) {
+            super("bogus");
+        }
+    }
+
+    /**
+     * Used to test a failure case - cause is ignored.
+     */
+    public static class IgnoreCauseThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public IgnoreCauseThrowable(Throwable cause) {
+            super(new Throwable("another cause"));
+        }
+    }
+
+    /**
+     * Used to test a failure case - this has no standard constructors. The only constructor it has
+     * takes an "int", thus it is not one of the standard constructors.
+     */
+    public static class NoConstructorsThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public NoConstructorsThrowable(int value) {
+            super();
+        }
+    }
+
+    /**
+     * Used to test a failure case - always suppresses.
+     */
+    public static class AlwaysSuppressThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public AlwaysSuppressThrowable(String message, Throwable cause, boolean enableSuppression,
+                boolean writableStackTrace) {
+            super(message, cause, true, writableStackTrace);
+        }
+    }
+
+    /**
+     * Used to test a failure case - never suppresses.
+     */
+    public static class NeverSuppressThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public NeverSuppressThrowable(String message, Throwable cause, boolean enableSuppression,
+                boolean writableStackTrace) {
+            super(message, cause, false, writableStackTrace);
+        }
+    }
+
+    /**
+     * Used to test a failure case - always allows stack writes.
+     */
+    public static class AlwaysWritableThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public AlwaysWritableThrowable(String message, Throwable cause, boolean enableSuppression,
+                boolean writableStackTrace) {
+            super(message, cause, enableSuppression, true);
+        }
+    }
+
+    /**
+     * Used to test a failure case - never allows stack writes.
+     */
+    public static class NeverWritableThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public NeverWritableThrowable(String message, Throwable cause, boolean enableSuppression,
+                boolean writableStackTrace) {
+            super(message, cause, enableSuppression, false);
+        }
+    }
+
+    /**
+     * Used to test a failure case - throws InstantiationException when constructed.
+     */
+    public static class ThrowInstantiationThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public ThrowInstantiationThrowable(String message, Throwable cause, boolean enableSuppression,
+                boolean writableStackTrace) throws InstantiationException {
+
+            throw new InstantiationException(ThrowablesTester.EXPECTED_EXCEPTION_MSG);
+        }
+    }
+
+    /**
+     * Used to test a simple success case.
+     */
+    public static class SimpleThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public SimpleThrowable() {
+            super();
+        }
+
+        public SimpleThrowable(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * Used to test the exhaustive success case.
+     */
+    public static class StaticThrowable extends Throwable {
+        private static final long serialVersionUID = 1L;
+
+        public StaticThrowable() {
+            super();
+        }
+
+        public StaticThrowable(String message) {
+            super(message);
+        }
+
+        public StaticThrowable(Throwable cause) {
+            super(cause);
+        }
+
+        public StaticThrowable(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public StaticThrowable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+            super(message, cause, enableSuppression, writableStackTrace);
+        }
+    }
+
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/test/log/logback/ExtractAppenderTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/test/log/logback/ExtractAppenderTest.java
new file mode 100644 (file)
index 0000000..9367209
--- /dev/null
@@ -0,0 +1,473 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Common Utils-Test
+ * ================================================================================
+ * 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.utils.test.log.logback;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+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.slf4j.LoggerFactory;
+
+class ExtractAppenderTest {
+    private static final String ABC_DIGIT = "abc[0-9]";
+    private static final String ABC_DIGIT1 = "abc[1-9]";
+    private static final String DEF_DIGIT = "def[0-9]";
+    private static final String HELLO = "hello";
+    private static final String HELLO_ABC = "hello abc";
+    private static final String HELLO_ABC1_WORLD = "hello abc1 world";
+    private static final String HELLO_ABC3 = "hello abc3";
+    private static final String WORLD = "world";
+    private static final String WORLD_ABC = "world abc";
+    private static final String WORLD_GHI2_WORLD = "world ghi2 world";
+
+    /**
+     * Milliseconds to wait for a thread to terminate.
+     */
+    private static final long THREAD_WAIT_MS = 5000L;
+
+    private static Logger logger;
+
+    private List<Thread> threads;
+
+    @BeforeAll
+    public static void setUpBeforeClass() {
+        logger = (Logger) LoggerFactory.getLogger(ExtractAppenderTest.class);
+        logger.setLevel(Level.INFO);
+    }
+
+    @BeforeEach
+    public void setUp() {
+        threads = new LinkedList<>();
+    }
+
+    /**
+     * Tear down all appenders and threads.
+     */
+    @AfterEach
+    public void tearDown() throws Exception {
+        logger.detachAndStopAllAppenders();
+
+        for (Thread p : threads) {
+            p.interrupt();
+            p.join(THREAD_WAIT_MS);
+        }
+    }
+
+    @Test
+    void testExtractAppender() {
+        AtomicInteger count = new AtomicInteger(0);
+
+        ExtractAppender appender = new ExtractAppender() {
+            @Override
+            protected void append(ILoggingEvent event) {
+                count.incrementAndGet();
+                super.append(event);
+            }
+        };
+
+        addAppender(appender);
+
+        logger.info(HELLO);
+        logger.info(WORLD);
+
+        // "append" should always be called
+        assertEquals(2, count.get());
+
+        // appender with no patterns - everything should match
+        assertEquals(strList(HELLO, WORLD), appender.getExtracted());
+
+        // add a pattern and verify match
+        appender.setPattern(ABC_DIGIT);
+        logger.info("hello abc1");
+
+        // this should not match
+        logger.info("hello def2");
+
+        assertEquals(4, count.get());
+        assertEquals(strList(HELLO, WORLD, "abc1"), appender.getExtracted());
+    }
+
+    @Test
+    void testExtractAppenderStringArray() {
+        AtomicInteger count = new AtomicInteger(0);
+
+        ExtractAppender appender = new ExtractAppender(ABC_DIGIT, DEF_DIGIT) {
+            @Override
+            protected void append(ILoggingEvent event) {
+                count.incrementAndGet();
+                super.append(event);
+            }
+        };
+
+        addAppender(appender);
+
+        logger.info(HELLO_ABC1_WORLD);
+        logger.info(WORLD_GHI2_WORLD); // no match
+        logger.info("world def3 world");
+        logger.info("hello abc4");
+        logger.info("abc5 world");
+        logger.info("hello def6");
+        logger.info("ghi7 world"); // no match
+        logger.info("def8 world");
+
+        // "append" should always be called
+        assertEquals(8, count.get());
+
+        assertEquals(strList("abc1", "def3", "abc4", "abc5", "def6", "def8"), appender.getExtracted());
+
+        appender.setPattern("ghi[0-9]");
+        logger.info("hello abc9");
+        logger.info("hello ghi9");
+
+        // this should not match
+        logger.info("hello xyz");
+
+        assertEquals(11, count.get());
+        assertEquals(strList("abc1", "def3", "abc4", "abc5", "def6", "def8", "abc9", "ghi9"), appender.getExtracted());
+    }
+
+    @Test
+    void testExtractAppenderQueueStringArray() {
+        // no. of matches allowed in the list
+        int nallowed = 3;
+
+        AtomicInteger count = new AtomicInteger(0);
+
+        LinkedList<String> queue = new LinkedList<String>() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public boolean offer(String element) {
+                if (count.incrementAndGet() <= nallowed) {
+                    return super.offer(element);
+
+                } else {
+                    return false;
+                }
+            }
+        };
+
+        ExtractAppender appender = new ExtractAppender(queue, ABC_DIGIT);
+        addAppender(appender);
+
+        // these shouldn't match
+        for (int x = 0; x < 10; ++x) {
+            logger.info("xyz");
+        }
+
+        int nmatches = 10;
+
+        LinkedList<String> expected = new LinkedList<>();
+
+        for (int x = 0; x < nmatches; ++x) {
+            String msg = "abc" + x;
+            logger.info("{} world", msg);
+
+            if (x < nallowed) {
+                expected.add(msg);
+            }
+        }
+
+        // "offer" should always be called for a match
+        assertEquals(nmatches, count.get());
+
+        assertEquals(expected, appender.getExtracted());
+    }
+
+    @Test
+    void testAppendILoggingEvent_NoPatterns() {
+        ExtractAppender appender = makeAppender();
+
+        logger.info(HELLO);
+        logger.info(WORLD);
+
+        assertEquals(strList(HELLO, WORLD), appender.getExtracted());
+    }
+
+    @Test
+    void testAppendILoggingEvent_Formatted() {
+        ExtractAppender appender = makeAppender();
+
+        logger.info("hello {} world{}", "there", "!");
+
+        assertEquals(strList("hello there world!"), appender.getExtracted());
+    }
+
+    @Test
+    void testAppendILoggingEvent_MatchFirstPattern() {
+        ExtractAppender appender = makeAppender(ABC_DIGIT, DEF_DIGIT);
+
+        logger.info("hello abc1");
+        logger.info("world xyz2");
+
+        assertEquals(strList("abc1"), appender.getExtracted());
+    }
+
+    @Test
+    void testAppendILoggingEvent_MatchLastPattern() {
+        ExtractAppender appender = makeAppender(ABC_DIGIT, DEF_DIGIT);
+
+        logger.info("hello def1");
+        logger.info("world xyz2");
+
+        assertEquals(strList("def1"), appender.getExtracted());
+    }
+
+    @Test
+    void testAppendILoggingEvent_Group1() {
+        ExtractAppender appender = makeAppender("hello (abc)|(xyz)", DEF_DIGIT);
+
+        logger.info("hello abc, world!");
+        logger.info(WORLD_ABC);
+
+        assertEquals(strList("abc"), appender.getExtracted());
+    }
+
+    @Test
+    void testAppendILoggingEvent_Group3() {
+        ExtractAppender appender = makeAppender("hello (abc)|(pdq)|(xyz)", DEF_DIGIT);
+
+        logger.info("say hello xyz, world!");
+        logger.info(WORLD_ABC);
+
+        assertEquals(strList("xyz"), appender.getExtracted());
+    }
+
+    @Test
+    void testAppendILoggingEvent_NoGroup() {
+        ExtractAppender appender = makeAppender(HELLO_ABC);
+
+        logger.info("say hello abc, world!");
+        logger.info(WORLD_ABC);
+
+        assertEquals(strList(HELLO_ABC), appender.getExtracted());
+    }
+
+    @Test
+    void testGetExtracted() {
+        ExtractAppender appender = makeAppender(ABC_DIGIT1);
+
+        logger.info(HELLO_ABC1_WORLD);
+        logger.info(WORLD_GHI2_WORLD); // no match
+        logger.info(HELLO_ABC3);
+
+        List<String> oldlst = appender.getExtracted();
+        assertEquals(strList("abc1", "abc3"), oldlst);
+        assertEquals(oldlst, appender.getExtracted());
+
+        logger.info("abc9");
+        assertEquals(strList("abc1", "abc3", "abc9"), appender.getExtracted());
+    }
+
+    @Test
+    void testClearExtractions() {
+        final ExtractAppender appender = makeAppender(ABC_DIGIT1);
+
+        logger.info(HELLO_ABC1_WORLD);
+        logger.info(WORLD_GHI2_WORLD);
+        logger.info(HELLO_ABC3);
+
+        assertEquals(strList("abc1", "abc3"), appender.getExtracted());
+
+        appender.clearExtractions();
+
+        // list should be empty now
+        assertEquals(strList(), appender.getExtracted());
+
+        logger.info("hello abc4 world");
+        logger.info("world ghi5 world");
+        logger.info("hello abc6");
+
+        // list should only contain the new items
+        assertEquals(strList("abc4", "abc6"), appender.getExtracted());
+    }
+
+    @Test
+    void testSetPattern() {
+        final ExtractAppender appender = makeAppender(ABC_DIGIT1);
+
+        logger.info(HELLO_ABC1_WORLD);
+        logger.info(WORLD_GHI2_WORLD); // no match
+        logger.info(HELLO_ABC3);
+
+        assertEquals(strList("abc1", "abc3"), appender.getExtracted());
+
+        appender.setPattern("ghi[0-9]");
+
+        logger.info("world ghi4 world"); // this should match now
+        logger.info("hello abc5"); // this should still match
+        logger.info("hello xyz5"); // no match
+
+        assertEquals(strList("abc1", "abc3", "ghi4", "abc5"), appender.getExtracted());
+    }
+
+    /**
+     * Launches threads doing everything in parallel to ensure nothing crashes.
+     */
+    @Test
+    void test_MultiThreaded() throws Exception {
+        // when to stop
+        long tend = System.currentTimeMillis() + 250;
+
+        // maximum number of items allowed in the extraction list
+        int maxItems = 10;
+
+        // this will be set if one of the threads generates an error
+        AtomicBoolean err = new AtomicBoolean(false);
+
+        // extracted messages go here - this is a finite-length queue since
+        // we don't know how many messages may actually be logged
+        LinkedList<String> queue = new LinkedList<String>() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public boolean offer(String element) {
+                if (size() < maxItems) {
+                    return super.offer(element);
+                } else {
+                    return false;
+                }
+            }
+        };
+
+        ExtractAppender app = new ExtractAppender(queue, ABC_DIGIT1);
+        addAppender(app);
+
+        // create some threads to add another pattern
+        addThread(tend, err, xtxt -> app.setPattern(DEF_DIGIT));
+
+        // create some threads to log "abc" messages
+        addThread(tend, err, xtxt -> logger.info("{}{}world!", HELLO_ABC, xtxt));
+
+        // create some threads to log "def" messages
+        addThread(tend, err, xtxt -> logger.info("hello def{}world!", xtxt));
+
+        // create some threads to get extractions
+        addThread(tend, err, xtxt -> app.getExtracted());
+
+        /*
+         * Finally ready to start.
+         */
+
+        // start all of the threads
+        for (Thread t : threads) {
+            t.setDaemon(true);
+            t.start();
+        }
+
+        // wait for each thread to stop
+        for (Thread t : threads) {
+            t.join(THREAD_WAIT_MS);
+            assertFalse(t.isAlive());
+        }
+
+        // ensure none of the threads threw an exception
+        assertFalse(err.get());
+    }
+
+    /**
+     * Adds multiple threads to perform some function repeatedly until the given time is reached.
+     *
+     * @param tend time, in milliseconds, when the test should terminate
+     * @param haderr this will be set to {@code true} if the function throws an exception other than
+     *        an InterruptedException
+     * @param func function to be repeatedly invoked
+     */
+    private void addThread(long tend, AtomicBoolean haderr, VoidFunction func) {
+        // number of threads of each type to create
+        int neach = 3;
+
+        for (int x = 0; x < neach; ++x) {
+            String xtxt = String.valueOf(x);
+
+            threads.add(new Thread() {
+                @Override
+                public void run() {
+                    try {
+                        while (System.currentTimeMillis() < tend) {
+                            func.apply(xtxt);
+                        }
+
+                    } catch (InterruptedException ex) {
+                        Thread.currentThread().interrupt();
+
+                    } catch (Exception ex) {
+                        haderr.set(true);
+                    }
+                }
+            });
+
+        }
+    }
+
+    /**
+     * Makes an appender that recognizes the given set of strings.
+     *
+     * @param strings regular expressions to be matched
+     * @return a new appender
+     */
+    private ExtractAppender makeAppender(String... strings) {
+        ExtractAppender appender = new ExtractAppender(strings);
+
+        addAppender(appender);
+
+        return appender;
+    }
+
+    /**
+     * Adds an appender to the logger.
+     *
+     * @param app appender to be added
+     */
+    private void addAppender(ExtractAppender app) {
+        app.setContext(logger.getLoggerContext());
+        app.start();
+
+        logger.addAppender(app);
+    }
+
+    /**
+     * Converts an array of strings into a list of strings.
+     *
+     * @param strings array of strings
+     * @return a list of the strings
+     */
+    private List<String> strList(String... strings) {
+        return Arrays.asList(strings);
+    }
+
+    @FunctionalInterface
+    public interface VoidFunction {
+        public void apply(String text) throws InterruptedException;
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/validation/AssertionsTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/validation/AssertionsTest.java
new file mode 100644 (file)
index 0000000..b633425
--- /dev/null
@@ -0,0 +1,95 @@
+/*-
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2016-2018 Ericsson. All rights reserved.
+ *  Modifications 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.utils.validation;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * The Class ResourceUtilsTest.
+ *
+ * @author Liam Fallon (liam.fallon@ericsson.com)
+ */
+class AssertionsTest {
+    private static final String HELLO = "Hello";
+    private static final String IT_IS_OK = "it is OK";
+    private static final String IT_IS_NULL = "it is null";
+    private static final String IT_IS_TRUE = "it is true";
+    private static final String IT_IS_FALSE = "it is false";
+
+    @Test
+    void testAssertions() {
+        Assertions.argumentNotFalse(true, IT_IS_TRUE);
+
+        assertThatIllegalArgumentException().isThrownBy(() -> Assertions.argumentNotFalse(false, IT_IS_FALSE))
+                        .withMessage(IT_IS_FALSE);
+
+
+        Assertions.argumentOfClassNotFalse(true, ArithmeticException.class, IT_IS_TRUE);
+
+        assertThatIllegalArgumentException().isThrownBy(
+            () -> Assertions.argumentOfClassNotFalse(false, ArithmeticException.class, IT_IS_FALSE))
+            .withMessage(IT_IS_FALSE);
+
+
+        Assertions.argumentNotNull(HELLO, IT_IS_OK);
+
+        assertThatIllegalArgumentException().isThrownBy(() -> Assertions.argumentNotNull(null, IT_IS_NULL))
+                        .withMessage(IT_IS_NULL);
+
+
+        Assertions.argumentOfClassNotNull(true, ArithmeticException.class, IT_IS_OK);
+
+        assertThatIllegalArgumentException().isThrownBy(
+            () -> Assertions.argumentOfClassNotNull(null, ArithmeticException.class, IT_IS_NULL))
+            .withMessage(IT_IS_NULL);
+
+
+        Assertions.assignableFrom(java.util.TreeMap.class, java.util.Map.class);
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> Assertions.assignableFrom(java.util.Map.class, java.util.TreeMap.class))
+                        .withMessage("java.util.Map is not an instance of java.util.TreeMap");
+
+
+        Assertions.instanceOf(HELLO, String.class);
+
+        assertThatIllegalArgumentException().isThrownBy(() -> Assertions.instanceOf(100, String.class))
+                        .withMessage("java.lang.Integer is not an instance of java.lang.String");
+
+
+        Assertions.validateStringParameter("name", "MyName", "^M.*e$");
+
+        assertThatIllegalArgumentException()
+            .isThrownBy(() -> Assertions.validateStringParameter("name", "MyName", "^M.*f$"))
+            .withMessage("parameter \"name\": value \"MyName\", does not match regular expression \"^M.*f$\"");
+
+
+        assertNull(Assertions.getStringParameterValidationMessage("Greeting", HELLO, "^H.*o$"));
+        assertEquals("parameter Greeting with value Hello does not match regular expression Goodbye",
+                        Assertions.getStringParameterValidationMessage("Greeting", HELLO, "Goodbye"));
+    }
+}
diff --git a/policy-common/src/test/java/org/onap/policy/common/utils/validation/VersionTest.java b/policy-common/src/test/java/org/onap/policy/common/utils/validation/VersionTest.java
new file mode 100644 (file)
index 0000000..1902312
--- /dev/null
@@ -0,0 +1,155 @@
+/*-
+ * ============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.utils.validation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class VersionTest {
+    private static final String TYPE = "my-type";
+    private static final String NAME = "my-name";
+
+    private static final int MAJOR = 10;
+    private static final int MINOR = 2;
+    private static final int PATCH = 3;
+
+    private Version vers;
+
+    @BeforeEach
+    public void setUp() {
+        vers = new Version(MAJOR, MINOR, PATCH);
+    }
+
+    @Test
+    void testHashCode() {
+        int hash = vers.hashCode();
+        int hash2 = new Version(MAJOR, MINOR, PATCH + 1).hashCode();
+        assertNotEquals(hash, hash2);
+    }
+
+    @Test
+    void testConstructor() {
+        Version versionTest = new Version("1.0.2");
+        assertEquals(1, versionTest.getMajor());
+        assertEquals(0, versionTest.getMinor());
+        assertEquals(2, versionTest.getPatch());
+
+        versionTest = new Version("null");
+        assertEquals(0, versionTest.getMajor());
+        assertEquals(0, versionTest.getMinor());
+        assertEquals(0, versionTest.getPatch());
+    }
+
+    @Test
+    void testMakeVersion() {
+        assertEquals("9.8.7", Version.makeVersion(TYPE, NAME, "9.8.7").toString());
+        assertEquals("9.0.0", Version.makeVersion(TYPE, NAME, "9").toString());
+
+        assertNull(Version.makeVersion(TYPE, NAME, ""));
+        assertNull(Version.makeVersion(TYPE, NAME, "a.3.4"));
+        assertNull(Version.makeVersion(TYPE, NAME, "100."));
+        assertNull(Version.makeVersion(TYPE, NAME, "10000000000000000.2.3"));
+        assertNull(Version.makeVersion(TYPE, NAME, "1.20000000000000000.3"));
+        assertNull(Version.makeVersion(TYPE, NAME, "1.2.30000000000000000"));
+    }
+
+    @Test
+    void testNewVersion() {
+        vers = vers.newVersion();
+        assertEquals("11.0.0", vers.toString());
+    }
+
+    @Test
+    void testEquals() {
+        assertNotEquals(null, vers);
+        assertNotEquals(vers, new Object());
+
+        assertEquals(vers, vers);
+
+        assertEquals(vers, new Version(MAJOR, MINOR, PATCH));
+
+        assertNotEquals(vers, new Version(MAJOR + 1, MINOR, PATCH));
+        assertNotEquals(vers, new Version(MAJOR, MINOR + 1, PATCH));
+        assertNotEquals(vers, new Version(MAJOR, MINOR, PATCH + 1));
+    }
+
+    @Test
+    void testCompareTo() {
+        vers = new Version(101, 201, 301);
+
+        // equals case
+        assertEquals(0, new Version(101, 201, 301).compareTo(vers));
+
+        // major takes precedence
+        assertTrue(new Version(102, 200, 300).compareTo(vers) > 0);
+
+        // minor takes precedence over patch
+        assertTrue(new Version(101, 202, 300).compareTo(vers) > 0);
+
+        // compare major
+        assertTrue(new Version(100, 201, 301).compareTo(vers) < 0);
+        assertTrue(new Version(102, 201, 301).compareTo(vers) > 0);
+
+        // compare minor
+        assertTrue(new Version(101, 200, 301).compareTo(vers) < 0);
+        assertTrue(new Version(101, 202, 301).compareTo(vers) > 0);
+
+        // compare patch
+        assertTrue(new Version(101, 201, 300).compareTo(vers) < 0);
+        assertTrue(new Version(101, 201, 302).compareTo(vers) > 0);
+    }
+
+    @Test
+    void testToString() {
+        assertEquals("10.2.3", vers.toString());
+    }
+
+    @Test
+    void testGetMajor() {
+        assertEquals(MAJOR, vers.getMajor());
+    }
+
+    @Test
+    void testGetMinor() {
+        assertEquals(MINOR, vers.getMinor());
+    }
+
+    @Test
+    void testGetPatch() {
+        assertEquals(PATCH, vers.getPatch());
+    }
+
+    @Test
+    void testVersionIntIntInt() {
+        assertEquals("5.6.7", new Version(5, 6, 7).toString());
+    }
+
+    @Test
+    void testVersion() {
+        assertEquals("0.0.0", new Version().toString());
+    }
+}
diff --git a/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.message.bus.features.NetLoggerFeatureApi b/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.message.bus.features.NetLoggerFeatureApi
new file mode 100644 (file)
index 0000000..7889cb8
--- /dev/null
@@ -0,0 +1 @@
+org.onap.policy.common.message.bus.utils.NetLoggerUtilTest$NetLoggerFeature
\ No newline at end of file
diff --git a/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.utils.services.OrderedServiceImplTest$GenericService b/policy-common/src/test/resources/META-INF/services/org.onap.policy.common.utils.services.OrderedServiceImplTest$GenericService
new file mode 100644 (file)
index 0000000..1e920f4
--- /dev/null
@@ -0,0 +1,2 @@
+org.onap.policy.common.utils.services.OrderedServiceImplTest$HighPriorityService
+org.onap.policy.common.utils.services.OrderedServiceImplTest$LowPriorityService
\ No newline at end of file
diff --git a/policy-common/src/test/resources/keystore-test b/policy-common/src/test/resources/keystore-test
new file mode 100644 (file)
index 0000000..5820e0f
Binary files /dev/null and b/policy-common/src/test/resources/keystore-test differ
diff --git a/policy-common/src/test/resources/logback-test.xml b/policy-common/src/test/resources/logback-test.xml
new file mode 100644 (file)
index 0000000..ddf4f8b
--- /dev/null
@@ -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/policy-common/src/test/resources/org/onap/policy/common/endpoints/http/server/internal/HttpServerTest.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/http/server/internal/HttpServerTest.json
new file mode 100644 (file)
index 0000000..aba9d68
--- /dev/null
@@ -0,0 +1,8 @@
+{
+    "alive": false,
+    "host": "localhost",
+    "name": "echo",
+    "port": ${obj.port},
+    "sniHostCheck":false,
+    "prometheus":false
+}
diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_invalid.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_invalid.json
new file mode 100644 (file)
index 0000000..a745552
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "port": 6969,
+    "userName": "username",
+    "password": "password",
+    "https": true
+}
\ No newline at end of file
diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_valid.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/RestServerParameters_valid.json
new file mode 100644 (file)
index 0000000..61d793a
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "host": "0.0.0.0",
+    "port": 6969,
+    "userName": "username",
+    "password": "password",
+    "https": true
+}
\ No newline at end of file
diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_all_params.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_all_params.json
new file mode 100644 (file)
index 0000000..89e464d
--- /dev/null
@@ -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/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_invalid.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_invalid.json
new file mode 100644 (file)
index 0000000..775b488
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "topicSources" : [{
+        "topic" : "ueb-source",
+        "servers" : ["my-server"]
+    }]
+}
\ No newline at end of file
diff --git a/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_missing_mandatory.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_missing_mandatory.json
new file mode 100644 (file)
index 0000000..216c11e
--- /dev/null
@@ -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/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_valid.json b/policy-common/src/test/resources/org/onap/policy/common/endpoints/rest/TopicParameters_valid.json
new file mode 100644 (file)
index 0000000..2603bfd
--- /dev/null
@@ -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
diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/TopicEndpointProxyTest.json
new file mode 100644 (file)
index 0000000..5b2e712
--- /dev/null
@@ -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/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/BusTopicBaseTest.json
new file mode 100644 (file)
index 0000000..462278a
--- /dev/null
@@ -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/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/InlineBusTopicSinkTest.json
new file mode 100644 (file)
index 0000000..1f2fb55
--- /dev/null
@@ -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/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/SingleThreadedBusTopicSourceTest.json
new file mode 100644 (file)
index 0000000..305620c
--- /dev/null
@@ -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/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/TopicBaseTest.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/event/base/TopicBaseTest.json
new file mode 100644 (file)
index 0000000..b72b4ef
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "servers" : [ "svra", "svrb" ],
+  "topic" : "my-topic",
+  "effectiveTopic" : "my-topic",
+  "recentEvents" : [ ],
+  "alive" : false,
+  "locked" : false,
+  "topicCommInfrastructure" : "NOOP"
+}
diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_all_params.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_all_params.json
new file mode 100644 (file)
index 0000000..89e464d
--- /dev/null
@@ -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/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_invalid.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_invalid.json
new file mode 100644 (file)
index 0000000..775b488
--- /dev/null
@@ -0,0 +1,6 @@
+{
+    "topicSources" : [{
+        "topic" : "ueb-source",
+        "servers" : ["my-server"]
+    }]
+}
\ No newline at end of file
diff --git a/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_missing_mandatory.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_missing_mandatory.json
new file mode 100644 (file)
index 0000000..216c11e
--- /dev/null
@@ -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/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_valid.json b/policy-common/src/test/resources/org/onap/policy/common/message/bus/parameters/TopicParameters_valid.json
new file mode 100644 (file)
index 0000000..2603bfd
--- /dev/null
@@ -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
diff --git a/policy-common/src/test/resources/org/onap/policy/common/utils/coder/StandardCoder.json b/policy-common/src/test/resources/org/onap/policy/common/utils/coder/StandardCoder.json
new file mode 100644 (file)
index 0000000..b50b53b
--- /dev/null
@@ -0,0 +1 @@
+[3000,3010]
\ No newline at end of file
diff --git a/policy-common/src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml b/policy-common/src/test/resources/org/onap/policy/common/utils/coder/YamlJsonTranslator.yaml
new file mode 100644 (file)
index 0000000..1da7bfa
--- /dev/null
@@ -0,0 +1,21 @@
+---
+item: &top
+  boolVal: true
+  longVal: 1000
+  floatVal: 1010.1
+list:
+- intVal: 20
+  stringVal: string 30
+  nullVal:
+-
+- doubleVal: 40
+  another:
+    intVal: 50
+- *top
+map:
+  itemA:
+    stringVal: stringA
+  itemB:
+    stringVal: stringB
+    doubleVal: 123456789012345678901234567890
+  itemC: *top
diff --git a/policy-common/src/test/resources/org/onap/policy/common/utils/gson/GsonTestUtilsTest.json b/policy-common/src/test/resources/org/onap/policy/common/utils/gson/GsonTestUtilsTest.json
new file mode 100644 (file)
index 0000000..ff7ed70
--- /dev/null
@@ -0,0 +1,4 @@
+{
+    "id": ${obj.id},
+    "text": "${obj.text}"
+}
diff --git a/policy-common/src/test/resources/testdir/testfile.xml b/policy-common/src/test/resources/testdir/testfile.xml
new file mode 100644 (file)
index 0000000..4de3db1
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ============LICENSE_START=======================================================
+   Copyright (C) 2018 Ericsson. 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=========================================================
+-->
diff --git a/policy-common/src/test/resources/version.txt b/policy-common/src/test/resources/version.txt
new file mode 100644 (file)
index 0000000..9970c7b
--- /dev/null
@@ -0,0 +1 @@
+ONAP Version test.
diff --git a/policy-common/src/test/resources/webapps/alt-root/index.html b/policy-common/src/test/resources/webapps/alt-root/index.html
new file mode 100644 (file)
index 0000000..8ef757e
--- /dev/null
@@ -0,0 +1,30 @@
+<!--
+  ============LICENSE_START=======================================================
+   Copyright (C) 2020 Nordix Foundation.
+   Modifications Copyright (C) 2020 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=========================================================
+-->
+
+<?xml !DOCTYPE html>
+<html>
+    <head>
+        <title>Hello World</title>
+    </head>
+    <body>
+        <h4>Test Jetty Static Resources Alt-Root</h4>
+    </body>
+</html>
diff --git a/policy-common/src/test/resources/webapps/root/index.html b/policy-common/src/test/resources/webapps/root/index.html
new file mode 100644 (file)
index 0000000..35f6101
--- /dev/null
@@ -0,0 +1,30 @@
+<!--
+  ============LICENSE_START=======================================================
+   Copyright (C) 2020 Nordix Foundation.
+   Modifications Copyright (C) 2020 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=========================================================
+-->
+
+<?xml !DOCTYPE html>
+<html>
+    <head>
+        <title>Hello World</title>
+    </head>
+    <body>
+        <h4>Test Jetty Static Resources Root</h4>
+    </body>
+</html>
index 9823bba..f525714 100644 (file)
 
     <dependencies>
         <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>common-parameters</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>utils</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>utils-test</artifactId>
-            <version>${policy.common.version}</version>
-            <scope>test</scope>
+            <groupId>org.onap.policy.clamp</groupId>
+            <artifactId>policy-common</artifactId>
+            <version>${project.version}</version>
         </dependency>
         <dependency>
             <groupId>com.google.code.gson</groupId>
diff --git a/pom.xml b/pom.xml
index 51d02a3..91a073a 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -37,6 +37,7 @@
         This is the root Maven module for the policy-clamp project.
 
         It contains the following modules:
+        - policy-common: dependency code previously in policy/common repo
         - policy-models: dependency code previously in policy/models repo
         - policy-clamp-common: Common code for all Policy/CLAMP modules
         - policy-clamp-models: POJOs and other model code for REST and Kafka messages and for persistence
@@ -57,6 +58,7 @@
         <policy.models.version>5.0.0-SNAPSHOT</policy.models.version>
     </properties>
     <modules>
+        <module>policy-common</module>
         <module>policy-models</module>
         <module>common</module>
         <module>examples</module>
index 6bc856b..a0d4d85 100644 (file)
             <artifactId>policy-clamp-models</artifactId>
             <version>${project.version}</version>
         </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>common-parameters</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>gson</artifactId>
-            <version>${policy.common.version}</version>
-            <scope>runtime</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>message-bus</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>policy-endpoints</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>spring-utils</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.onap.policy.common</groupId>
-            <artifactId>utils</artifactId>
-            <version>${policy.common.version}</version>
-        </dependency>
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>