summaryrefslogtreecommitdiffstats
path: root/feature-pooling-dmaap/src
diff options
context:
space:
mode:
Diffstat (limited to 'feature-pooling-dmaap/src')
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/DmaapManager.java294
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/EventQueue.java121
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/FeatureEnabledChecker.java144
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java390
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java59
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java50
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java152
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java871
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java162
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/Serializer.java79
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/SpecProperties.java109
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ClassExtractors.java466
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/Extractor.java35
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ExtractorException.java49
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/FieldExtractor.java59
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MapExtractor.java60
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MethodExtractor.java58
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java215
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Forward.java180
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java60
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Identification.java45
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Leader.java75
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Message.java103
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java77
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Offline.java45
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Query.java44
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java255
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/FilterUtils.java96
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java85
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java55
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java410
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java209
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StartState.java132
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/State.java370
-rw-r--r--feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java37
-rw-r--r--feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureAPI1
-rw-r--r--feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.DroolsControllerFeatureAPI1
-rw-r--r--feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureAPI1
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/DmaapManagerTest.java355
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/EventQueueTest.java196
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/FeatureEnabledCheckerTest.java74
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java42
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java35
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java495
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java1342
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java178
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java96
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SpecPropertiesTest.java186
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTest.java440
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport.java40
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport2.java32
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ExtractorExceptionTest.java34
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/FieldExtractorTest.java77
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MapExtractorTest.java72
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MethodExtractorTest.java99
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/feature-pooling-dmaap.properties33
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BasicMessageTester.java245
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java351
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/ForwardTest.java217
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java62
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java77
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java77
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java80
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageWithAssignmentsTester.java110
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java41
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java41
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/Trial.java41
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java441
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/BasicStateTester.java318
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/FilterUtilsTest.java109
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java121
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java83
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java328
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java462
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java180
-rw-r--r--feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java440
-rw-r--r--feature-pooling-dmaap/src/test/resources/logback-test.xml17
77 files changed, 13321 insertions, 0 deletions
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/DmaapManager.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/DmaapManager.java
new file mode 100644
index 00000000..98543f29
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/DmaapManager.java
@@ -0,0 +1,294 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import java.util.List;
+import java.util.Properties;
+import org.onap.policy.drools.event.comm.FilterableTopicSource;
+import org.onap.policy.drools.event.comm.TopicEndpoint;
+import org.onap.policy.drools.event.comm.TopicListener;
+import org.onap.policy.drools.event.comm.TopicSink;
+import org.onap.policy.drools.event.comm.TopicSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages the internal DMaaP topic.
+ */
+public class DmaapManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(DmaapManager.class);
+
+ /**
+ * Factory used to construct objects.
+ */
+ private static Factory factory = new Factory();
+
+ /**
+ * Name of the DMaaP topic.
+ */
+ private final String topic;
+
+ /**
+ * Topic source whose filter is to be manipulated.
+ */
+ private final FilterableTopicSource topicSource;
+
+ /**
+ * Where to publish messages.
+ */
+ private final TopicSink topicSink;
+
+ /**
+ * Topic sources. In theory, there's only one item in this list, the
+ * internal DMaaP topic.
+ */
+ private final List<TopicSource> sources;
+
+ /**
+ * Topic sinks. In theory, there's only one item in this list, the internal
+ * DMaaP topic.
+ */
+ private final List<TopicSink> sinks;
+
+ /**
+ * {@code True} if the consumer is running, {@code false} otherwise.
+ */
+ private boolean consuming = false;
+
+ /**
+ * {@code True} if the publisher is running, {@code false} otherwise.
+ */
+ private boolean publishing = false;
+
+ /**
+ * Constructs the manager, but does not start the source or sink.
+ *
+ * @param topic name of the internal DMaaP topic
+ * @param props properties to configure the topic source & sink
+ * @throws PoolingFeatureException if an error occurs
+ */
+ public DmaapManager(String topic, Properties props) throws PoolingFeatureException {
+
+ logger.info("initializing bus for topic {}", topic);
+
+ try {
+ this.topic = topic;
+ this.sources = factory.initTopicSources(props);
+ this.sinks = factory.initTopicSinks(props);
+
+ this.topicSource = findTopicSource();
+ this.topicSink = findTopicSink();
+
+ // verify that we can set the filter
+ setFilter(null);
+
+ } catch (IllegalArgumentException e) {
+ logger.error("failed to attach to topic {}", topic);
+ throw new PoolingFeatureException(e);
+ }
+ }
+
+ protected static Factory getFactory() {
+ return factory;
+ }
+
+ /**
+ * Used by junit tests to set the factory used to create various objects
+ * used by this class.
+ *
+ * @param factory the new factory
+ */
+ protected static void setFactory(Factory factory) {
+ DmaapManager.factory = factory;
+ }
+
+ public String getTopic() {
+ return topic;
+ }
+
+ /**
+ * Finds the topic source associated with the internal DMaaP topic.
+ *
+ * @return the topic source
+ * @throws PoolingFeatureException if the source doesn't exist or is not
+ * filterable
+ */
+ private FilterableTopicSource findTopicSource() throws PoolingFeatureException {
+ for (TopicSource src : sources) {
+ if (topic.equals(src.getTopic())) {
+ if (src instanceof FilterableTopicSource) {
+ return (FilterableTopicSource) src;
+
+ } else {
+ throw new PoolingFeatureException("topic source " + topic + " is not filterable");
+ }
+ }
+ }
+
+ throw new PoolingFeatureException("missing topic source " + topic);
+ }
+
+ /**
+ * Finds the topic sink associated with the internal DMaaP topic.
+ *
+ * @return the topic sink
+ * @throws PoolingFeatureException if the sink doesn't exist
+ */
+ private TopicSink findTopicSink() throws PoolingFeatureException {
+ for (TopicSink sink : sinks) {
+ if (topic.equals(sink.getTopic())) {
+ return sink;
+ }
+ }
+
+ throw new PoolingFeatureException("missing topic sink " + topic);
+ }
+
+ /**
+ * Starts the publisher, if it isn't already running.
+ *
+ * @throws PoolingFeatureException if an error occurs
+ */
+ public void startPublisher() throws PoolingFeatureException {
+ if (publishing) {
+ return;
+ }
+
+ try {
+ topicSink.start();
+ publishing = true;
+
+ } catch (IllegalStateException e) {
+ throw new PoolingFeatureException("cannot start topic sink " + topic, e);
+ }
+ }
+
+ /**
+ * Stops the publisher.
+ */
+ public void stopPublisher() {
+ if (!publishing) {
+ return;
+ }
+
+ try {
+ publishing = false;
+ topicSink.stop();
+
+ } catch (IllegalStateException e) {
+ logger.error("cannot stop sink for topic {}", topic, e);
+ }
+ }
+
+ /**
+ * Starts the consumer, if it isn't already running.
+ *
+ * @param listener listener to register with the source
+ */
+ public void startConsumer(TopicListener listener) {
+ if (consuming) {
+ return;
+ }
+
+ topicSource.register(listener);
+ consuming = true;
+ }
+
+ /**
+ * Stops the consumer.
+ *
+ * @param listener listener to unregister with the source
+ */
+ public void stopConsumer(TopicListener listener) {
+ if (!consuming) {
+ return;
+ }
+
+ consuming = false;
+ topicSource.unregister(listener);
+ }
+
+ /**
+ * Sets the server-side filter to be used by the consumer.
+ *
+ * @param filter the filter string, or {@code null} if no filter is to be
+ * used
+ * @throws PoolingFeatureException if the topic is not filterable
+ */
+ public void setFilter(String filter) throws PoolingFeatureException {
+ try {
+ topicSource.setFilter(filter);
+
+ } catch (UnsupportedOperationException e) {
+ throw new PoolingFeatureException("cannot filter topic " + topic);
+ }
+ }
+
+ /**
+ * Publishes a message to the sink.
+ *
+ * @param msg message to be published
+ * @throws PoolingFeatureException if an error occurs or the publisher isn't
+ * running
+ */
+ public void publish(String msg) throws PoolingFeatureException {
+ if (!publishing) {
+ throw new PoolingFeatureException(new IllegalStateException("no topic sink " + topic));
+ }
+
+ try {
+ if (!topicSink.send(msg)) {
+ throw new PoolingFeatureException("failed to send to topic sink " + topic);
+ }
+
+ } catch (IllegalStateException e) {
+ throw new PoolingFeatureException("cannot send to topic sink " + topic, e);
+ }
+ }
+
+ /**
+ * Factory used to construct objects.
+ */
+ public static class Factory {
+
+ /**
+ * Initializes the topic sources.
+ *
+ * @param props properties used to configure the topics
+ * @return the topic sources
+ */
+ public List<TopicSource> initTopicSources(Properties props) {
+ return TopicEndpoint.manager.addTopicSources(props);
+ }
+
+ /**
+ * Initializes the topic sinks.
+ *
+ * @param props properties used to configure the topics
+ * @return the topic sinks
+ */
+ public List<TopicSink> initTopicSinks(Properties props) {
+ return TopicEndpoint.manager.addTopicSinks(props);
+ }
+
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/EventQueue.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/EventQueue.java
new file mode 100644
index 00000000..0bed85b5
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/EventQueue.java
@@ -0,0 +1,121 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import java.util.Deque;
+import java.util.LinkedList;
+import org.onap.policy.drools.pooling.message.Forward;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Finite queue of events waiting to be processed once the buckets have been
+ * assigned.
+ */
+public class EventQueue {
+
+ private static final Logger logger = LoggerFactory.getLogger(EventQueue.class);
+
+ /**
+ * Maximum number of events allowed in the queue. When excess events are
+ * added, the older events are removed.
+ */
+ private int maxEvents;
+
+ /**
+ * Maximum age, in milliseconds, of events in the queue. Events that are
+ * older than this are discarded rather than being handed off when
+ * {@link #poll()} is invoked.
+ */
+ private long maxAgeMs;
+
+ /**
+ * The actual queue of events.
+ */
+ private Deque<Forward> events = new LinkedList<>();
+
+ /**
+ *
+ * @param maxEvents maximum number of events to hold in the queue
+ * @param maxAgeMs maximum age of events in the queue
+ */
+ public EventQueue(int maxEvents, long maxAgeMs) {
+ this.maxEvents = maxEvents;
+ this.maxAgeMs = maxAgeMs;
+ }
+
+ /**
+ *
+ * @return {@code true} if the queue is empty, {@code false} otherwise
+ */
+ public boolean isEmpty() {
+ return events.isEmpty();
+ }
+
+ /**
+ * Clears the queue.
+ */
+ public void clear() {
+ events.clear();
+ }
+
+ /**
+ *
+ * @return the number of elements in the queue
+ */
+ public int size() {
+ return events.size();
+ }
+
+ /**
+ * Adds an item to the queue. If the queue is full, the older item is
+ * removed and discarded.
+ *
+ * @param event
+ */
+ public void add(Forward event) {
+ if (events.size() >= maxEvents) {
+ logger.warn("full queue - discarded event for topic {}", event.getTopic());
+ events.remove();
+ }
+
+ events.add(event);
+ }
+
+ /**
+ * Gets the oldest, un-expired event from the queue.
+ *
+ * @return the oldest, un-expired event
+ */
+ public Forward poll() {
+ long tmin = System.currentTimeMillis() - maxAgeMs;
+
+ Forward ev;
+ while ((ev = events.poll()) != null) {
+ if (!ev.isExpired(tmin)) {
+ break;
+ }
+ }
+
+ return ev;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/FeatureEnabledChecker.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/FeatureEnabledChecker.java
new file mode 100644
index 00000000..d2f32043
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/FeatureEnabledChecker.java
@@ -0,0 +1,144 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+// TODO move to policy-utils
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.util.Properties;
+import org.onap.policy.common.utils.properties.SpecPropertyConfiguration;
+import org.onap.policy.common.utils.properties.exception.PropertyException;
+
+/**
+ * Checks whether or not a feature is enabled. The name of the "enable" property
+ * is assumed to be of the form accepted by a {@link SpecPropertyConfiguration},
+ * which contains a substitution place-holder into which a "specializer" (e.g.,
+ * controller or session name) is substituted.
+ */
+public class FeatureEnabledChecker {
+
+ /**
+ *
+ */
+ private FeatureEnabledChecker() {
+ super();
+ }
+
+ /**
+ * Determines if a feature is enabled for a particular specializer.
+ *
+ * @param props properties from which to extract the "enabled" flag
+ * @param specializer specializer to be substituted into the property name
+ * when extracting
+ * @param propName the name of the "enabled" property
+ * @return {@code true} if the feature is enabled, or {@code false} if it is
+ * not enabled (or if the property doesn't exist)
+ * @throws IllegalArgumentException if the "enabled" property is not a
+ * boolean value
+ */
+ public static boolean isFeatureEnabled(Properties props, String specializer, String propName) {
+
+ try {
+ return new Config(specializer, props, propName).isEnabled();
+
+ } catch (PropertyException e) {
+ throw new IllegalArgumentException("cannot check property " + propName, e);
+ }
+ }
+
+
+ /**
+ * Configuration used to extract the value.
+ */
+ private static class Config extends SpecPropertyConfiguration {
+
+ /**
+ * There is a bit of trickery here. This annotation is just a
+ * place-holder to get the superclass to invoke the
+ * {@link #setValue(java.lang.reflect.Field, Properties, Property)
+ * setValue()} method. When that's invoked, we'll substitute
+ * {@link #propOverride} instead of this annotation.
+ */
+ @Property(name = "feature-enabled-property-place-holder")
+ private boolean enabled;
+
+ /**
+ * Annotation that will actually be used to set the field.
+ */
+ private Property propOverride;
+
+ /**
+ *
+ * @param specializer specializer to be substituted into the property
+ * name when extracting
+ * @param props properties from which to extract the "enabled" flag
+ * @param propName the name of the "enabled" property
+ * @throws PropertyException if an error occurs
+ */
+ public Config(String specializer, Properties props, String propName) throws PropertyException {
+ super(specializer);
+
+ propOverride = new Property() {
+
+ @Override
+ public String name() {
+ return propName;
+ }
+
+ @Override
+ public String defaultValue() {
+ // feature is disabled by default
+ return "false";
+ }
+
+ @Override
+ public String accept() {
+ return "";
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return Property.class;
+ }
+ };
+
+ setAllFields(props);
+ }
+
+ /**
+ * Substitutes {@link #propOverride} for "prop".
+ */
+ @Override
+ protected boolean setValue(Field field, Properties props, Property prop) throws PropertyException {
+ return super.setValue(field, props, propOverride);
+ }
+
+ /**
+ *
+ * @return {@code true} if the feature is enabled, {@code false}
+ * otherwise
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+ };
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java
new file mode 100644
index 00000000..da47a031
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java
@@ -0,0 +1,390 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import java.io.IOException;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import org.onap.policy.common.utils.properties.exception.PropertyException;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.core.PolicySessionFeatureAPI;
+import org.onap.policy.drools.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.drools.features.DroolsControllerFeatureAPI;
+import org.onap.policy.drools.features.PolicyControllerFeatureAPI;
+import org.onap.policy.drools.system.PolicyController;
+import org.onap.policy.drools.utils.PropertyUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Controller/session pooling. Multiple hosts may be launched, all servicing the
+ * same controllers/sessions. When this feature is enabled, the requests are
+ * divided across the different hosts, instead of all running on a single,
+ * active host.
+ * <p>
+ * With each controller, there is an associated DMaaP topic that is used for
+ * internal communication between the different hosts serving the controller.
+ */
+public class PoolingFeature implements PolicyControllerFeatureAPI, DroolsControllerFeatureAPI, PolicySessionFeatureAPI {
+
+ private static final Logger logger = LoggerFactory.getLogger(PoolingFeature.class);
+
+ // TODO state-management doesn't allow more than one active host at a time
+
+ /**
+ * Factory used to create objects.
+ */
+ private static Factory factory;
+
+ /**
+ * Entire set of feature properties, including those specific to various
+ * controllers.
+ */
+ private Properties featProps = null;
+
+ /**
+ * Maps a controller name to its associated manager.
+ */
+ private ConcurrentHashMap<String, PoolingManagerImpl> ctlr2pool = new ConcurrentHashMap<>(107);
+
+ /**
+ * Arguments passed to beforeOffer(), which are saved for when the
+ * beforeInsert() is called later. As multiple threads can be active within
+ * the methods at the same time, we must keep this in thread local storage.
+ */
+ private ThreadLocal<OfferArgs> offerArgs = new ThreadLocal<>();
+
+ /**
+ *
+ */
+ public PoolingFeature() {
+ super();
+ }
+
+ protected static Factory getFactory() {
+ return factory;
+ }
+
+ /**
+ * Sets the factory to be used to create objects. Used by junit tests.
+ *
+ * @param factory the new factory to be used to create objects
+ */
+ protected static void setFactory(Factory factory) {
+ PoolingFeature.factory = factory;
+ }
+
+ @Override
+ public int getSequenceNumber() {
+ return 0;
+ }
+
+ /**
+ * @throws PoolingFeatureRtException if the properties cannot be read or are
+ * invalid
+ */
+ @Override
+ public void globalInit(String[] args, String configDir) {
+ logger.info("initializing pooling feature");
+
+ try {
+ featProps = PropertyUtil.getProperties(configDir + "/feature-pooling-dmaap.properties");
+
+ } catch (IOException ex) {
+ throw new PoolingFeatureRtException(ex);
+ }
+ }
+
+ /**
+ * Adds the controller and a new pooling manager to {@link #ctlr2pool}.
+ *
+ * @throws PoolingFeatureRtException if an error occurs
+ */
+ @Override
+ public boolean afterCreate(PolicyController controller) {
+
+ if (featProps == null) {
+ logger.error("pooling feature properties have not been loaded");
+ throw new PoolingFeatureRtException(new IllegalStateException("missing pooling feature properties"));
+ }
+
+ String name = controller.getName();
+
+ if (FeatureEnabledChecker.isFeatureEnabled(featProps, name, PoolingProperties.FEATURE_ENABLED)) {
+ try {
+ // get & validate the properties
+ PoolingProperties props = new PoolingProperties(name, featProps);
+
+ logger.info("pooling enabled for {}", name);
+ ctlr2pool.computeIfAbsent(name, xxx -> factory.makeManager(controller, props));
+
+ } catch (PropertyException e) {
+ logger.error("pooling disabled due to exception for {}", name, e);
+ throw new PoolingFeatureRtException(e);
+ }
+
+ } else {
+ logger.info("pooling disabled for {}", name);
+ }
+
+
+ return false;
+ }
+
+ @Override
+ public boolean beforeStart(PolicyController controller) {
+ return doManager(controller, mgr -> {
+ mgr.beforeStart();
+ return false;
+ });
+ }
+
+ @Override
+ public boolean afterStart(PolicyController controller) {
+ return doManager(controller, mgr -> {
+ mgr.afterStart();
+ return false;
+ });
+ }
+
+ @Override
+ public boolean beforeStop(PolicyController controller) {
+ return doManager(controller, mgr -> {
+ mgr.beforeStop();
+ return false;
+ });
+ }
+
+ @Override
+ public boolean afterStop(PolicyController controller) {
+
+ // NOTE: using doDeleteManager() instead of doManager()
+
+ return doDeleteManager(controller, mgr -> {
+
+ mgr.afterStop();
+ return false;
+ });
+ }
+
+ @Override
+ public boolean beforeLock(PolicyController controller) {
+ return doManager(controller, mgr -> {
+ mgr.beforeLock();
+ return false;
+ });
+ }
+
+ @Override
+ public boolean afterUnlock(PolicyController controller) {
+ return doManager(controller, mgr -> {
+ mgr.afterUnlock();
+ return false;
+ });
+ }
+
+ @Override
+ public boolean beforeOffer(PolicyController controller, CommInfrastructure protocol, String topic2, String event) {
+ /*
+ * As this is invoked a lot, we'll directly call the manager's method
+ * instead of using the functional interface via doManager().
+ */
+ PoolingManagerImpl mgr = ctlr2pool.get(controller.getName());
+ if (mgr == null) {
+ return false;
+ }
+
+ if (mgr.beforeOffer(protocol, topic2, event)) {
+ return true;
+ }
+
+ offerArgs.set(new OfferArgs(protocol, topic2, event));
+ return false;
+ }
+
+ @Override
+ public boolean beforeInsert(DroolsController droolsController, Object fact) {
+
+ OfferArgs args = offerArgs.get();
+ if (args == null) {
+ return false;
+ }
+
+ PolicyController controller;
+ try {
+ controller = factory.getController(droolsController);
+
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ return false;
+ }
+
+ if (controller == null) {
+ return false;
+ }
+
+ /*
+ * As this is invoked a lot, we'll directly call the manager's method
+ * instead of using the functional interface via doManager().
+ */
+ PoolingManagerImpl mgr = ctlr2pool.get(controller.getName());
+ if (mgr == null) {
+ return false;
+ }
+
+ return mgr.beforeInsert(args.protocol, args.topic, args.event, fact);
+ }
+
+ @Override
+ public boolean afterOffer(PolicyController controller, CommInfrastructure protocol, String topic, String event,
+ boolean success) {
+
+ // clear any stored arguments
+ offerArgs.set(null);
+
+ return false;
+ }
+
+ /**
+ * Executes a function using the manager associated with the controller.
+ * Catches any exceptions from the function and re-throws it as a runtime
+ * exception.
+ *
+ * @param controller
+ * @param func function to be executed
+ * @return {@code true} if the function handled the request, {@code false}
+ * otherwise
+ * @throws PoolingFeatureRtException if an error occurs
+ */
+ private boolean doManager(PolicyController controller, MgrFunc func) {
+ PoolingManagerImpl mgr = ctlr2pool.get(controller.getName());
+ if (mgr == null) {
+ return false;
+ }
+
+ try {
+ return func.apply(mgr);
+
+ } catch (PoolingFeatureException e) {
+ throw e.toRuntimeException();
+ }
+ }
+
+ /**
+ * Executes a function using the manager associated with the controller and
+ * then deletes the manager. Catches any exceptions from the function and
+ * re-throws it as a runtime exception.
+ *
+ * @param controller
+ * @param func function to be executed
+ * @return {@code true} if the function handled the request, {@code false}
+ * otherwise
+ * @throws PoolingFeatureRtException if an error occurs
+ */
+ private boolean doDeleteManager(PolicyController controller, Function<PoolingManagerImpl, Boolean> func) {
+
+ // NOTE: using "remove()" instead of "get()"
+
+ PoolingManagerImpl mgr = ctlr2pool.remove(controller.getName());
+
+ if (mgr == null) {
+ return false;
+ }
+
+ return func.apply(mgr);
+ }
+
+ /**
+ * Function that operates on a manager.
+ */
+ @FunctionalInterface
+ private static interface MgrFunc {
+
+ /**
+ *
+ * @param mgr
+ * @return {@code true} if the request was handled by the manager,
+ * {@code false} otherwise
+ * @throws PoolingFeatureException
+ */
+ public boolean apply(PoolingManagerImpl mgr) throws PoolingFeatureException;
+ }
+
+ /**
+ * Arguments captured from beforeOffer().
+ */
+ private static class OfferArgs {
+
+ /**
+ * Protocol of the receiving topic.
+ */
+ private CommInfrastructure protocol;
+
+ /**
+ * Topic on which the event was received.
+ */
+ private String topic;
+
+ /**
+ * The event text that was received on the topic.
+ */
+ private String event;
+
+ /**
+ *
+ * @param protocol
+ * @param topic
+ * @param event the actual event data received on the topic
+ */
+ public OfferArgs(CommInfrastructure protocol, String topic, String event) {
+ this.protocol = protocol;
+ this.topic = topic;
+ this.event = event;
+ }
+ }
+
+ /**
+ * Used to create objects.
+ */
+ public static class Factory {
+
+ /**
+ * Makes a pooling manager for a controller.
+ *
+ * @param controller
+ * @param props properties to use to configure the manager
+ * @return a new pooling manager
+ */
+ public PoolingManagerImpl makeManager(PolicyController controller, PoolingProperties props) {
+ return new PoolingManagerImpl(controller, props);
+ }
+
+ /**
+ * Gets the policy controller associated with a drools controller.
+ *
+ * @param droolsController
+ * @return the policy controller associated with a drools controller
+ */
+ public PolicyController getController(DroolsController droolsController) {
+ return PolicyController.factory.get(droolsController);
+ }
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java
new file mode 100644
index 00000000..5efd1414
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java
@@ -0,0 +1,59 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+/**
+ * Exception thrown by the pooling feature.
+ */
+public class PoolingFeatureException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public PoolingFeatureException() {
+ super();
+ }
+
+ public PoolingFeatureException(String message) {
+ super(message);
+ }
+
+ public PoolingFeatureException(Throwable cause) {
+ super(cause);
+ }
+
+ public PoolingFeatureException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public PoolingFeatureException(String message, Throwable cause, boolean enableSuppression,
+ boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+ /**
+ * Converts the exception to a runtime exception.
+ *
+ * @return a new runtime exception, wrapping this exception
+ */
+ public PoolingFeatureRtException toRuntimeException() {
+ return new PoolingFeatureRtException(this);
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java
new file mode 100644
index 00000000..6fdb6c69
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java
@@ -0,0 +1,50 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+/**
+ * A runtime exception thrown by the pooling feature.
+ */
+public class PoolingFeatureRtException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public PoolingFeatureRtException() {
+ super();
+ }
+
+ public PoolingFeatureRtException(String message) {
+ super(message);
+ }
+
+ public PoolingFeatureRtException(Throwable cause) {
+ super(cause);
+ }
+
+ public PoolingFeatureRtException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public PoolingFeatureRtException(String message, Throwable cause, boolean enableSuppression,
+ boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java
new file mode 100644
index 00000000..de08d1e1
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java
@@ -0,0 +1,152 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledFuture;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Forward;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.state.State;
+import org.onap.policy.drools.pooling.state.StateTimerTask;
+
+/**
+ * Pooling manager for a single PolicyController.
+ */
+public interface PoolingManager {
+
+ /**
+ * Gets the properties used to configure the manager.
+ *
+ * @return
+ */
+ public PoolingProperties getProperties();
+
+ /**
+ * Gets the host id.
+ *
+ * @return the host id
+ */
+ public String getHost();
+
+ /**
+ * Gets the name of the internal DMaaP topic used by this manager to
+ * communicate with its other hosts.
+ *
+ * @return the name of the internal DMaaP topic
+ */
+ public String getTopic();
+
+ /**
+ * Indicates that communication with internal DMaaP topic failed, typically
+ * due to a missed heart beat. Stops the PolicyController.
+ *
+ * @return a latch that can be used to determine when the controller's
+ * stop() method has completed
+ */
+ public CountDownLatch internalTopicFailed();
+
+ /**
+ * Starts distributing requests according to the given bucket assignments.
+ *
+ * @param assignments must <i>not</i> be {@code null}
+ */
+ public void startDistributing(BucketAssignments assignments);
+
+ /**
+ * Gets the current bucket assignments.
+ *
+ * @return the current bucket assignments, or {@code null} if no assignments
+ * have been made
+ */
+ public BucketAssignments getAssignments();
+
+ /**
+ * Publishes a message to the internal topic on the administrative channel.
+ *
+ * @param msg message to be published
+ */
+ public void publishAdmin(Message msg);
+
+ /**
+ * Publishes a message to the internal topic on a particular channel.
+ *
+ * @param channel channel on which the message should be published
+ * @param msg message to be published
+ */
+ public void publish(String channel, Message msg);
+
+ /**
+ * Handles a {@link Forward} event that was received from the internal
+ * topic.
+ *
+ * @param event
+ */
+ public void handle(Forward event);
+
+ /**
+ * Schedules a timer to fire after a delay.
+ *
+ * @param delayMs delay, in milliseconds
+ * @param task
+ * @return a future that can be used to cancel the timer
+ */
+ public ScheduledFuture<?> schedule(long delayMs, StateTimerTask task);
+
+ /**
+ * Schedules a timer to fire repeatedly.
+ *
+ * @param initialDelayMs initial delay, in milliseconds
+ * @param delayMs delay, in milliseconds
+ * @param task
+ * @return a future that can be used to cancel the timer
+ */
+ public ScheduledFuture<?> scheduleWithFixedDelay(long initialDelayMs, long delayMs, StateTimerTask task);
+
+ /**
+ * Transitions to the "start" state.
+ *
+ * @return the new state
+ */
+ public State goStart();
+
+ /**
+ * Transitions to the "query" state.
+ *
+ * @return the new state
+ */
+ public State goQuery();
+
+ /**
+ * Transitions to the "active" state.
+ *
+ * @return the new state
+ */
+ public State goActive();
+
+ /**
+ * Transitions to the "inactive" state.
+ *
+ * @return the new state
+ */
+ public State goInactive();
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java
new file mode 100644
index 00000000..cd71670d
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java
@@ -0,0 +1,871 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.drools.event.comm.TopicListener;
+import org.onap.policy.drools.pooling.extractor.ClassExtractors;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Forward;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.state.ActiveState;
+import org.onap.policy.drools.pooling.state.IdleState;
+import org.onap.policy.drools.pooling.state.InactiveState;
+import org.onap.policy.drools.pooling.state.QueryState;
+import org.onap.policy.drools.pooling.state.StartState;
+import org.onap.policy.drools.pooling.state.State;
+import org.onap.policy.drools.pooling.state.StateTimerTask;
+import org.onap.policy.drools.protocol.coders.EventProtocolCoder;
+import org.onap.policy.drools.system.PolicyController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+/**
+ * Implementation of a {@link PoolingManager}. Until bucket assignments have
+ * been made, events coming from external topics are saved in a queue for later
+ * processing. Once assignments are made, the saved events are processed. In
+ * addition, while the controller is locked, events are still forwarded to other
+ * hosts and bucket assignments are still updated, based on any {@link Leader}
+ * messages that it receives.
+ */
+public class PoolingManagerImpl implements PoolingManager, TopicListener {
+
+ private static final Logger logger = LoggerFactory.getLogger(PoolingManagerImpl.class);
+
+ // TODO metrics, audit logging
+
+ /**
+ * Maximum number of times a message can be forwarded.
+ */
+ public static final int MAX_HOPS = 5;
+
+ /**
+ * Type of item that the extractors will be extracting.
+ */
+ private static final String EXTRACTOR_TYPE = "requestId";
+
+ /**
+ * Prefix for extractor properties.
+ */
+ private static final String PROP_EXTRACTOR_PREFIX = "extractor." + EXTRACTOR_TYPE;
+
+ /**
+ * Factory used to create various objects. Can be overridden during junit
+ * testing.
+ */
+ private static Factory factory = new Factory();
+
+ /**
+ * ID of this host.
+ */
+ private final String host;
+
+ /**
+ * Properties with which this was configured.
+ */
+ private final PoolingProperties props;
+
+ /**
+ * Associated controller.
+ */
+ private final PolicyController controller;
+
+ /**
+ * Where to offer events that have been forwarded to this host (i.e, the
+ * controller).
+ */
+ private final TopicListener listener;
+
+ /**
+ * Used to encode & decode request objects received from & sent to a rule
+ * engine.
+ */
+ private final Serializer serializer;
+
+ /**
+ * Internal DMaaP topic used by this controller.
+ */
+ private final String topic;
+
+ /**
+ * Manager for the internal DMaaP topic.
+ */
+ private final DmaapManager dmaapMgr;
+
+ /**
+ * Used to extract the request id from the decoded message.
+ */
+ private final ClassExtractors extractors;
+
+ /**
+ * Lock used while updating {@link #current}. In general, public methods
+ * must use this, while private methods assume the lock is already held.
+ */
+ private final Object curLocker = new Object();
+
+ /**
+ * Current state.
+ * <p>
+ * This uses a finite state machine, wherein the state object contains all
+ * of the data relevant to that state. Each state object has a process()
+ * method, specific to each type of {@link Message} subclass. The method
+ * returns the next state object, or {@code null} if the state is to remain
+ * the same.
+ */
+ private State current;
+
+ /**
+ * Current bucket assignments or {@code null}.
+ */
+ private BucketAssignments assignments = null;
+
+ /**
+ * Pool used to execute timers.
+ */
+ private ScheduledThreadPoolExecutor scheduler = null;
+
+ /**
+ * Queue used when no bucket assignments are available.
+ */
+ private EventQueue eventq;
+
+ /**
+ * {@code True} if events offered by the controller should be intercepted,
+ * {@code false} otherwise.
+ */
+ private boolean intercept = true;
+
+ /**
+ * Constructs the manager, initializing all of the data structures.
+ *
+ * @param controller controller with which this is associated
+ * @param props feature properties specific to the controller
+ */
+ public PoolingManagerImpl(PolicyController controller, PoolingProperties props) {
+ this.host = UUID.randomUUID().toString();
+ this.controller = controller;
+ this.props = props;
+
+ try {
+ this.listener = (TopicListener) controller;
+ this.serializer = new Serializer();
+ this.topic = props.getPoolingTopic();
+ this.eventq = factory.makeEventQueue(props);
+
+ SpecProperties spec = new SpecProperties(PROP_EXTRACTOR_PREFIX, controller.getName());
+ this.extractors = factory.makeClassExtractors(spec);
+
+ this.dmaapMgr = factory.makeDmaapManager(props);
+ this.current = new IdleState(this);
+
+ logger.info("allocating host {} to controller {} for topic {}", host, controller.getName(), topic);
+
+ } catch (ClassCastException e) {
+ logger.error("not a topic listener, controller {}", controller.getName());
+ throw new PoolingFeatureRtException(e);
+
+ } catch (PoolingFeatureException e) {
+ logger.error("failed to attach internal DMaaP topic to controller {}", controller.getName());
+ throw e.toRuntimeException();
+ }
+ }
+
+ protected static Factory getFactory() {
+ return factory;
+ }
+
+ protected static void setFactory(Factory factory) {
+ PoolingManagerImpl.factory = factory;
+ }
+
+ /**
+ * Should only be used by junit tests.
+ *
+ * @return the current state
+ */
+ protected State getCurrent() {
+ synchronized (curLocker) {
+ return current;
+ }
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public String getTopic() {
+ return topic;
+ }
+
+ @Override
+ public PoolingProperties getProperties() {
+ return props;
+ }
+
+ /**
+ * Indicates that the controller is about to start. Starts the publisher for
+ * the internal topic, and creates a thread pool for the timers.
+ *
+ * @throws PoolingFeatureException if the internal topic publisher cannot be
+ * started
+ */
+ public void beforeStart() throws PoolingFeatureException {
+ synchronized (curLocker) {
+ if (scheduler == null) {
+ dmaapMgr.startPublisher();
+
+ scheduler = factory.makeScheduler();
+
+ /*
+ * Only a handful of timers at any moment, thus we can afford to
+ * take the time to remove them when they're cancelled.
+ */
+ scheduler.setRemoveOnCancelPolicy(true);
+ scheduler.setMaximumPoolSize(1);
+ scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+ scheduler.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+ }
+ }
+ }
+
+ /**
+ * Indicates that the controller has successfully started. Starts the
+ * consumer for the internal topic, enters the {@link StartState}, and sets
+ * the filter for the initial state.
+ */
+ public void afterStart() {
+ synchronized (curLocker) {
+ if (current instanceof IdleState) {
+ dmaapMgr.startConsumer(this);
+ changeState(new StartState(this));
+ }
+ }
+ }
+
+ /**
+ * Indicates that the controller is about to stop. Stops the consumer, the
+ * scheduler, and the current state.
+ */
+ public void beforeStop() {
+ ScheduledThreadPoolExecutor sched;
+
+ synchronized (curLocker) {
+ sched = scheduler;
+ scheduler = null;
+
+ if (!(current instanceof IdleState)) {
+ dmaapMgr.stopConsumer(this);
+ changeState(new IdleState(this));
+
+ // TODO
+ /*
+ * Need a brief delay here to allow "offline" message to be
+ * transmitted?
+ */
+ }
+ }
+
+ if (sched != null) {
+ sched.shutdownNow();
+ }
+ }
+
+ /**
+ * Indicates that the controller has stopped. Stops the publisher and logs a
+ * warning if any events are still in the queue.
+ */
+ public void afterStop() {
+ synchronized (curLocker) {
+ if (!eventq.isEmpty()) {
+ logger.warn("discarded {} messages after stopping topic {}", eventq.size(), topic);
+ eventq.clear();
+ }
+
+ dmaapMgr.stopPublisher();
+ }
+ }
+
+ /**
+ * Indicates that the controller is about to be locked. Enters the idle
+ * state, as all it will be doing is forwarding messages.
+ */
+ public void beforeLock() {
+ synchronized (curLocker) {
+ changeState(new IdleState(this));
+ }
+ }
+
+ /**
+ * Indicates that the controller has been unlocked. Enters the start state,
+ * if the controller is running.
+ */
+ public void afterUnlock() {
+ synchronized (curLocker) {
+ if (controller.isAlive() && current instanceof IdleState && scheduler != null) {
+ changeState(new StartState(this));
+ }
+ }
+ }
+
+ /**
+ * Changes the finite state machine to a new state, provided the new state
+ * is not {@code null}.
+ *
+ * @param newState new state, or {@code null} if to remain unchanged
+ */
+ private void changeState(State newState) {
+ if (newState != null) {
+ current.cancelTimers();
+ current = newState;
+
+ // set the filter before starting the state
+ setFilter(newState.getFilter());
+ newState.start();
+ }
+ }
+
+ /**
+ * Sets the server-side filter for the internal topic.
+ *
+ * @param filter new filter to be used
+ */
+ private void setFilter(Map<String, Object> filter) {
+ try {
+ dmaapMgr.setFilter(serializer.encodeFilter(filter));
+
+ } catch (JsonProcessingException e) {
+ logger.error("failed to encode server-side filter for topic {}, {}", topic, filter, e);
+
+ } catch (PoolingFeatureException e) {
+ logger.error("failed to set server-side filter for topic {}, {}", topic, filter, e);
+ }
+ }
+
+ @Override
+ public CountDownLatch internalTopicFailed() {
+ logger.error("communication failed for topic {}", topic);
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ /*
+ * We don't want to build up items in our queue if we can't forward them
+ * to other hosts, so we just stop the controller.
+ *
+ * Use a background thread to prevent deadlocks.
+ */
+ new Thread() {
+ @Override
+ public void run() {
+ controller.stop();
+ latch.countDown();
+ }
+ }.start();
+
+ return latch;
+ }
+
+ @Override
+ public ScheduledFuture<?> schedule(long delayMs, StateTimerTask task) {
+ return scheduler.schedule(new TimerAction(task), delayMs, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleWithFixedDelay(long initialDelayMs, long delayMs, StateTimerTask task) {
+ return scheduler.scheduleWithFixedDelay(new TimerAction(task), initialDelayMs, delayMs, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void publishAdmin(Message msg) {
+ publish(Message.ADMIN, msg);
+ }
+
+ @Override
+ public void publish(String channel, Message msg) {
+ msg.setChannel(channel);
+
+ try {
+ // ensure it's valid before we send it
+ msg.checkValidity();
+
+ String txt = serializer.encodeMsg(msg);
+ dmaapMgr.publish(txt);
+
+ } catch (JsonProcessingException e) {
+ logger.error("failed to serialize message for topic {} channel {}", topic, channel, e);
+
+ } catch (PoolingFeatureException e) {
+ logger.error("failed to publish message for topic {} channel {}", topic, channel, e);
+ }
+ }
+
+ /**
+ * Handles an event from the internal topic.
+ *
+ * @param topic2
+ * @param event
+ * @return {@code true} if the event was handled, {@code false} if the
+ * controller should handle it
+ */
+ @Override
+ public void onTopicEvent(CommInfrastructure commType, String topic2, String event) {
+
+ if (event == null) {
+ logger.error("null event on topic {}", topic);
+ return;
+ }
+
+ synchronized (curLocker) {
+ // it's on the internal topic
+ handleInternal(event);
+ }
+ }
+
+ /**
+ * Called by the PolicyController before it offers the event to the
+ * DroolsController. If the controller is locked, then it isn't processing
+ * events. However, they still need to be forwarded, thus in that case, they
+ * are decoded and forwarded.
+ * <p>
+ * On the other hand, if the controller is not locked, then we just return
+ * immediately and let {@link #beforeInsert(Object, String, String, Object)
+ * beforeInsert()} handle it instead, as it already has the decoded message.
+ *
+ * @param protocol
+ * @param topic2
+ * @param event
+ * @return {@code true} if the event was handled by the manager,
+ * {@code false} if it must still be handled by the invoker
+ */
+ public boolean beforeOffer(CommInfrastructure protocol, String topic2, String event) {
+
+ if (!controller.isLocked() || !intercept) {
+ // we should NOT intercept this message - let the invoker handle it
+ return false;
+ }
+
+ return handleExternal(protocol, topic2, event, extractRequestId(decodeEvent(topic2, event)));
+ }
+
+ /**
+ * Called by the DroolsController before it inserts the event into the rule
+ * engine.
+ *
+ * @param protocol
+ * @param topic2
+ * @param event original event text, as received from the Bus
+ * @param event2 event, as an object
+ * @return {@code true} if the event was handled by the manager,
+ * {@code false} if it must still be handled by the invoker
+ */
+ public boolean beforeInsert(CommInfrastructure protocol, String topic2, String event, Object event2) {
+
+ if (!intercept) {
+ // we should NOT intercept this message - let the invoker handle it
+ return false;
+ }
+
+ return handleExternal(protocol, topic2, event, extractRequestId(event2));
+ }
+
+ /**
+ * Handles an event from an external topic.
+ *
+ * @param protocol
+ * @param topic2
+ * @param event
+ * @param reqid request id extracted from the event, or {@code null} if it
+ * couldn't be extracted
+ * @return {@code true} if the event was handled by the manager,
+ * {@code false} if it must still be handled by the invoker
+ */
+ private boolean handleExternal(CommInfrastructure protocol, String topic2, String event, String reqid) {
+ if (reqid == null) {
+ // no request id - let the invoker handle it
+ return false;
+ }
+
+ if (reqid.isEmpty()) {
+ logger.warn("handle locally due to empty request id for topic {}", topic2);
+ // no request id - let the invoker handle it
+ return false;
+ }
+
+ Forward ev = makeForward(protocol, topic2, event, reqid);
+ if (ev == null) {
+ // invalid args - consume the message
+ return true;
+ }
+
+ synchronized (curLocker) {
+ return handleExternal(ev);
+ }
+ }
+
+ /**
+ * Handles an event from an external topic.
+ *
+ * @param event
+ * @return {@code true} if the event was handled, {@code false} if the
+ * invoker should handle it
+ */
+ private boolean handleExternal(Forward event) {
+ if (assignments == null) {
+ // no bucket assignments yet - add it to the queue
+ eventq.add(event);
+
+ // we've consumed the event
+ return true;
+
+ } else {
+ return handleEvent(event);
+ }
+ }
+
+ /**
+ * Handles a {@link Forward} event, possibly forwarding it again.
+ *
+ * @param event
+ * @return {@code true} if the event was handled, {@code false} if the
+ * invoker should handle it
+ */
+ private boolean handleEvent(Forward event) {
+ int bucket = Math.abs(event.getRequestId().hashCode()) % assignments.size();
+ String target = assignments.getAssignedHost(bucket);
+
+ if (target == null) {
+ /*
+ * This bucket has no assignment - just discard the event
+ */
+ return true;
+ }
+
+ if (target.equals(host)) {
+ /*
+ * Message belongs to this host - allow the controller to handle it.
+ */
+ return false;
+ }
+
+ // forward to a different host, if hop count has been exhausted
+ if (event.getNumHops() > MAX_HOPS) {
+ logger.warn("message discarded - hop count {} exceeded {} for topic {}", event.getNumHops(), MAX_HOPS,
+ topic);
+
+ } else {
+ event.bumpNumHops();
+ publish(target, event);
+ }
+
+ // either way, consume the event
+ return true;
+ }
+
+ /**
+ * Extract the request id from an event object.
+ *
+ * @param event the event object, or {@code null}
+ * @return the event's request id, or {@code null} if it can't be extracted
+ */
+ private String extractRequestId(Object event) {
+ if (event == null) {
+ return null;
+ }
+
+ Object reqid = extractors.extract(event);
+ return (reqid != null ? reqid.toString() : null);
+ }
+
+ /**
+ * Decodes an event from a String into an event Object.
+ *
+ * @param topic2
+ * @param event
+ * @return the decoded event object, or {@code null} if it can't be decoded
+ */
+ private Object decodeEvent(String topic2, String event) {
+ DroolsController drools = controller.getDrools();
+
+ // check if this topic has a decoder
+
+ if (!factory.canDecodeEvent(drools, topic2)) {
+
+ logger.warn("{}: DECODING-UNSUPPORTED {}:{}:{}", drools, topic2, drools.getGroupId(),
+ drools.getArtifactId());
+ return null;
+ }
+
+ // decode
+
+ try {
+ return factory.decodeEvent(drools, topic2, event);
+
+ } catch (UnsupportedOperationException | IllegalStateException | IllegalArgumentException e) {
+ logger.debug("{}: DECODE FAILED: {} <- {} because of {}", drools, topic2, event, e.getMessage(), e);
+ return null;
+ }
+ }
+
+ /**
+ * Makes a {@link Forward}, and validates its contents.
+ *
+ * @param protocol
+ * @param topic2
+ * @param event
+ * @param reqid
+ * @return a new message, or {@code null} if the message was invalid
+ */
+ private Forward makeForward(CommInfrastructure protocol, String topic2, String event, String reqid) {
+ try {
+ Forward ev = new Forward(host, protocol, topic2, event, reqid);
+
+ // required for the validity check
+ ev.setChannel(host);
+
+ ev.checkValidity();
+
+ return ev;
+
+ } catch (PoolingFeatureException e) {
+ logger.error("invalid message for topic {}", topic2, e);
+ return null;
+ }
+ }
+
+ @Override
+ public void handle(Forward event) {
+ synchronized (curLocker) {
+ if (!handleExternal(event)) {
+ // this host should handle it - inject it
+ inject(event);
+ }
+ }
+ }
+
+ /**
+ * Injects an event into the controller.
+ *
+ * @param event
+ */
+ private void inject(Forward event) {
+ intercept = false;
+ listener.onTopicEvent(event.getProtocol(), event.getTopic(), event.getPayload());
+
+ intercept = true;
+ }
+
+ /**
+ * Handles an event from the internal topic. This uses reflection to
+ * identify the appropriate process() method to invoke, based on the type of
+ * Message that was decoded.
+ *
+ * @param event the serialized {@link Message} read from the internal topic
+ */
+ private void handleInternal(String event) {
+ Class<?> clazz = null;
+
+ try {
+ Message msg = serializer.decodeMsg(event);
+
+ // get the class BEFORE checking the validity
+ clazz = msg.getClass();
+
+ msg.checkValidity();
+
+ Method meth = current.getClass().getMethod("process", msg.getClass());
+ changeState((State) meth.invoke(current, msg));
+
+ } catch (IOException e) {
+ logger.warn("failed to decode message for topic {}", topic, e);
+
+ } catch (NoSuchMethodException | SecurityException e) {
+ logger.error("no processor for message {} for topic {}", clazz, topic, e);
+
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+ logger.error("failed to process message {} for topic {}", clazz, topic, e);
+
+ } catch (PoolingFeatureException e) {
+ logger.error("failed to process message {} for topic {}", clazz, topic, e);
+ }
+ }
+
+ @Override
+ public void startDistributing(BucketAssignments assignments) {
+ if (assignments == null) {
+ return;
+ }
+
+ synchronized (curLocker) {
+ this.assignments = assignments;
+
+ // now that we have assignments, we can process the queue
+ Forward ev;
+ while ((ev = eventq.poll()) != null) {
+ handle(ev);
+ }
+ }
+ }
+
+ @Override
+ public BucketAssignments getAssignments() {
+ return assignments;
+ }
+
+ @Override
+ public State goStart() {
+ return new StartState(this);
+ }
+
+ @Override
+ public State goQuery() {
+ return new QueryState(this);
+ }
+
+ @Override
+ public State goActive() {
+ return new ActiveState(this);
+ }
+
+ @Override
+ public State goInactive() {
+ return new InactiveState(this);
+ }
+
+ /**
+ * Action to run a timer task. Only runs the task if the machine is still in
+ * the state that it was in when the timer was created.
+ */
+ private class TimerAction implements Runnable {
+
+ /**
+ * State of the machine when the timer was created.
+ */
+ private State origState;
+
+ /**
+ * Task to be executed.
+ */
+ private StateTimerTask task;
+
+ /**
+ *
+ * @param task task to execute when this timer runs
+ */
+ public TimerAction(StateTimerTask task) {
+ this.origState = current;
+ this.task = task;
+ }
+
+ @Override
+ public void run() {
+ synchronized (curLocker) {
+ if (current == origState) {
+ changeState(task.fire(null));
+ }
+ }
+ }
+ }
+
+ /**
+ * Factory used to create objects.
+ */
+ public static class Factory {
+
+ /**
+ * Creates an event queue.
+ *
+ * @param props properties used to configure the event queue
+ * @return a new event queue
+ */
+ public EventQueue makeEventQueue(PoolingProperties props) {
+ return new EventQueue(props.getOfflineLimit(), props.getOfflineAgeMs());
+ }
+
+ /**
+ * Creates object extractors.
+ *
+ * @param props properties used to configure the extractors
+ * @return a new set of extractors
+ */
+ public ClassExtractors makeClassExtractors(Properties props) {
+ return new ClassExtractors(props, PROP_EXTRACTOR_PREFIX, EXTRACTOR_TYPE);
+ }
+
+ /**
+ * Creates a DMaaP manager.
+ *
+ * @param props properties used to configure the manager
+ * @return a new DMaaP manager
+ * @throws PoolingFeatureException if an error occurs
+ */
+ public DmaapManager makeDmaapManager(PoolingProperties props) throws PoolingFeatureException {
+ return new DmaapManager(props.getPoolingTopic(), props.getSource());
+ }
+
+ /**
+ * Creates a scheduled thread pool.
+ *
+ * @return a new scheduled thread pool
+ */
+ public ScheduledThreadPoolExecutor makeScheduler() {
+ return new ScheduledThreadPoolExecutor(1);
+ }
+
+ /**
+ * Determines if the event can be decoded.
+ *
+ * @param drools drools controller
+ * @param topic topic on which the event was received
+ * @return {@code true} if the event can be decoded, {@code false}
+ * otherwise
+ */
+ public boolean canDecodeEvent(DroolsController drools, String topic) {
+ return EventProtocolCoder.manager.isDecodingSupported(drools.getGroupId(), drools.getArtifactId(), topic);
+ }
+
+ /**
+ * Decodes the event.
+ *
+ * @param drools drools controller
+ * @param topic topic on which the event was received
+ * @param event event text to be decoded
+ * @return the decoded event
+ * @throws IllegalArgumentException
+ * @throw UnsupportedOperationException
+ * @throws IllegalStateException
+ */
+ public Object decodeEvent(DroolsController drools, String topic, String event) {
+ return EventProtocolCoder.manager.decode(drools.getGroupId(), drools.getArtifactId(), topic, event);
+ }
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java
new file mode 100644
index 00000000..1cbe5cb9
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java
@@ -0,0 +1,162 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import java.util.Properties;
+import org.onap.policy.common.utils.properties.SpecPropertyConfiguration;
+import org.onap.policy.common.utils.properties.exception.PropertyException;
+
+/**
+ * Properties used by the pooling feature, specific to a controller.
+ */
+public class PoolingProperties extends SpecPropertyConfiguration {
+
+ /**
+ * Feature properties all begin with this prefix.
+ */
+ public static final String PREFIX = "pooling.";
+
+ /*
+ * These properties REQUIRE a controller name, thus they use the "{$}" form.
+ */
+ public static final String POOLING_TOPIC = PREFIX + "{$}.topic";
+
+ /*
+ * These properties allow the controller name to be left out, thus they use
+ * the "{prefix?suffix}" form.
+ */
+ public static final String FEATURE_ENABLED = PREFIX + "{?.}enabled";
+ public static final String OFFLINE_LIMIT = PREFIX + "{?.}offline.queue.limit";
+ public static final String OFFLINE_AGE_MS = PREFIX + "{?.}offline.queue.age.milliseconds";
+ public static final String START_HEARTBEAT_MS = PREFIX + "{?.}start.heartbeat.milliseconds";
+ public static final String REACTIVATE_MS = PREFIX + "{?.}reactivate.milliseconds";
+ public static final String IDENTIFICATION_MS = PREFIX + "{?.}identification.milliseconds";
+ public static final String ACTIVE_HEARTBEAT_MS = PREFIX + "{?.}active.heartbeat.milliseconds";
+ public static final String INTER_HEARTBEAT_MS = PREFIX + "{?.}inter.heartbeat.milliseconds";
+
+ /**
+ * Properties from which this was constructed.
+ */
+ private Properties source;
+
+ /**
+ * Topic used for inter-host communication.
+ */
+ @Property(name = POOLING_TOPIC)
+ private String poolingTopic;
+
+ /**
+ * Maximum number of events to retain in the queue while waiting for
+ * buckets to be assigned.
+ */
+ @Property(name = OFFLINE_LIMIT, defaultValue = "1000")
+ private int offlineLimit;
+
+ /**
+ * Maximum age, in milliseconds, of events to be retained in the queue.
+ * Events older than this are discarded.
+ */
+ @Property(name = OFFLINE_AGE_MS, defaultValue = "60000")
+ private long offlineAgeMs;
+
+ /**
+ * Time, in milliseconds, to wait for this host's heart beat during the
+ * start-up state.
+ */
+ @Property(name = START_HEARTBEAT_MS, defaultValue = "50000")
+ private long startHeartbeatMs;
+
+ /**
+ * Time, in milliseconds, to wait before attempting to re-active this
+ * host when it has no bucket assignments.
+ */
+ @Property(name = REACTIVATE_MS, defaultValue = "50000")
+ private long reactivateMs;
+
+ /**
+ * Time, in milliseconds, to wait for all Identification messages to
+ * arrive during the query state.
+ */
+ @Property(name = IDENTIFICATION_MS, defaultValue = "50000")
+ private long identificationMs;
+
+ /**
+ * Time, in milliseconds, to wait for heart beats from this host, or its
+ * predecessor, during the active state.
+ */
+ @Property(name = ACTIVE_HEARTBEAT_MS, defaultValue = "50000")
+ private long activeHeartbeatMs;
+
+ /**
+ * Time, in milliseconds, to wait between heart beat generations during
+ * the active state.
+ */
+ @Property(name = INTER_HEARTBEAT_MS, defaultValue = "15000")
+ private long interHeartbeatMs;
+
+ /**
+ * @param controllerName the name of the controller
+ * @param props set of properties used to configure this
+ * @throws PropertyException if an error occurs
+ *
+ */
+ public PoolingProperties(String controllerName, Properties props) throws PropertyException {
+ super(controllerName, props);
+
+ source = props;
+ }
+
+ public Properties getSource() {
+ return source;
+ }
+
+ public String getPoolingTopic() {
+ return poolingTopic;
+ }
+
+ public int getOfflineLimit() {
+ return offlineLimit;
+ }
+
+ public long getOfflineAgeMs() {
+ return offlineAgeMs;
+ }
+
+ public long getStartHeartbeatMs() {
+ return startHeartbeatMs;
+ }
+
+ public long getReactivateMs() {
+ return reactivateMs;
+ }
+
+ public long getIdentificationMs() {
+ return identificationMs;
+ }
+
+ public long getActiveHeartbeatMs() {
+ return activeHeartbeatMs;
+ }
+
+ public long getInterHeartbeatMs() {
+ return interHeartbeatMs;
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/Serializer.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/Serializer.java
new file mode 100644
index 00000000..63aefb7a
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/Serializer.java
@@ -0,0 +1,79 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import java.io.IOException;
+import java.util.Map;
+import org.onap.policy.drools.pooling.message.Message;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Serialization helper functions.
+ */
+public class Serializer {
+
+ /**
+ * Used to encode & decode JSON messages sent & received, respectively, on
+ * the internal DMaaP topic.
+ */
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ /**
+ *
+ */
+ public Serializer() {
+ super();
+ }
+
+ /**
+ * Encodes a filter.
+ *
+ * @param filter filter to be encoded
+ * @return the filter, serialized as a JSON string
+ * @throws JsonProcessingException if it cannot be serialized
+ */
+ public String encodeFilter(Map<String, Object> filter) throws JsonProcessingException {
+ return mapper.writeValueAsString(filter);
+ }
+
+ /**
+ * Encodes a message.
+ *
+ * @param msg message to be encoded
+ * @return the message, serialized as a JSON string
+ * @throws JsonProcessingException if it cannot be serialized
+ */
+ public String encodeMsg(Message msg) throws JsonProcessingException {
+ return mapper.writeValueAsString(msg);
+ }
+
+ /**
+ * Decodes a JSON string into a Message.
+ *
+ * @param msg JSON string representing the message
+ * @return the message
+ * @throws IOException if it cannot be serialized
+ */
+ public Message decodeMsg(String msg) throws IOException {
+ return mapper.readValue(msg, Message.class);
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/SpecProperties.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/SpecProperties.java
new file mode 100644
index 00000000..e4557404
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/SpecProperties.java
@@ -0,0 +1,109 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import java.util.Properties;
+
+/**
+ * Properties with an optional specialization (e.g., session name, controller
+ * name).
+ */
+public class SpecProperties extends Properties {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The property prefix, ending with ".".
+ */
+ private final String prefix;
+
+ /**
+ * The specialized property prefix, ending with ".".
+ */
+ private final String specPrefix;
+
+ /**
+ *
+ * @param prefix the property name prefix that appears before any
+ * specialization
+ * @param specialization the property name specialization (e.g., session
+ * name)
+ */
+ public SpecProperties(String prefix, String specialization) {
+ this.prefix = withTrailingDot(prefix);
+ this.specPrefix = withTrailingDot(this.prefix + specialization);
+ }
+
+ /**
+ *
+ * @param prefix the property name prefix that appears before any
+ * specialization
+ * @param specialization the property name specialization (e.g., session
+ * name)
+ * @param props the default properties
+ */
+ public SpecProperties(String prefix, String specialization, Properties props) {
+ super(props);
+
+ this.prefix = withTrailingDot(prefix);
+ this.specPrefix = withTrailingDot(this.prefix + specialization);
+ }
+
+ /**
+ * Adds a trailing "." to a String, if it doesn't already have one.
+ *
+ * @param text text to which the "." should be added
+ * @return the text, with a trailing "."
+ */
+ private static String withTrailingDot(String text) {
+ return text.endsWith(".") ? text : text + ".";
+ }
+
+ /**
+ * Gets the property whose value has the given key, looking first for the
+ * specialized property name, and then for the generalized property name.
+ *
+ * @param key property name, without the specialization
+ * @return the value from the property set, or {@code null} if the property
+ * set does not contain the value
+ */
+ public String getProperty(String key) {
+ if (!key.startsWith(prefix)) {
+ return super.getProperty(key);
+ }
+
+ String suffix = key.substring(prefix.length());
+
+ String val = super.getProperty(specPrefix + suffix);
+ if (val != null) {
+ return val;
+ }
+
+ return super.getProperty(key);
+ }
+
+ protected String getPrefix() {
+ return prefix;
+ }
+
+ protected String getSpecPrefix() {
+ return specPrefix;
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ClassExtractors.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ClassExtractors.java
new file mode 100644
index 00000000..cb12a6ac
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ClassExtractors.java
@@ -0,0 +1,466 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.lang.StringUtils;
+import org.onap.policy.drools.utils.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Extractors for each object class. Properties define how the data is to be
+ * extracted for a given class, where the properties are similar to the
+ * following:
+ *
+ * <pre>
+ * <code>&lt;a.prefix>.&lt;class.name> = ${event.reqid}</code>
+ * </pre>
+ *
+ * If it doesn't find a property for the class, then it looks for a property for
+ * that class' super class or interfaces. Extractors are compiled and cached.
+ */
+public class ClassExtractors {
+
+ private static final Logger logger = LoggerFactory.getLogger(ClassExtractors.class);
+
+ /**
+ * Properties that specify how the data is to be extracted from a given
+ * class.
+ */
+ private final Properties properties;
+
+ /**
+ * Property prefix, including a trailing ".".
+ */
+ private final String prefix;
+
+ /**
+ * Type of item to be extracted.
+ */
+ private final String type;
+
+ /**
+ * Maps the class name to its extractor.
+ */
+ private final ConcurrentHashMap<String, Extractor> class2extractor = new ConcurrentHashMap<>();
+
+ /**
+ *
+ * @param props properties that specify how the data is to be extracted from
+ * a given class
+ * @param prefix property name prefix, prepended before the class name
+ * @param type type of item to be extracted
+ */
+ public ClassExtractors(Properties props, String prefix, String type) {
+ this.properties = props;
+ this.prefix = (prefix.endsWith(".") ? prefix : prefix + ".");
+ this.type = type;
+ }
+
+ /**
+ * Gets the number of extractors in the map.
+ *
+ * @return gets the number of extractors in the map
+ */
+ protected int size() {
+ return class2extractor.size();
+ }
+
+ /**
+ * Extracts the desired data item from an object.
+ *
+ * @param object object from which to extract the data item
+ * @return the extracted item, or {@code null} if it could not be extracted
+ */
+ public Object extract(Object object) {
+ if (object == null) {
+ return null;
+ }
+
+ Extractor ext = getExtractor(object);
+
+ return ext.extract(object);
+ }
+
+ /**
+ * Gets the extractor for the given type of object, creating one if it
+ * doesn't exist yet.
+ *
+ * @param object object whose extracted is desired
+ * @return an extractor for the object
+ */
+ private Extractor getExtractor(Object object) {
+ Class<?> clazz = object.getClass();
+ Extractor ext = class2extractor.get(clazz.getName());
+
+ if (ext == null) {
+ // allocate a new extractor, if another thread doesn't beat us to it
+ ext = class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz));
+ }
+
+ return ext;
+ }
+
+ /**
+ * Builds an extractor for the class.
+ *
+ * @param clazz class for which the extractor should be built
+ *
+ * @return a new extractor
+ */
+ private Extractor buildExtractor(Class<?> clazz) {
+ String value = properties.getProperty(prefix + clazz.getName(), null);
+ if (value != null) {
+ // property has config info for this class - build the extractor
+ return buildExtractor(clazz, value);
+ }
+
+ /*
+ * Get the extractor, if any, for the super class or interfaces, but
+ * don't add one if it doesn't exist
+ */
+ Extractor ext = getClassExtractor(clazz, false);
+ if (ext != null) {
+ return ext;
+ }
+
+ /*
+ * No extractor defined for for this class or its super class - we
+ * cannot extract data items from objects of this type, so just
+ * allocated a null extractor.
+ */
+ logger.warn("missing property " + prefix + clazz.getName());
+ return new NullExtractor();
+ }
+
+ /**
+ * Builds an extractor for the class, based on the config value extracted
+ * from the corresponding property.
+ *
+ * @param clazz class for which the extractor should be built
+ * @param value config value (e.g., "${event.request.id}"
+ * @return a new extractor
+ */
+ private Extractor buildExtractor(Class<?> clazz, String value) {
+ if (!value.startsWith("${")) {
+ logger.warn("property value for " + prefix + clazz.getName() + " does not start with '${'");
+ return new NullExtractor();
+ }
+
+ if (!value.endsWith("}")) {
+ logger.warn("property value for " + prefix + clazz.getName() + " does not end with '}'");
+ return new NullExtractor();
+ }
+
+ // get the part in the middle
+ String val = value.substring(2, value.length() - 1);
+ if (val.startsWith(".")) {
+ logger.warn("property value for " + prefix + clazz.getName() + " begins with '.'");
+ return new NullExtractor();
+ }
+
+ if (val.endsWith(".")) {
+ logger.warn("property value for " + prefix + clazz.getName() + " ends with '.'");
+ return new NullExtractor();
+ }
+
+ // everything's valid - create the extractor
+ try {
+ ComponetizedExtractor ext = new ComponetizedExtractor(clazz, val.split("[.]"));
+
+ /*
+ * If there's only one extractor, then just return it, otherwise
+ * return the whole extractor.
+ */
+ return (ext.extractors.length == 1 ? ext.extractors[0] : ext);
+
+ } catch (ExtractorException e) {
+ logger.warn("cannot build extractor for " + clazz.getName());
+ return new NullExtractor();
+ }
+ }
+
+ /**
+ * Gets the extractor for a class, examining all super classes and
+ * interfaces.
+ *
+ * @param clazz class whose extractor is desired
+ * @param addOk {@code true} if the extractor may be added, provided the
+ * property is defined, {@code false} otherwise
+ * @return the extractor to be used for the class, or {@code null} if no
+ * extractor has been defined yet
+ */
+ private Extractor getClassExtractor(Class<?> clazz, boolean addOk) {
+ if (clazz == null) {
+ return null;
+ }
+
+ Extractor ext = null;
+
+ if (addOk) {
+ String val = properties.getProperty(prefix + clazz.getName(), null);
+
+ if (val != null) {
+ /*
+ * A property is defined for this class, so create the extractor
+ * for it.
+ */
+ return class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz));
+ }
+ }
+
+ // see if the superclass has an extractor
+ if ((ext = getClassExtractor(clazz.getSuperclass(), true)) != null) {
+ return ext;
+ }
+
+ // check the interfaces, too
+ for (Class<?> clz : clazz.getInterfaces()) {
+ if ((ext = getClassExtractor(clz, true)) != null) {
+ break;
+ }
+ }
+
+ return ext;
+ }
+
+ /**
+ * Extractor that always returns {@code null}. Used when no extractor could
+ * be built for a given object type.
+ */
+ private class NullExtractor implements Extractor {
+
+ @Override
+ public Object extract(Object object) {
+ logger.warn("cannot extract " + type + " from " + object.getClass());
+ return null;
+ }
+ }
+
+ /**
+ * Component-ized extractor. Extracts an object that is referenced
+ * hierarchically, where each name identifies a particular component within
+ * the hierarchy. Supports retrieval from {@link Map} objects, as well as
+ * via getXxx() methods, or by direct field retrieval.
+ * <p>
+ * Note: this will <i>not</i> work if POJOs are contained within a Map.
+ */
+ private class ComponetizedExtractor implements Extractor {
+
+ /**
+ * Extractor for each component.
+ */
+ private final Extractor[] extractors;
+
+ /**
+ *
+ * @param clazz the class associated with the object at the root of the
+ * hierarchy
+ * @param names name associated with each component
+ * @throws ExtractorException
+ */
+ public ComponetizedExtractor(Class<?> clazz, String[] names) throws ExtractorException {
+ this.extractors = new Extractor[names.length];
+
+ Class<?> clz = clazz;
+
+ for (int x = 0; x < names.length; ++x) {
+ String comp = names[x];
+
+ Pair<Extractor, Class<?>> pair = buildExtractor(clz, comp);
+
+ extractors[x] = pair.first();
+ clz = pair.second();
+ }
+ }
+
+ /**
+ * Builds an extractor for the given component of an object.
+ *
+ * @param clazz type of object from which the component will be
+ * extracted
+ * @param comp name of the component to extract
+ * @return a pair containing the extractor and the extracted object's
+ * type
+ * @throws ExtractorException
+ */
+ private Pair<Extractor, Class<?>> buildExtractor(Class<?> clazz, String comp) throws ExtractorException {
+ Pair<Extractor, Class<?>> pair = null;
+
+ if (pair == null) {
+ pair = getMethodExtractor(clazz, comp);
+ }
+
+ if (pair == null) {
+ pair = getFieldExtractor(clazz, comp);
+ }
+
+ if (pair == null) {
+ pair = getMapExtractor(clazz, comp);
+ }
+
+
+ // didn't find an extractor
+ if (pair == null) {
+ throw new ExtractorException("class " + clazz + " contains no element " + comp);
+ }
+
+ return pair;
+ }
+
+ @Override
+ public Object extract(Object object) {
+ Object obj = object;
+
+ for (Extractor ext : extractors) {
+ if (obj == null) {
+ break;
+ }
+
+ obj = ext.extract(obj);
+ }
+
+ return obj;
+ }
+
+ /**
+ * Gets an extractor that invokes a getXxx() method to retrieve the
+ * object.
+ *
+ * @param clazz container's class
+ * @param name name of the property to be retrieved
+ * @return a new extractor, or {@code null} if the class does not
+ * contain the corresponding getXxx() method
+ * @throws ExtractorException if the getXxx() method is inaccessible
+ */
+ private Pair<Extractor, Class<?>> getMethodExtractor(Class<?> clazz, String name) throws ExtractorException {
+ Method meth;
+
+ String nm = "get" + StringUtils.capitalize(name);
+
+ try {
+ meth = clazz.getMethod(nm);
+
+ Class<?> retType = meth.getReturnType();
+ if (retType == void.class) {
+ // it's a void method, thus it won't return an object
+ return null;
+ }
+
+ return new Pair<>(new MethodExtractor(meth), retType);
+
+ } catch (NoSuchMethodException expected) {
+ // no getXxx() method, maybe there's a field by this name
+ return null;
+
+ } catch (SecurityException e) {
+ throw new ExtractorException("inaccessible method " + clazz + "." + nm, e);
+ }
+ }
+
+ /**
+ * Gets an extractor for a field within the object.
+ *
+ * @param clazz container's class
+ * @param name name of the field whose value is to be extracted
+ * @return a new extractor, or {@code null} if the class does not
+ * contain the given field
+ * @throws ExtractorException if the field is inaccessible
+ */
+ private Pair<Extractor, Class<?>> getFieldExtractor(Class<?> clazz, String name) throws ExtractorException {
+
+ Field field = getClassField(clazz, name);
+ if (field == null) {
+ return null;
+ }
+
+ return new Pair<>(new FieldExtractor(field), field.getType());
+ }
+
+ /**
+ * Gets an extractor for an item within a Map object.
+ *
+ * @param clazz container's class
+ * @param key item key within the map
+ * @return a new extractor, or {@code null} if the class is not a Map
+ * subclass
+ * @throws ExtractorException
+ */
+ private Pair<Extractor, Class<?>> getMapExtractor(Class<?> clazz, String key) throws ExtractorException {
+
+ if (!Map.class.isAssignableFrom(clazz)) {
+ return null;
+ }
+
+ /*
+ * Don't know the value's actual type, so we'll assume it's a Map
+ * for now. Things should still work OK, as this is only used to
+ * direct the constructor on what type of extractor to create next.
+ * If the object turns out not to be a map, then the MapExtractor
+ * for the next component will just return null.
+ */
+ return new Pair<>(new MapExtractor(key), Map.class);
+ }
+
+ /**
+ * Gets field within a class, examining all super classes and
+ * interfaces.
+ *
+ * @param clazz class whose field is desired
+ * @param name name of the desired field
+ * @return the field within the class, or {@code null} if the field does
+ * not exist
+ * @throws ExtractorException if the field is inaccessible
+ */
+ private Field getClassField(Class<?> clazz, String name) throws ExtractorException {
+ if (clazz == null) {
+ return null;
+ }
+
+ try {
+ return clazz.getDeclaredField(name);
+
+ } catch (NoSuchFieldException expected) {
+ // no field by this name - try super class & interfaces
+
+ } catch (SecurityException e) {
+ throw new ExtractorException("inaccessible field " + clazz + "." + name, e);
+ }
+
+
+ Field field;
+
+ // see if the superclass has an extractor
+ if ((field = getClassField(clazz.getSuperclass(), name)) != null) {
+ return field;
+ }
+
+ // not necessary to check the interfaces
+
+ return field;
+ }
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/Extractor.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/Extractor.java
new file mode 100644
index 00000000..9ed32ae4
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/Extractor.java
@@ -0,0 +1,35 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+/**
+ * Used to extract an object contained within another object.
+ */
+public interface Extractor {
+
+ /**
+ * Extracts an object contained within another object.
+ *
+ * @param object object from which to extract the contained object
+ * @return the extracted value, or {@code null} if it cannot be extracted
+ */
+ public Object extract(Object object);
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ExtractorException.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ExtractorException.java
new file mode 100644
index 00000000..a864672b
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ExtractorException.java
@@ -0,0 +1,49 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+/**
+ * Exception generated by extractors.
+ */
+public class ExtractorException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public ExtractorException() {
+ super();
+ }
+
+ public ExtractorException(String message) {
+ super(message);
+ }
+
+ public ExtractorException(Throwable cause) {
+ super(cause);
+ }
+
+ public ExtractorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ExtractorException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/FieldExtractor.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/FieldExtractor.java
new file mode 100644
index 00000000..132b8ed0
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/FieldExtractor.java
@@ -0,0 +1,59 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+import java.lang.reflect.Field;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Used to extract an object stored in one of the container's fields.
+ */
+public class FieldExtractor implements Extractor {
+
+ private static final Logger logger = LoggerFactory.getLogger(FieldExtractor.class);
+
+ /**
+ * Field containing the object.
+ */
+ private final Field field;
+
+ /**
+ *
+ * @param field field containing the object
+ */
+ public FieldExtractor(Field field) {
+ this.field = field;
+
+ field.setAccessible(true);
+ }
+
+ @Override
+ public Object extract(Object object) {
+ try {
+ return field.get(object);
+
+ } catch (IllegalAccessException | IllegalArgumentException e) {
+ logger.warn("cannot get {} from {}", field.getName(), object.getClass(), e);
+ return null;
+ }
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MapExtractor.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MapExtractor.java
new file mode 100644
index 00000000..aff9d860
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MapExtractor.java
@@ -0,0 +1,60 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Used to extract an object stored in a map.
+ */
+public class MapExtractor implements Extractor {
+
+ private static final Logger logger = LoggerFactory.getLogger(MapExtractor.class);
+
+ /**
+ * Key to the item to extract from the map.
+ */
+ private final String key;
+
+ /**
+ *
+ * @param key key to the item to extract from the map
+ */
+ public MapExtractor(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public Object extract(Object object) {
+
+ if (object instanceof Map) {
+ Map<?, ?> map = (Map<?, ?>) object;
+
+ return map.get(key);
+
+ } else {
+ logger.warn("expecting a map, instead of " + object.getClass());
+ return null;
+ }
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MethodExtractor.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MethodExtractor.java
new file mode 100644
index 00000000..20c4a1a7
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MethodExtractor.java
@@ -0,0 +1,58 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Used to extract an object by invoking a method on the container.
+ */
+public class MethodExtractor implements Extractor {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodExtractor.class);
+
+ /**
+ * Method to invoke to extract the contained object.
+ */
+ private final Method method;
+
+ /**
+ *
+ * @param method method to invoke to extract the contained object
+ */
+ public MethodExtractor(Method method) {
+ this.method = method;
+ }
+
+ @Override
+ public Object extract(Object object) {
+ try {
+ return method.invoke(object);
+
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+ logger.warn("cannot invoke {} on {}", method.getName(), object.getClass(), e);
+ return null;
+ }
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java
new file mode 100644
index 00000000..8fd86c1e
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java
@@ -0,0 +1,215 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import org.onap.policy.drools.pooling.PoolingFeatureException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Bucket assignments, which is simply an array of host names.
+ */
+public class BucketAssignments {
+
+ @JsonIgnore
+ private static final Logger logger = LoggerFactory.getLogger(BucketAssignments.class);
+
+ /**
+ * Number of buckets.
+ */
+ public static final int MAX_BUCKETS = 1024;
+
+ /**
+ * Identifies the host serving a particular bucket.
+ */
+ private String[] hostArray = null;
+
+ /**
+ *
+ */
+ public BucketAssignments() {
+ super();
+ }
+
+ /**
+ *
+ * @param hostArray maps a bucket number (i.e., array index) to a host. All
+ * values must be non-null
+ */
+ public BucketAssignments(String[] hostArray) {
+ this.hostArray = hostArray;
+ }
+
+ public String[] getHostArray() {
+ return hostArray;
+ }
+
+ public void setHostArray(String[] hostArray) {
+ this.hostArray = hostArray;
+ }
+
+ /**
+ * Gets the leader, which is the host with the minimum UUID.
+ *
+ * @return the assignment leader
+ */
+ @JsonIgnore
+ public String getLeader() {
+ if (hostArray == null) {
+ return null;
+ }
+
+ String leader = null;
+
+ for (String host : hostArray) {
+ if (host != null && (leader == null || host.compareTo(leader) < 0)) {
+ leader = host;
+ }
+ }
+
+ return leader;
+
+ }
+
+ /**
+ * Determines if a host has an assignment.
+ *
+ * @param host host to be checked
+ * @return {@code true} if the host has an assignment, {@code false}
+ * otherwise
+ */
+ @JsonIgnore
+ public boolean hasAssignment(String host) {
+ if (hostArray == null) {
+ return false;
+ }
+
+ for (String host2 : hostArray) {
+ if (host.equals(host2)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Gets all of the hosts that have an assignment.
+ *
+ * @return all of the hosts that have an assignment
+ */
+ @JsonIgnore
+ public Set<String> getAllHosts() {
+ Set<String> set = new HashSet<>();
+ if (hostArray == null) {
+ return set;
+ }
+
+ for (String host : hostArray) {
+ if (host != null) {
+ set.add(host);
+ }
+ }
+
+ return set;
+ }
+
+ /**
+ * Gets the host assigned to a given bucket.
+ *
+ * @param bucket bucket number
+ * @return the assigned host, or {@code null} if the bucket has no assigned
+ * host
+ */
+ @JsonIgnore
+ public String getAssignedHost(int bucket) {
+ if (hostArray == null) {
+ logger.error("no buckets have been assigned");
+ return null;
+ }
+
+ if (bucket < 0 || bucket >= hostArray.length) {
+ logger.error("invalid bucket number {} maximum {}", bucket, hostArray.length);
+ return null;
+ }
+
+ return hostArray[bucket];
+ }
+
+ /**
+ * Gets the number of buckets.
+ *
+ * @return the number of buckets
+ */
+ @JsonIgnore
+ public int size() {
+ return (hostArray != null ? hostArray.length : 0);
+ }
+
+ /**
+ * Checks the validity of the assignments, verifying that all buckets have
+ * been assigned to a host.
+ *
+ * @throws PoolingFeatureException if the assignments are invalid
+ */
+ @JsonIgnore
+ public void checkValidity() throws PoolingFeatureException {
+ if (hostArray == null || hostArray.length == 0) {
+ throw new PoolingFeatureException("missing hosts in message bucket assignments");
+ }
+
+ if (hostArray.length > MAX_BUCKETS) {
+ throw new PoolingFeatureException("too many hosts in message bucket assignments");
+ }
+
+ for (int x = 0; x < hostArray.length; ++x) {
+ if (hostArray[x] == null) {
+ throw new PoolingFeatureException("bucket " + x + " has no assignment");
+ }
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Arrays.hashCode(hostArray);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ BucketAssignments other = (BucketAssignments) obj;
+ if (!Arrays.equals(hostArray, other.hostArray))
+ return false;
+ return true;
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Forward.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Forward.java
new file mode 100644
index 00000000..b59cfbb2
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Forward.java
@@ -0,0 +1,180 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import org.onap.policy.drools.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.drools.pooling.PoolingFeatureException;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Message to forward an event to another host.
+ */
+public class Forward extends Message {
+
+ /**
+ * Number of hops (i.e., number of times it's been forwarded) so far.
+ */
+ private int numHops;
+
+ /**
+ * Time, in milliseconds, at which the message was created.
+ */
+ private long createTimeMs;
+
+ /**
+ * Protocol of the receiving topic.
+ */
+ private CommInfrastructure protocol;
+
+ /**
+ * Topic on which the event was received.
+ */
+ private String topic;
+
+ /**
+ * The event pay load that was received on the topic.
+ */
+ private String payload;
+
+ /**
+ * The request id that was extracted from the event.
+ */
+ private String requestId;
+
+ /**
+ *
+ */
+ public Forward() {
+ super();
+ }
+
+ /**
+ *
+ * @param source host on which the message originated
+ * @param protocol
+ * @param topic
+ * @param payload the actual event data received on the topic
+ * @param requestId
+ */
+ public Forward(String source, CommInfrastructure protocol, String topic, String payload, String requestId) {
+ super(source);
+
+ this.numHops = 0;
+ this.createTimeMs = System.currentTimeMillis();
+ this.protocol = protocol;
+ this.topic = topic;
+ this.payload = payload;
+ this.requestId = requestId;
+ }
+
+ /**
+ * Increments {@link #numHops}.
+ */
+ public void bumpNumHops() {
+ ++numHops;
+ }
+
+ public int getNumHops() {
+ return numHops;
+ }
+
+ public void setNumHops(int numHops) {
+ this.numHops = numHops;
+ }
+
+ public long getCreateTimeMs() {
+ return createTimeMs;
+ }
+
+ public void setCreateTimeMs(long createTimeMs) {
+ this.createTimeMs = createTimeMs;
+ }
+
+ public CommInfrastructure getProtocol() {
+ return protocol;
+ }
+
+ public void setProtocol(CommInfrastructure protocol) {
+ this.protocol = protocol;
+ }
+
+ public String getTopic() {
+ return topic;
+ }
+
+ public void setTopic(String topic) {
+ this.topic = topic;
+ }
+
+ public String getPayload() {
+ return payload;
+ }
+
+ public void setPayload(String payload) {
+ this.payload = payload;
+ }
+
+ public String getRequestId() {
+ return requestId;
+ }
+
+ public void setRequestId(String requestId) {
+ this.requestId = requestId;
+ }
+
+ @JsonIgnore
+ public boolean isExpired(long minCreateTimeMs) {
+ return (createTimeMs < minCreateTimeMs);
+
+ }
+
+ @JsonIgnore
+ public void checkValidity() throws PoolingFeatureException {
+
+ super.checkValidity();
+
+ if (protocol == null) {
+ throw new PoolingFeatureException("missing message protocol");
+ }
+
+ if (topic == null || topic.isEmpty()) {
+ throw new PoolingFeatureException("missing message topic");
+ }
+
+ /*
+ * Note: an empty pay load is OK, as an empty message could have been
+ * received on the topic.
+ */
+
+ if (payload == null) {
+ throw new PoolingFeatureException("missing message payload");
+ }
+
+ if (requestId == null || requestId.isEmpty()) {
+ throw new PoolingFeatureException("missing message requestId");
+ }
+
+ if (numHops < 0) {
+ throw new PoolingFeatureException("invalid message hop count");
+ }
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java
new file mode 100644
index 00000000..2a63a5be
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java
@@ -0,0 +1,60 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+/**
+ * Heart beat message sent to self, or to the succeeding host.
+ */
+public class Heartbeat extends Message {
+
+ /**
+ * Time, in milliseconds, when this was created.
+ */
+ private long timestampMs;
+
+ /**
+ *
+ */
+ public Heartbeat() {
+ super();
+
+ }
+
+ /**
+ *
+ * @param source host on which the message originated
+ * @param timestampMs time, in milliseconds, associated with the message
+ */
+ public Heartbeat(String source, long timestampMs) {
+ super(source);
+
+ this.timestampMs = timestampMs;
+ }
+
+ public long getTimestampMs() {
+ return timestampMs;
+ }
+
+ public void setTimestampMs(long timestampMs) {
+ this.timestampMs = timestampMs;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Identification.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Identification.java
new file mode 100644
index 00000000..5de6b8f9
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Identification.java
@@ -0,0 +1,45 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+/**
+ * Identifies the source host and the bucket assignments which it knows about.
+ */
+public class Identification extends MessageWithAssignments {
+
+ /**
+ *
+ */
+ public Identification() {
+ super();
+
+ }
+
+ /**
+ *
+ * @param source host on which the message originated
+ * @param assignments
+ */
+ public Identification(String source, BucketAssignments assignments) {
+ super(source, assignments);
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Leader.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Leader.java
new file mode 100644
index 00000000..0fc48c3c
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Leader.java
@@ -0,0 +1,75 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import org.onap.policy.drools.pooling.PoolingFeatureException;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Indicates that the "source" of this message is now the "lead" host.
+ */
+public class Leader extends MessageWithAssignments {
+
+ /**
+ *
+ */
+ public Leader() {
+ super();
+ }
+
+ /**
+ *
+ * @param source host on which the message originated
+ * @param assignments
+ */
+ public Leader(String source, BucketAssignments assignments) {
+ super(source, assignments);
+ }
+
+ /**
+ * Also verifies that buckets have been assigned and that the source is
+ * indeed the leader.
+ */
+ @Override
+ @JsonIgnore
+ public void checkValidity() throws PoolingFeatureException {
+
+ super.checkValidity();
+
+ BucketAssignments assignments = getAssignments();
+ if (assignments == null) {
+ throw new PoolingFeatureException("missing message bucket assignments");
+ }
+
+ String leader = getSource();
+
+ if(!assignments.hasAssignment(leader)) {
+ throw new PoolingFeatureException("leader " + leader + " has no bucket assignments");
+ }
+
+ for (String host : assignments.getHostArray()) {
+ if (host.compareTo(leader) < 0) {
+ throw new PoolingFeatureException("invalid leader " + leader + ", should be " + host);
+ }
+ }
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Message.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Message.java
new file mode 100644
index 00000000..e8a4671d
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Message.java
@@ -0,0 +1,103 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import org.onap.policy.drools.pooling.PoolingFeatureException;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * Messages sent on the internal topic.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
+@JsonSubTypes({@Type(value = Forward.class, name = "forward"), @Type(value = Heartbeat.class, name = "heartbeat"),
+ @Type(value = Identification.class, name = "identification"),
+ @Type(value = Leader.class, name = "leader"), @Type(value = Offline.class, name = "offline"),
+ @Type(value = Query.class, name = "query")})
+public class Message {
+
+ /**
+ * Name of the administrative channel.
+ */
+ public static final String ADMIN = "_admin";
+
+ /**
+ * Host that originated the message.
+ */
+ private String source;
+
+ /**
+ * Channel on which the message is routed, which is either the target host
+ * or {@link #ADMIN}.
+ */
+ private String channel;
+
+ /**
+ *
+ */
+ public Message() {
+ super();
+ }
+
+ /**
+ *
+ * @param source host on which the message originated
+ */
+ public Message(String source) {
+ this.source = source;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ }
+
+ public String getChannel() {
+ return channel;
+ }
+
+ public void setChannel(String channel) {
+ this.channel = channel;
+ }
+
+ /**
+ * Checks the validity of the message, including verifying that required
+ * fields are not missing.
+ *
+ * @throws PoolingFeatureException if the message is invalid
+ */
+ @JsonIgnore
+ public void checkValidity() throws PoolingFeatureException {
+ if (source == null || source.isEmpty()) {
+ throw new PoolingFeatureException("missing message source");
+ }
+
+ if (channel == null || channel.isEmpty()) {
+ throw new PoolingFeatureException("missing message channel");
+ }
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java
new file mode 100644
index 00000000..9fded815
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java
@@ -0,0 +1,77 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import org.onap.policy.drools.pooling.PoolingFeatureException;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * A Message that includes bucket assignments.
+ */
+public class MessageWithAssignments extends Message {
+
+ /**
+ * Bucket assignments, as known by the source host.
+ */
+ private BucketAssignments assignments;
+
+ /**
+ *
+ */
+ public MessageWithAssignments() {
+ super();
+ }
+
+ /**
+ *
+ * @param source host on which the message originated
+ * @param assignments
+ */
+ public MessageWithAssignments(String source, BucketAssignments assignments) {
+ super(source);
+
+ this.assignments = assignments;
+
+ }
+
+ public BucketAssignments getAssignments() {
+ return assignments;
+ }
+
+ public void setAssignments(BucketAssignments assignments) {
+ this.assignments = assignments;
+ }
+
+ /**
+ * If there are any assignments, it verifies there validity.
+ */
+ @Override
+ @JsonIgnore
+ public void checkValidity() throws PoolingFeatureException {
+
+ super.checkValidity();
+
+ if (assignments != null) {
+ assignments.checkValidity();
+ }
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Offline.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Offline.java
new file mode 100644
index 00000000..297671ac
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Offline.java
@@ -0,0 +1,45 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+/**
+ * Indicates that the source host is going offline and will be unable to process
+ * any further requests.
+ */
+public class Offline extends Message {
+
+ /**
+ *
+ */
+ public Offline() {
+ super();
+
+ }
+
+ /**
+ *
+ * @param source host on which the message originated
+ */
+ public Offline(String source) {
+ super(source);
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Query.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Query.java
new file mode 100644
index 00000000..c995a288
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Query.java
@@ -0,0 +1,44 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+/**
+ * Query the other hosts for their identification.
+ */
+public class Query extends Message {
+
+ /**
+ *
+ */
+ public Query() {
+ super();
+
+ }
+
+ /**
+ *
+ * @param source host on which the message originated
+ */
+ public Query(String source) {
+ super(source);
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java
new file mode 100644
index 00000000..5f503a3b
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java
@@ -0,0 +1,255 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import java.util.Arrays;
+import java.util.TreeSet;
+import org.onap.policy.drools.pooling.PoolingManager;
+import org.onap.policy.drools.pooling.message.Heartbeat;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.message.Query;
+
+/**
+ * The active state. In this state, this host has one more more bucket
+ * assignments and processes any events associated with one of its buckets.
+ * Other events are forwarded to appropriate target hosts.
+ */
+public class ActiveState extends ProcessingState {
+
+ /**
+ * Set of hosts that have been assigned a bucket.
+ */
+ private final TreeSet<String> assigned = new TreeSet<>();
+
+ /**
+ * Host that comes after this host, or {@code null} if it has no successor.
+ */
+ private String succHost = null;
+
+ /**
+ * Host that comes before this host, or "" if it has no predecessor.
+ */
+ private String predHost = "";
+
+ /**
+ * {@code True} if we saw this host's heart beat since the last check,
+ * {@code false} otherwise.
+ */
+ private boolean myHeartbeatSeen = false;
+
+ /**
+ * {@code True} if we saw the predecessor's heart beat since the last check,
+ * {@code false} otherwise.
+ */
+ private boolean predHeartbeatSeen = false;
+
+ /**
+ *
+ * @param mgr
+ */
+ public ActiveState(PoolingManager mgr) {
+ super(mgr, mgr.getAssignments().getLeader());
+
+ assigned.addAll(Arrays.asList(mgr.getAssignments().getHostArray()));
+
+ detmNeighbors();
+ }
+
+ /**
+ * Determine this host's neighbors based on the order of the host UUIDs.
+ * Updates {@link #succHost} and {@link #predHost}.
+ */
+ private void detmNeighbors() {
+ if (assigned.size() < 2) {
+ /*
+ * this host is the only one with any assignments - it has no
+ * neighbors
+ */
+ succHost = null;
+ predHost = "";
+ return;
+ }
+
+ if ((succHost = assigned.higher(getHost())) == null) {
+ // wrapped around - successor is the first host in the set
+ succHost = assigned.first();
+ }
+
+ if ((predHost = assigned.lower(getHost())) == null) {
+ // wrapped around - predecessor is the last host in the set
+ predHost = assigned.last();
+ }
+ }
+
+ @Override
+ public void start() {
+ addTimers();
+ genHeartbeat();
+ }
+
+ /**
+ * Adds the timers.
+ */
+ private void addTimers() {
+
+ /*
+ * heart beat generator
+ */
+ long genMs = getProperties().getActiveHeartbeatMs();
+
+ scheduleWithFixedDelay(genMs, genMs, xxx -> {
+ genHeartbeat();
+ return null;
+ });
+
+ /*
+ * my heart beat checker
+ */
+ long interMs = getProperties().getInterHeartbeatMs();
+
+ scheduleWithFixedDelay(interMs, interMs, xxx -> {
+ if (myHeartbeatSeen) {
+ myHeartbeatSeen = false;
+ return null;
+ }
+
+ // missed my heart beat
+
+ return internalTopicFailed();
+ });
+
+ /*
+ * predecessor heart beat checker
+ */
+ if (!predHost.isEmpty()) {
+
+ scheduleWithFixedDelay(interMs, interMs, xxx -> {
+ if (predHeartbeatSeen) {
+ predHeartbeatSeen = false;
+ return null;
+ }
+
+ // missed the predecessor's heart beat
+ publish(makeQuery());
+
+ return goQuery();
+ });
+ }
+ }
+
+ /**
+ * Generates a heart beat for this host and its successor.
+ */
+ private void genHeartbeat() {
+ Heartbeat msg = makeHeartbeat(System.currentTimeMillis());
+ publish(getHost(), msg);
+
+ if (succHost != null) {
+ publish(succHost, msg);
+ }
+ }
+
+ @Override
+ public State process(Heartbeat msg) {
+ String src = msg.getSource();
+
+ if (src == null) {
+ return null;
+
+ } else if (src.equals(getHost())) {
+ myHeartbeatSeen = true;
+
+ } else if (src.equals(predHost)) {
+ predHeartbeatSeen = true;
+
+ }
+
+ return null;
+ }
+
+ @Override
+ public State process(Offline msg) {
+ String src = msg.getSource();
+
+ if (src == null) {
+ return null;
+
+ } else if (!assigned.contains(src)) {
+ /*
+ * the offline host wasn't assigned any buckets, so just ignore the
+ * message
+ */
+ return null;
+
+ } else if (isLeader() || (predHost.equals(src) && predHost.equals(assigned.first()))) {
+ /*
+ * Case 1: We are the leader.
+ *
+ * Case 2: Our predecessor was the leader and it has gone offline -
+ * we should become the leader.
+ *
+ * In either case, we are now the leader and we must re-balance the
+ * buckets since one of the hosts has gone offline.
+ */
+
+ assigned.remove(src);
+
+ return becomeLeader(assigned);
+
+ } else {
+ /*
+ * Otherwise, we don't care right now - we'll wait for the leader to
+ * tell us it's been removed.
+ */
+ return null;
+ }
+ }
+
+ /**
+ * Transitions to the query state.
+ */
+ @Override
+ public State process(Query msg) {
+ State next = super.process(msg);
+ if (next != null) {
+ return next;
+ }
+
+ return goQuery();
+ }
+
+ protected String getSuccHost() {
+ return succHost;
+ }
+
+ protected String getPredHost() {
+ return predHost;
+ }
+
+ protected boolean isMyHeartbeatSeen() {
+ return myHeartbeatSeen;
+ }
+
+ protected boolean isPredHeartbeatSeen() {
+ return predHeartbeatSeen;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/FilterUtils.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/FilterUtils.java
new file mode 100644
index 00000000..a2da0ea2
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/FilterUtils.java
@@ -0,0 +1,96 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Filter Utilities. These methods create <i>TreeMap</i> objects, because they
+ * should only contain a small number of items.
+ */
+public class FilterUtils {
+ // message element names
+ public static final String MSG_CHANNEL = "channel";
+ public static final String MSG_TIMESTAMP = "timestampMs";
+
+ // json element names
+ protected static final String JSON_CLASS = "class";
+ protected static final String JSON_FILTERS = "filters";
+ protected static final String JSON_FIELD = "field";
+ protected static final String JSON_VALUE = "value";
+
+ // values to be stuck into the "class" element
+ protected static final String CLASS_OR = "Or";
+ protected static final String CLASS_AND = "And";
+ protected static final String CLASS_EQUALS = "Equals";
+
+ /**
+ *
+ */
+ private FilterUtils() {
+ super();
+ }
+
+ /**
+ * Makes a filter that verifies that a field equals a value.
+ *
+ * @param field name of the field to check
+ * @param value desired value
+ * @return a map representing an "equals" filter
+ */
+ public static Map<String, Object> makeEquals(String field, String value) {
+ Map<String, Object> map = new TreeMap<>();
+ map.put(JSON_CLASS, CLASS_EQUALS);
+ map.put(JSON_FIELD, field);
+ map.put(JSON_VALUE, value);
+
+ return map;
+ }
+
+ /**
+ * Makes an "and" filter, where all of the items must be true.
+ *
+ * @param items items to be checked
+ * @return an "and" filter
+ */
+ public static Map<String, Object> makeAnd(@SuppressWarnings("unchecked") Map<String, Object>... items) {
+ Map<String, Object> map = new TreeMap<>();
+ map.put(JSON_CLASS, CLASS_AND);
+ map.put(JSON_FILTERS, items);
+
+ return map;
+ }
+
+ /**
+ * Makes an "or" filter, where at least one of the items must be true.
+ *
+ * @param items items to be checked
+ * @return an "or" filter
+ */
+ public static Map<String, Object> makeOr(@SuppressWarnings("unchecked") Map<String, Object>... items) {
+ Map<String, Object> map = new TreeMap<>();
+ map.put(JSON_CLASS, CLASS_OR);
+ map.put(JSON_FILTERS, items);
+
+ return map;
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java
new file mode 100644
index 00000000..27678360
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java
@@ -0,0 +1,85 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import org.onap.policy.drools.pooling.PoolingManager;
+import org.onap.policy.drools.pooling.message.Heartbeat;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.message.Query;
+
+/**
+ * Idle state, used when offline.
+ */
+public class IdleState extends State {
+
+ public IdleState(PoolingManager mgr) {
+ super(mgr);
+ }
+
+ @Override
+ public void stop() {
+ // do nothing - don't even send of "offline" message
+ }
+
+ /**
+ * Discards the message.
+ */
+ @Override
+ public State process(Heartbeat msg) {
+ return null;
+ }
+
+ /**
+ * Discards the message.
+ */
+ @Override
+ public State process(Identification msg) {
+ return null;
+ }
+
+ /**
+ * Copies the assignments, but doesn't change states.
+ */
+ @Override
+ public State process(Leader msg) {
+ super.process(msg);
+ return null;
+ }
+
+ /**
+ * Discards the message.
+ */
+ @Override
+ public State process(Offline msg) {
+ return null;
+ }
+
+ /**
+ * Discards the message.
+ */
+ @Override
+ public State process(Query msg) {
+ return null;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java
new file mode 100644
index 00000000..1c8e4dcc
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java
@@ -0,0 +1,55 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import org.onap.policy.drools.pooling.PoolingManager;
+
+/**
+ * The inactive state. In this state, we just wait a bit and then try to
+ * re-activate. In the meantime, all messages are ignored.
+ */
+public class InactiveState extends State {
+
+ /**
+ *
+ * @param mgr
+ */
+ public InactiveState(PoolingManager mgr) {
+ super(mgr);
+ }
+
+ @Override
+ public void start() {
+
+ super.start();
+
+ schedule(getProperties().getReactivateMs(), xxx -> goStart());
+ }
+
+ /**
+ * Remains in this state.
+ */
+ @Override
+ protected State goInactive() {
+ return null;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java
new file mode 100644
index 00000000..2f830c66
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java
@@ -0,0 +1,410 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import org.onap.policy.drools.pooling.PoolingManager;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Query;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Any state in which events are being processed locally and forwarded, as
+ * appropriate.
+ */
+public class ProcessingState extends State {
+
+ private static final Logger logger = LoggerFactory.getLogger(ProcessingState.class);
+
+ /**
+ * Current known leader, never {@code null}.
+ */
+ private String leader;
+
+ /**
+ *
+ * @param mgr
+ * @param leader current known leader, which need not be the same as the
+ * assignment leader. Never {@code null}
+ * @throws IllegalArgumentException if an argument is invalid
+ */
+ public ProcessingState(PoolingManager mgr, String leader) {
+ super(mgr);
+
+ if (leader == null) {
+ throw new IllegalArgumentException("null leader");
+ }
+
+ BucketAssignments assignments = mgr.getAssignments();
+
+ if (assignments != null) {
+ String[] arr = assignments.getHostArray();
+ if (arr != null && arr.length == 0) {
+ throw new IllegalArgumentException("zero-length bucket assignments");
+ }
+ }
+
+ this.leader = leader;
+ }
+
+ /**
+ * Generates an Identification message and returns {@code null}.
+ */
+ @Override
+ public State process(Query msg) {
+ publish(makeIdentification());
+ return goQuery();
+ }
+
+ /**
+ * Makes an Identification message.
+ *
+ * @return a new message
+ */
+ protected Identification makeIdentification() {
+ return new Identification(getHost(), getAssignments());
+ }
+
+ /**
+ * Sets the assignments.
+ *
+ * @param assignments new assignments, or {@code null}
+ */
+ protected final void setAssignments(BucketAssignments assignments) {
+ if (assignments != null) {
+ startDistributing(assignments);
+ }
+ }
+
+ public String getLeader() {
+ return leader;
+ }
+
+ /**
+ * Sets the leader.
+ *
+ * @param leader the new leader
+ * @throws IllegalArgumentException if an argument is invalid
+ */
+ protected void setLeader(String leader) {
+ if (leader == null) {
+ throw new IllegalArgumentException("null leader");
+ }
+
+ this.leader = leader;
+ }
+
+ /**
+ * Determines if this host is the leader, based on the current assignments.
+ *
+ * @return {@code true} if this host is the leader, {@code false} otherwise
+ */
+ public boolean isLeader() {
+ return getHost().equals(leader);
+ }
+
+ /**
+ * Becomes the leader. Publishes a Leader message and enters the
+ * {@link ActiveState}.
+ *
+ * @param alive hosts that are known to be alive
+ *
+ * @return the new state
+ */
+ protected State becomeLeader(SortedSet<String> alive) {
+ String leader = getHost();
+
+ if (!leader.equals(alive.first())) {
+ throw new IllegalArgumentException(leader + " cannot replace " + alive.first());
+ }
+
+ Leader msg = makeLeader(alive);
+ publish(msg);
+
+ setAssignments(msg.getAssignments());
+
+ return goActive();
+ }
+
+ /**
+ * Makes a leader message. Assumes "this" host is the leader, and thus
+ * appears as the first host in the set of hosts that are still alive.
+ *
+ * @param alive hosts that are known to be alive
+ *
+ * @return a new message
+ */
+ private Leader makeLeader(Set<String> alive) {
+ return new Leader(getHost(), makeAssignments(alive));
+ }
+
+ /**
+ * Makes a set of bucket assignments. Assumes "this" host is the leader.
+ *
+ * @param alive hosts that are known to be alive
+ *
+ * @return a new set of bucket assignments
+ */
+ private BucketAssignments makeAssignments(Set<String> alive) {
+
+ // make a working array from the CURRENT assignments
+ String[] bucket2host = makeBucketArray();
+
+ TreeSet<String> avail = new TreeSet<>(alive);
+
+ // if we have more hosts than buckets, then remove the extra hosts
+ removeExcessHosts(bucket2host.length, avail);
+
+ // create a host bucket for each available host
+ Map<String, HostBucket> host2hb = new HashMap<>();
+ avail.forEach(host -> host2hb.put(host, new HostBucket(host)));
+
+ // add bucket indices to the appropriate host bucket
+ addIndicesToHostBuckets(bucket2host, host2hb);
+
+ // convert the collection back to an array
+ fillArray(host2hb.values(), bucket2host);
+
+ // update bucket2host with new assignments
+ rebalanceBuckets(host2hb.values(), bucket2host);
+
+ return new BucketAssignments(bucket2host);
+ }
+
+ /**
+ * Makes a bucket array, copying the current assignments, if available.
+ *
+ * @return a new bucket array
+ */
+ private String[] makeBucketArray() {
+ BucketAssignments asgn = getAssignments();
+ if (asgn == null) {
+ return new String[BucketAssignments.MAX_BUCKETS];
+ }
+
+ String[] oldArray = asgn.getHostArray();
+ if (oldArray.length == 0) {
+ return new String[BucketAssignments.MAX_BUCKETS];
+ }
+
+ String[] newArray = new String[oldArray.length];
+ System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
+
+ return newArray;
+ }
+
+ /**
+ * Removes excess hosts from the set of available hosts. Assumes "this" host
+ * is the leader, and thus appears as the first host in the set.
+ *
+ * @param maxHosts maximum number of hosts to be retained
+ * @param avail available hosts
+ */
+ private void removeExcessHosts(int maxHosts, SortedSet<String> avail) {
+ while (avail.size() > maxHosts) {
+ /*
+ * Don't remove this host, as it's the leader. Since the leader is
+ * always at the front of the sorted set, we'll just pick off hosts
+ * from the back of the set.
+ */
+ String host = avail.last();
+ avail.remove(host);
+
+ logger.warn("not using extra host {} for topic {}", host, getTopic());
+ }
+ }
+
+ /**
+ * Adds bucket indices to {@link HostBucket} objects. Buckets that are
+ * unassigned or assigned to a host that does not appear within the map are
+ * re-assigned to a host that appears within the map.
+ *
+ * @param bucket2host bucket assignments
+ * @param host2data maps a host name to its {@link HostBucket}
+ */
+ private void addIndicesToHostBuckets(String[] bucket2host, Map<String, HostBucket> host2data) {
+ LinkedList<Integer> nullBuckets = new LinkedList<Integer>();
+
+ for (int x = 0; x < bucket2host.length; ++x) {
+ String host = bucket2host[x];
+ if (host == null) {
+ nullBuckets.add(x);
+
+ } else {
+ HostBucket hb = host2data.get(host);
+ if (hb == null) {
+ nullBuckets.add(x);
+
+ } else {
+ hb.add(x);
+ }
+ }
+ }
+
+ // assign the null buckets to other hosts
+ assignNullBuckets(nullBuckets, host2data.values());
+ }
+
+ /**
+ * Assigns null buckets (i.e., those having no assignment) to available
+ * hosts.
+ *
+ * @param buckets available hosts
+ * @param coll collection of current host-bucket assignments
+ */
+ private void assignNullBuckets(Queue<Integer> buckets, Collection<HostBucket> coll) {
+ // assign null buckets to the hosts with the fewest buckets
+ TreeSet<HostBucket> assignments = new TreeSet<>(coll);
+
+ for (Integer index : buckets) {
+ // add it to the host with the shortest bucket list
+ HostBucket newhb = assignments.pollFirst();
+ newhb.add(index);
+
+ // put the item back into the queue, with its new count
+ assignments.add(newhb);
+ }
+ }
+
+ /**
+ * Re-balances the buckets, taking from those that have a larger count and
+ * giving to those that have a smaller count. Populates an output array with
+ * the new assignments.
+ *
+ * @param coll current bucket assignment
+ * @param bucket2host array to be populated with the new assignments
+ */
+ private void rebalanceBuckets(Collection<HostBucket> coll, String[] bucket2host) {
+ if (coll.size() <= 1) {
+ // only one hosts - nothing to rebalance
+ return;
+ }
+
+ TreeSet<HostBucket> assignments = new TreeSet<>(coll);
+
+ for (;;) {
+ HostBucket smaller = assignments.pollFirst();
+ HostBucket larger = assignments.pollLast();
+
+ if (larger.size() - smaller.size() <= 1) {
+ // it's as balanced as it will get
+ break;
+ }
+
+ // move the bucket from the larger to the smaller
+ Integer b = larger.remove();
+ smaller.add(b);
+
+ bucket2host[b] = smaller.host;
+
+ // put the items back, with their new counts
+ assignments.add(larger);
+ assignments.add(smaller);
+ }
+
+ }
+
+ /**
+ * Fills the array with the host assignments.
+ *
+ * @param coll the host assignments
+ * @param bucket2host array to be filled
+ */
+ private void fillArray(Collection<HostBucket> coll, String[] bucket2host) {
+ for (HostBucket hb : coll) {
+ for (Integer index : hb.buckets) {
+ bucket2host[index] = hb.host;
+ }
+ }
+ }
+
+ /**
+ * Tracks buckets that have been assigned to a host.
+ */
+ public static class HostBucket implements Comparable<HostBucket> {
+ /**
+ * Host to which the buckets have been assigned.
+ */
+ private String host;
+
+ /**
+ * Buckets that have been assigned to this host.
+ */
+ private Queue<Integer> buckets = new LinkedList<>();
+
+ /**
+ *
+ * @param host
+ */
+ public HostBucket(String host) {
+ this.host = host;
+ }
+
+ /**
+ * Removes the next bucket from the list.
+ *
+ * @return the next bucket
+ */
+ public final Integer remove() {
+ return buckets.remove();
+ }
+
+ /**
+ * Adds a bucket to the list.
+ *
+ * @param index index of the bucket to add
+ */
+ public final void add(Integer index) {
+ buckets.add(index);
+ }
+
+ /**
+ *
+ * @return the number of buckets assigned to this host
+ */
+ public final int size() {
+ return buckets.size();
+ }
+
+ /**
+ * Compares host buckets, first by the number of buckets, and then by
+ * the host name.
+ */
+ @Override
+ public final int compareTo(HostBucket other) {
+ int d = buckets.size() - other.buckets.size();
+ if (d == 0) {
+ d = host.compareTo(other.host);
+ }
+ return d;
+ }
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java
new file mode 100644
index 00000000..57521960
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java
@@ -0,0 +1,209 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import java.util.TreeSet;
+import org.onap.policy.drools.pooling.PoolingManager;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Offline;
+
+// TODO add logging
+
+/**
+ * The Query state. In this state, the host waits for the other hosts to
+ * identify themselves. Eventually, a leader should come forth. If not, it will
+ * transition to the active or inactive state, depending on whether or not it
+ * has an assignment in the current bucket assignments. The other possibility is
+ * that it may <i>become</i> the leader, in which case it will also transition
+ * to the active state.
+ */
+public class QueryState extends ProcessingState {
+
+ /**
+ * Hosts that have sent an "Identification" message. Always includes this
+ * host.
+ */
+ private TreeSet<String> alive = new TreeSet<>();
+
+ /**
+ *
+ * @param mgr
+ */
+ public QueryState(PoolingManager mgr) {
+ // this host is the leader, until a better candidate identifies itself
+ super(mgr, mgr.getHost());
+
+ alive.add(getHost());
+ }
+
+ @Override
+ public void start() {
+
+ super.start();
+
+ // start identification timer
+ awaitIdentification();
+ }
+
+ /**
+ * Starts a timer to wait for all Identification messages to arrive.
+ */
+ private void awaitIdentification() {
+
+ /*
+ * Once we've waited long enough for all Identification messages to
+ * arrive, become the leader, assuming we should.
+ */
+
+ schedule(getProperties().getIdentificationMs(), xxx -> {
+
+ if (isLeader()) {
+ // "this" host is the new leader
+ return becomeLeader(alive);
+
+ } else if (hasAssignment()) {
+ /*
+ * this host is not the new leader, but it does have an
+ * assignment - return to the active state while we wait for the
+ * leader
+ */
+ return goActive();
+
+ } else {
+ // not the leader and no assignment yet
+ return goInactive();
+ }
+ });
+ }
+
+ /**
+ * Remains in this state.
+ */
+ @Override
+ public State goQuery() {
+ return null;
+ }
+
+ /**
+ * Determines if this host has an assignment in the CURRENT assignments.
+ *
+ * @return {@code true} if this host has an assignment, {@code false}
+ * otherwise
+ */
+ protected boolean hasAssignment() {
+ BucketAssignments asgn = getAssignments();
+ return (asgn != null && asgn.hasAssignment(getHost()));
+ }
+
+ @Override
+ public State process(Identification msg) {
+
+ recordInfo(msg.getSource(), msg.getAssignments());
+
+ return null;
+ }
+
+ /**
+ * If the message leader is better than the leader we have, then go active
+ * with it. Otherwise, simply treat it like an {@link Identification}
+ * message.
+ */
+ @Override
+ public State process(Leader msg) {
+ BucketAssignments asgn = msg.getAssignments();
+ if (asgn == null) {
+ return null;
+ }
+
+ // ignore Leader messages from ourself
+ String source = msg.getSource();
+ if (source == null || source.equals(getHost())) {
+ return null;
+ }
+
+ // the new leader must equal the source
+ if (!source.equals(asgn.getLeader())) {
+ return null;
+ }
+
+ // go active, if this has a better leader than the one we have
+ if (source.compareTo(getLeader()) < 0) {
+ return super.process(msg);
+ }
+
+ /*
+ * The message does not have an acceptable leader, but we'll still
+ * record its info.
+ */
+ recordInfo(msg.getSource(), msg.getAssignments());
+
+ return null;
+ }
+
+ /**
+ * Records info from a message, adding the source host name to
+ * {@link #alive}, and updating the bucket assignments.
+ *
+ * @param source the message's source host
+ * @param assignments assignments, or {@code null}
+ */
+ private void recordInfo(String source, BucketAssignments assignments) {
+ // add this message's source host to "alive"
+ if (source != null) {
+ alive.add(source);
+ setLeader(alive.first());
+ }
+
+ if (assignments == null || assignments.getLeader() == null) {
+ return;
+ }
+
+ // record assignments, if we don't have any yet
+ BucketAssignments current = getAssignments();
+ if (current == null) {
+ setAssignments(assignments);
+ return;
+ }
+
+ /*
+ * Record assignments, if the new assignments have a better (i.e.,
+ * lesser) leader.
+ */
+ String curldr = current.getLeader();
+ if (curldr == null || curldr.compareTo(assignments.getLeader()) > 0) {
+ setAssignments(assignments);
+ }
+ }
+
+ @Override
+ public State process(Offline msg) {
+ String host = msg.getSource();
+
+ if (host != null && !host.equals(getHost())) {
+ alive.remove(host);
+ setLeader(alive.first());
+ }
+
+ return null;
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StartState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StartState.java
new file mode 100644
index 00000000..decbdfda
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StartState.java
@@ -0,0 +1,132 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.onap.policy.drools.pooling.state.FilterUtils.MSG_CHANNEL;
+import static org.onap.policy.drools.pooling.state.FilterUtils.MSG_TIMESTAMP;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeAnd;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeEquals;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeOr;
+import java.util.Map;
+import org.onap.policy.drools.pooling.PoolingManager;
+import org.onap.policy.drools.pooling.message.Heartbeat;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.message.Query;
+
+/**
+ * The start state. Upon entry, a heart beat is generated and the event filter
+ * is changed to look for just that particular message. Once the message is
+ * seen, it goes into the {@link QueryState}.
+ */
+public class StartState extends State {
+
+ /**
+ * Time stamp inserted into the heart beat message.
+ */
+ private long hbTimestampMs = System.currentTimeMillis();
+
+ /**
+ *
+ * @param mgr
+ */
+ public StartState(PoolingManager mgr) {
+ super(mgr);
+ }
+
+ /**
+ *
+ * @return the time stamp inserted into the heart beat message
+ */
+ public long getHbTimestampMs() {
+ return hbTimestampMs;
+ }
+
+ @Override
+ public void start() {
+
+ super.start();
+
+ publish(getHost(), makeHeartbeat(hbTimestampMs));
+
+ schedule(getProperties().getStartHeartbeatMs(), xxx -> internalTopicFailed());
+ }
+
+ /**
+ * Transitions to the query state if the heart beat originated from this
+ * host and its time stamp matches.
+ */
+ @Override
+ public State process(Heartbeat msg) {
+ if (msg.getTimestampMs() == hbTimestampMs && getHost().equals(msg.getSource())) {
+ // saw our own heart beat - transition to query state
+ publish(makeQuery());
+ return goQuery();
+ }
+
+ return null;
+ }
+
+ /**
+ * Discards the message.
+ */
+ @Override
+ public State process(Identification msg) {
+ return null;
+ }
+
+ /**
+ * Processes the assignments, but remains in the current state.
+ */
+ @Override
+ public State process(Leader msg) {
+ super.process(msg);
+ return null;
+ }
+
+ /**
+ * Discards the message.
+ */
+ @Override
+ public State process(Offline msg) {
+ return null;
+ }
+
+ /**
+ * Discards the message.
+ */
+ @Override
+ public State process(Query msg) {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map<String, Object> getFilter() {
+ // ignore everything except our most recent heart beat message
+ return makeOr(makeEquals(MSG_CHANNEL, Message.ADMIN), makeAnd(makeEquals(MSG_CHANNEL, getHost()),
+ makeEquals(MSG_TIMESTAMP, String.valueOf(hbTimestampMs))));
+
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/State.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/State.java
new file mode 100644
index 00000000..1e3a907e
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/State.java
@@ -0,0 +1,370 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.onap.policy.drools.pooling.state.FilterUtils.MSG_CHANNEL;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeEquals;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeOr;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import org.onap.policy.drools.pooling.PoolingManager;
+import org.onap.policy.drools.pooling.PoolingProperties;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Forward;
+import org.onap.policy.drools.pooling.message.Heartbeat;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.message.Query;
+
+/**
+ * A state in the finite state machine.
+ * <p>
+ * A state may have several timers associated with it, which must be cancelled
+ * whenever the state is changed. Assumes that timers are not continuously added
+ * to the same state.
+ */
+public abstract class State {
+
+ /**
+ * Host pool manager.
+ */
+ private final PoolingManager mgr;
+
+ /**
+ * Timers added by this state.
+ */
+ private final List<ScheduledFuture<?>> timers = new LinkedList<>();
+
+ /**
+ *
+ * @param mgr
+ */
+ public State(PoolingManager mgr) {
+ this.mgr = mgr;
+ }
+
+ /**
+ * Gets the server-side filter to use when polling the DMaaP internal topic.
+ * The default method returns a filter that accepts messages on the admin
+ * channel and on the host's own channel.
+ *
+ * @return the server-side filter to use.
+ */
+ @SuppressWarnings("unchecked")
+ public Map<String, Object> getFilter() {
+ return makeOr(makeEquals(MSG_CHANNEL, Message.ADMIN), makeEquals(MSG_CHANNEL, getHost()));
+ }
+
+ /**
+ * Cancels the timers added by this state.
+ */
+ public void cancelTimers() {
+ for (ScheduledFuture<?> fut : timers) {
+ fut.cancel(false);
+ }
+ }
+
+ /**
+ * Starts the state.
+ */
+ public void start() {
+
+ }
+
+ /**
+ * Indicates that the finite state machine is stopping. Sends an "offline"
+ * message to the other hosts.
+ */
+ public void stop() {
+ publish(makeOffline());
+ }
+
+ /**
+ * Transitions to the "start" state.
+ *
+ * @return the new state
+ */
+ public State goStart() {
+ return mgr.goStart();
+ }
+
+ /**
+ * Transitions to the "query" state.
+ *
+ * @return the new state
+ */
+ public State goQuery() {
+ return mgr.goQuery();
+ }
+
+ /**
+ * Transitions to the "active" state.
+ *
+ * @return the new state
+ */
+ public State goActive() {
+ return mgr.goActive();
+ }
+
+ /**
+ * Transitions to the "inactive" state.
+ *
+ * @return the new state
+ */
+ protected State goInactive() {
+ return mgr.goInactive();
+ }
+
+ /**
+ * Processes a message. The default method passes it to the manager to
+ * handle and returns {@code null}.
+ *
+ * @param msg message to be processed
+ * @return the new state, or {@code null} if the state is unchanged
+ */
+ public State process(Forward msg) {
+ mgr.handle(msg);
+ return null;
+ }
+
+ /**
+ * Processes a message. The default method just returns {@code null}.
+ *
+ * @param msg message to be processed
+ * @return the new state, or {@code null} if the state is unchanged
+ */
+ public State process(Heartbeat msg) {
+ return null;
+ }
+
+ /**
+ * Processes a message. The default method just returns {@code null}.
+ *
+ * @param msg message to be processed
+ * @return the new state, or {@code null} if the state is unchanged
+ */
+ public State process(Identification msg) {
+ return null;
+ }
+
+ /**
+ * Processes a message. If this host has a new assignment, then it
+ * transitions to the active state. Otherwise, it transitions to the
+ * inactive state.
+ *
+ * @param msg message to be processed
+ * @return the new state, or {@code null} if the state is unchanged
+ */
+ public State process(Leader msg) {
+ BucketAssignments asgn = msg.getAssignments();
+ if (asgn == null) {
+ return null;
+ }
+
+ String source = msg.getSource();
+ if (source == null) {
+ return null;
+ }
+
+ // the new leader must equal the source
+ if (source.equals(asgn.getLeader())) {
+ startDistributing(asgn);
+
+ if (asgn.hasAssignment(getHost())) {
+ return goActive();
+
+ } else {
+ return goInactive();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Processes a message. The default method just returns {@code null}.
+ *
+ * @param msg message to be processed
+ * @return the new state, or {@code null} if the state is unchanged
+ */
+ public State process(Offline msg) {
+ return null;
+ }
+
+ /**
+ * Processes a message. The default method just returns {@code null}.
+ *
+ * @param msg message to be processed
+ * @return the new state, or {@code null} if the state is unchanged
+ */
+ public State process(Query msg) {
+ return null;
+ }
+
+ /**
+ * Publishes a message.
+ *
+ * @param msg message to be published
+ */
+ protected void publish(Identification msg) {
+ mgr.publishAdmin(msg);
+ }
+
+ /**
+ * Publishes a message.
+ *
+ * @param msg message to be published
+ */
+ protected void publish(Leader msg) {
+ mgr.publishAdmin(msg);
+ }
+
+ /**
+ * Publishes a message.
+ *
+ * @param msg message to be published
+ */
+ protected void publish(Offline msg) {
+ mgr.publishAdmin(msg);
+ }
+
+ /**
+ * Publishes a message.
+ *
+ * @param msg message to be published
+ */
+ protected void publish(Query msg) {
+ mgr.publishAdmin(msg);
+ }
+
+ /**
+ * Publishes a message on the specified channel.
+ *
+ * @param channel
+ * @param msg message to be published
+ */
+ protected void publish(String channel, Forward msg) {
+ mgr.publish(channel, msg);
+ }
+
+ /**
+ * Publishes a message on the specified channel.
+ *
+ * @param channel
+ * @param msg message to be published
+ */
+ protected void publish(String channel, Heartbeat msg) {
+ mgr.publish(channel, msg);
+ }
+
+ /**
+ * Starts distributing messages using the specified bucket assignments.
+ *
+ * @param assignments
+ */
+ protected void startDistributing(BucketAssignments assignments) {
+ if (assignments != null) {
+ mgr.startDistributing(assignments);
+ }
+ }
+
+ /**
+ * Schedules a timer to fire after a delay.
+ *
+ * @param delayMs
+ * @param task
+ */
+ protected void schedule(long delayMs, StateTimerTask task) {
+ timers.add(mgr.schedule(delayMs, task));
+ }
+
+ /**
+ * Schedules a timer to fire repeatedly.
+ *
+ * @param initialDelayMs
+ * @param delayMs
+ * @param task
+ */
+ protected void scheduleWithFixedDelay(long initialDelayMs, long delayMs, StateTimerTask task) {
+ timers.add(mgr.scheduleWithFixedDelay(initialDelayMs, delayMs, task));
+ }
+
+ /**
+ * Indicates that the internal topic failed.
+ *
+ * @return a new {@link InactiveState}
+ */
+ protected State internalTopicFailed() {
+ publish(makeOffline());
+ mgr.internalTopicFailed();
+
+ return mgr.goInactive();
+ }
+
+ /**
+ * Makes a heart beat message.
+ *
+ * @param timestampMs time, in milliseconds, associated with the message
+ *
+ * @return a new message
+ */
+ protected Heartbeat makeHeartbeat(long timestampMs) {
+ return new Heartbeat(getHost(), timestampMs);
+ }
+
+ /**
+ * Makes an "offline" message.
+ *
+ * @return a new message
+ */
+ protected Offline makeOffline() {
+ return new Offline(getHost());
+ }
+
+ /**
+ * Makes a query message.
+ *
+ * @return a new message
+ */
+ protected Query makeQuery() {
+ return new Query(getHost());
+ }
+
+ public final BucketAssignments getAssignments() {
+ return mgr.getAssignments();
+ }
+
+ public final String getHost() {
+ return mgr.getHost();
+ }
+
+ public final String getTopic() {
+ return mgr.getTopic();
+ }
+
+ public final PoolingProperties getProperties() {
+ return mgr.getProperties();
+ }
+}
diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java
new file mode 100644
index 00000000..bd388b4e
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java
@@ -0,0 +1,37 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+/**
+ * Task to be executed when a timer fires within a {@link State}.
+ */
+@FunctionalInterface
+public interface StateTimerTask {
+
+ /**
+ * Fires the timer.
+ *
+ * @param arg always {@code null}
+ * @return the new state, or {@code null} if the state is unchanged
+ */
+ public State fire(Void arg);
+
+}
diff --git a/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureAPI b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureAPI
new file mode 100644
index 00000000..cd59e469
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureAPI
@@ -0,0 +1 @@
+org.onap.policy.drools.pooling.PoolingFeature
diff --git a/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.DroolsControllerFeatureAPI b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.DroolsControllerFeatureAPI
new file mode 100644
index 00000000..cd59e469
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.DroolsControllerFeatureAPI
@@ -0,0 +1 @@
+org.onap.policy.drools.pooling.PoolingFeature
diff --git a/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureAPI b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureAPI
new file mode 100644
index 00000000..cd59e469
--- /dev/null
+++ b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureAPI
@@ -0,0 +1 @@
+org.onap.policy.drools.pooling.PoolingFeature
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/DmaapManagerTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/DmaapManagerTest.java
new file mode 100644
index 00000000..f68f2395
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/DmaapManagerTest.java
@@ -0,0 +1,355 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Properties;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onap.policy.drools.event.comm.FilterableTopicSource;
+import org.onap.policy.drools.event.comm.TopicListener;
+import org.onap.policy.drools.event.comm.TopicSink;
+import org.onap.policy.drools.event.comm.TopicSource;
+import org.onap.policy.drools.pooling.DmaapManager.Factory;
+
+public class DmaapManagerTest {
+
+ private static String MY_TOPIC = "my.topic";
+ private static String MSG = "a message";
+ private static String FILTER = "a filter";
+
+ /**
+ * Original factory, to be restored when all tests complete.
+ */
+ private static Factory saveFactory;
+
+ private Properties props;
+ private Factory factory;
+ private TopicListener listener;
+ private FilterableTopicSource source;
+ private TopicSink sink;
+ private DmaapManager mgr;
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ saveFactory = DmaapManager.getFactory();
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception {
+ DmaapManager.setFactory(saveFactory);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ props = new Properties();
+
+ listener = mock(TopicListener.class);
+ factory = mock(Factory.class);
+ source = mock(FilterableTopicSource.class);
+ sink = mock(TopicSink.class);
+
+ DmaapManager.setFactory(factory);
+
+ when(source.getTopic()).thenReturn(MY_TOPIC);
+
+ when(sink.getTopic()).thenReturn(MY_TOPIC);
+ when(sink.send(any())).thenReturn(true);
+
+ // three sources, with the desired one in the middle
+ when(factory.initTopicSources(props))
+ .thenReturn(Arrays.asList(mock(TopicSource.class), source, mock(TopicSource.class)));
+
+ // three sinks, with the desired one in the middle
+ when(factory.initTopicSinks(props))
+ .thenReturn(Arrays.asList(mock(TopicSink.class), sink, mock(TopicSink.class)));
+
+ mgr = new DmaapManager(MY_TOPIC, props);
+ }
+
+ @Test
+ public void testDmaapManager() {
+ // verify that the init methods were called
+ verify(factory).initTopicSinks(props);
+ verify(factory).initTopicSinks(props);
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testDmaapManager_PoolingEx() throws PoolingFeatureException {
+ // force error by having no topics match
+ when(source.getTopic()).thenReturn("");
+
+ new DmaapManager(MY_TOPIC, props);
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testDmaapManager_IllegalArgEx() throws PoolingFeatureException {
+ // force error
+ when(factory.initTopicSources(props)).thenThrow(new IllegalArgumentException("expected"));
+
+ new DmaapManager(MY_TOPIC, props);
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testDmaapManager_CannotFilter() throws PoolingFeatureException {
+ // force an error when setFilter() is called
+ doThrow(new UnsupportedOperationException("expected")).when(source).setFilter(any());
+
+ new DmaapManager(MY_TOPIC, props);
+ }
+
+ @Test
+ public void testGetTopic() {
+ assertEquals(MY_TOPIC, mgr.getTopic());
+ }
+
+ @Test
+ public void testFindTopicSource() {
+ // getting here means it worked
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testFindTopicSource_NotFilterableTopicSource() throws PoolingFeatureException {
+
+ // matching topic, but doesn't have the correct interface
+ TopicSource source2 = mock(TopicSource.class);
+ when(source2.getTopic()).thenReturn(MY_TOPIC);
+
+ when(factory.initTopicSources(props)).thenReturn(Arrays.asList(source2));
+
+ new DmaapManager(MY_TOPIC, props);
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testFindTopicSource_NotFound() throws PoolingFeatureException {
+ // one item in list, and its topic doesn't match
+ when(factory.initTopicSources(props)).thenReturn(Arrays.asList(mock(TopicSource.class)));
+
+ new DmaapManager(MY_TOPIC, props);
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testFindTopicSource_EmptyList() throws PoolingFeatureException {
+ // empty list
+ when(factory.initTopicSources(props)).thenReturn(new LinkedList<>());
+
+ new DmaapManager(MY_TOPIC, props);
+ }
+
+ @Test
+ public void testFindTopicSink() {
+ // getting here means it worked
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testFindTopicSink_NotFound() throws PoolingFeatureException {
+ // one item in list, and its topic doesn't match
+ when(factory.initTopicSinks(props)).thenReturn(Arrays.asList(mock(TopicSink.class)));
+
+ new DmaapManager(MY_TOPIC, props);
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testFindTopicSink_EmptyList() throws PoolingFeatureException {
+ // empty list
+ when(factory.initTopicSinks(props)).thenReturn(new LinkedList<>());
+
+ new DmaapManager(MY_TOPIC, props);
+ }
+
+ @Test
+ public void testStartPublisher() throws PoolingFeatureException {
+ // not started yet
+ verify(sink, never()).start();
+
+ mgr.startPublisher();
+ verify(sink).start();
+
+ // restart should have no effect
+ mgr.startPublisher();
+ verify(sink).start();
+
+ // should be able to publish now
+ mgr.publish(MSG);
+ verify(sink).send(MSG);
+ }
+
+ @Test
+ public void testStartPublisher_Exception() throws PoolingFeatureException {
+ // force exception when it starts
+ doThrow(new IllegalStateException("expected")).when(sink).start();
+
+ expectException("startPublisher,start", xxx -> mgr.startPublisher());
+ expectException("startPublisher,publish", xxx -> mgr.publish(MSG));
+
+ // allow it to succeed this time
+ reset(sink);
+ when(sink.send(any())).thenReturn(true);
+
+ mgr.startPublisher();
+ verify(sink).start();
+
+ // should be able to publish now
+ mgr.publish(MSG);
+ verify(sink).send(MSG);
+ }
+
+ @Test
+ public void testStopPublisher() throws PoolingFeatureException {
+ // not publishing yet, so stopping should have no effect
+ mgr.stopPublisher();
+ verify(sink, never()).stop();
+
+ // now start it
+ mgr.startPublisher();
+
+ // this time, stop should do something
+ mgr.stopPublisher();
+ verify(sink).stop();
+
+ // re-stopping should have no effect
+ mgr.stopPublisher();
+ verify(sink).stop();
+ }
+
+ @Test
+ public void testStopPublisher_Exception() throws PoolingFeatureException {
+ mgr.startPublisher();
+
+ // force exception when it stops
+ doThrow(new IllegalStateException("expected")).when(sink).stop();
+
+ mgr.stopPublisher();
+ }
+
+ @Test
+ public void testStartConsumer() {
+ // not started yet
+ verify(source, never()).register(any());
+
+ mgr.startConsumer(listener);
+ verify(source).register(listener);
+
+ // restart should have no effect
+ mgr.startConsumer(listener);
+ verify(source).register(listener);
+ }
+
+ @Test
+ public void testStopConsumer() {
+ // not consuming yet, so stopping should have no effect
+ mgr.stopConsumer(listener);
+ verify(source, never()).unregister(any());
+
+ // now start it
+ mgr.startConsumer(listener);
+
+ // this time, stop should do something
+ mgr.stopConsumer(listener);
+ verify(source).unregister(listener);
+
+ // re-stopping should have no effect
+ mgr.stopConsumer(listener);
+ verify(source).unregister(listener);
+ }
+
+ @Test
+ public void testSetFilter() throws PoolingFeatureException {
+ mgr.setFilter(FILTER);
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testSetFilter_Exception() throws PoolingFeatureException {
+ // force an error when setFilter() is called
+ doThrow(new UnsupportedOperationException("expected")).when(source).setFilter(any());
+
+ mgr.setFilter(FILTER);
+ }
+
+ @Test
+ public void testPublish() throws PoolingFeatureException {
+ // cannot publish before starting
+ expectException("publish,pre", xxx -> mgr.publish(MSG));
+
+ mgr.startPublisher();
+
+ // publish several messages
+ mgr.publish(MSG);
+ verify(sink).send(MSG);
+
+ mgr.publish(MSG+"a");
+ verify(sink).send(MSG+"a");
+
+ mgr.publish(MSG+"b");
+ verify(sink).send(MSG+"b");
+
+ // stop and verify we can no longer publish
+ mgr.stopPublisher();
+ expectException("publish,stopped", xxx -> mgr.publish(MSG));
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testPublish_SendFailed() throws PoolingFeatureException {
+ mgr.startPublisher();
+
+ // arrange for send() to fail
+ when(sink.send(MSG)).thenReturn(false);
+
+ mgr.publish(MSG);
+ }
+
+ @Test(expected = PoolingFeatureException.class)
+ public void testPublish_SendEx() throws PoolingFeatureException {
+ mgr.startPublisher();
+
+ // arrange for send() to throw an exception
+ doThrow(new IllegalStateException("expected")).when(sink).send(MSG);
+
+ mgr.publish(MSG);
+ }
+
+ private void expectException(String testnm, VFunction func) {
+ try {
+ func.apply(null);
+ fail(testnm + " missing exception");
+
+ } catch (PoolingFeatureException expected) {
+ // OK
+ }
+ }
+
+ @FunctionalInterface
+ public static interface VFunction {
+ public void apply(Void arg) throws PoolingFeatureException;
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/EventQueueTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/EventQueueTest.java
new file mode 100644
index 00000000..24144686
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/EventQueueTest.java
@@ -0,0 +1,196 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import java.util.LinkedList;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.drools.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.drools.pooling.message.Forward;
+
+public class EventQueueTest {
+
+ private static final int MAX_SIZE = 5;
+ private static final long MAX_AGE_MS = 3000L;
+
+ private static final String MY_SOURCE = "my.source";
+ private static final CommInfrastructure MY_PROTO = CommInfrastructure.UEB;
+ private static final String MY_TOPIC = "my.topic";
+ private static final String MY_PAYLOAD = "my.payload";
+ private static final String MY_REQID = "my.request.id";
+
+ private EventQueue queue;
+
+ @Before
+ public void setUp() {
+ queue = new EventQueue(MAX_SIZE, MAX_AGE_MS);
+
+ }
+
+ @Test
+ public void testEventQueue() {
+ // shouldn't generate an exception
+ new EventQueue(1, 1);
+ }
+
+ @Test
+ public void testClear() {
+ // add some items
+ queue.add(makeActive());
+ queue.add(makeActive());
+
+ assertFalse(queue.isEmpty());
+
+ queue.clear();
+
+ // should be empty now
+ assertTrue(queue.isEmpty());
+ }
+
+ @Test
+ public void testIsEmpty() {
+ // test when empty
+ assertTrue(queue.isEmpty());
+
+ // all active
+ Forward msg1 = makeActive();
+ Forward msg2 = makeActive();
+ queue.add(msg1);
+ assertFalse(queue.isEmpty());
+
+ queue.add(msg2);
+ assertFalse(queue.isEmpty());
+
+ assertEquals(msg1, queue.poll());
+ assertFalse(queue.isEmpty());
+
+ assertEquals(msg2, queue.poll());
+ assertTrue(queue.isEmpty());
+
+ // active, expired, expired, active
+ queue.add(msg1);
+ queue.add(makeInactive());
+ queue.add(makeInactive());
+ queue.add(msg2);
+
+ assertEquals(msg1, queue.poll());
+ assertFalse(queue.isEmpty());
+
+ assertEquals(msg2, queue.poll());
+ assertTrue(queue.isEmpty());
+ }
+
+ @Test
+ public void testSize() {
+ queue = new EventQueue(2, 1000L);
+ assertEquals(0, queue.size());
+
+ queue.add(makeActive());
+ assertEquals(1, queue.size());
+
+ queue.poll();
+ assertEquals(0, queue.size());
+
+ queue.add(makeActive());
+ queue.add(makeActive());
+ assertEquals(2, queue.size());
+
+ queue.poll();
+ assertEquals(1, queue.size());
+
+ queue.poll();
+ assertEquals(0, queue.size());
+ }
+
+ @Test
+ public void testAdd() {
+ int nextra = 3;
+
+ // create excess messages
+ LinkedList<Forward> msgs = new LinkedList<>();
+ for (int x = 0; x < MAX_SIZE + nextra; ++x) {
+ msgs.add(makeActive());
+ }
+
+ // add them to the queue
+ msgs.forEach(msg -> queue.add(msg));
+
+ // should not have added too many messages
+ assertEquals(MAX_SIZE, queue.size());
+
+ // should have discarded the first "nextra" items
+ for (int x = 0; x < MAX_SIZE; ++x) {
+ assertEquals("x=" + x, msgs.get(x + nextra), queue.poll());
+ }
+
+ assertEquals(null, queue.poll());
+ }
+
+ @Test
+ public void testPoll() {
+ // poll when empty
+ assertNull(queue.poll());
+
+ // all active
+ Forward msg1 = makeActive();
+ Forward msg2 = makeActive();
+ queue.add(msg1);
+ queue.add(msg2);
+
+ assertEquals(msg1, queue.poll());
+ assertEquals(msg2, queue.poll());
+ assertEquals(null, queue.poll());
+
+ // active, expired, expired, active
+ queue.add(msg1);
+ queue.add(makeInactive());
+ queue.add(makeInactive());
+ queue.add(msg2);
+
+ assertEquals(msg1, queue.poll());
+ assertEquals(msg2, queue.poll());
+ assertEquals(null, queue.poll());
+
+ // one that's close to the age limit
+ msg1 = makeActive();
+ msg1.setCreateTimeMs(System.currentTimeMillis() - MAX_AGE_MS + 100);
+ queue.add(msg1);
+ assertEquals(msg1, queue.poll());
+ assertEquals(null, queue.poll());
+ }
+
+ private Forward makeActive() {
+ return new Forward(MY_SOURCE, MY_PROTO, MY_TOPIC, MY_PAYLOAD, MY_REQID);
+ }
+
+ private Forward makeInactive() {
+ Forward msg = new Forward(MY_SOURCE, MY_PROTO, MY_TOPIC, MY_PAYLOAD, MY_REQID);
+
+ msg.setCreateTimeMs(System.currentTimeMillis() - MAX_AGE_MS - 100);
+
+ return msg;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/FeatureEnabledCheckerTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/FeatureEnabledCheckerTest.java
new file mode 100644
index 00000000..5f918f73
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/FeatureEnabledCheckerTest.java
@@ -0,0 +1,74 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.onap.policy.common.utils.properties.SpecPropertyConfiguration.generalize;
+import static org.onap.policy.common.utils.properties.SpecPropertyConfiguration.specialize;
+import java.util.Properties;
+import org.junit.Test;
+
+public class FeatureEnabledCheckerTest {
+
+ private static final String PROP_NAME = "enable.{?.}it";
+
+ private static final String SPEC = "my.specializer";
+
+ @Test
+ public void test() {
+ assertFalse(check(null, null));
+ assertTrue(check(null, true));
+ assertFalse(check(null, false));
+
+ assertTrue(check(true, null));
+ assertTrue(check(true, true));
+ assertFalse(check(true, false));
+
+ assertFalse(check(false, null));
+ assertTrue(check(false, true));
+ assertFalse(check(false, false));
+ }
+
+ /**
+ * Adds properties, as specified, and checks if the feature is enabled.
+ *
+ * @param wantGen value to assign to the generalized property, or
+ * {@code null} to leave it unset
+ * @param wantSpec value to assign to the specialized property, or
+ * {@code null} to leave it unset
+ * @return {@code true} if the feature is enabled, {@code false} otherwise
+ */
+ public boolean check(Boolean wantGen, Boolean wantSpec) {
+ Properties props = new Properties();
+
+ if (wantGen != null) {
+ props.setProperty(generalize(PROP_NAME), wantGen.toString());
+ }
+
+ if (wantSpec != null) {
+ props.setProperty(specialize(PROP_NAME, SPEC), wantSpec.toString());
+ }
+
+ return FeatureEnabledChecker.isFeatureEnabled(props, SPEC, PROP_NAME);
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java
new file mode 100644
index 00000000..5b423d4b
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java
@@ -0,0 +1,42 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.*;
+import org.junit.Test;
+import org.onap.policy.common.utils.test.ExceptionsTester;
+
+public class PoolingFeatureExceptionTest extends ExceptionsTester {
+
+ @Test
+ public void test() {
+ assertEquals(5, test(PoolingFeatureException.class));
+ }
+
+ @Test
+ public void testToRuntimeException() {
+ PoolingFeatureException plainExc = new PoolingFeatureException("hello");
+ PoolingFeatureRtException runtimeExc = plainExc.toRuntimeException();
+
+ assertTrue(plainExc == runtimeExc.getCause());
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java
new file mode 100644
index 00000000..cbb24421
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java
@@ -0,0 +1,35 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.*;
+import org.junit.Test;
+import org.onap.policy.common.utils.test.ExceptionsTester;
+import org.onap.policy.drools.pooling.PoolingFeatureRtException;
+
+public class PoolingFeatureRtExceptionTest extends ExceptionsTester {
+
+ @Test
+ public void test() {
+ assertEquals(5, test(PoolingFeatureRtException.class));
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java
new file mode 100644
index 00000000..cd1aea09
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java
@@ -0,0 +1,495 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+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.util.LinkedList;
+import java.util.List;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.drools.pooling.PoolingFeature.Factory;
+import org.onap.policy.drools.system.PolicyController;
+import org.onap.policy.drools.utils.Pair;
+
+public class PoolingFeatureTest {
+
+ private static final String CONFIG_DIR = "src/test/java/org/onap/policy/drools/pooling";
+
+ private static final String CONTROLLER1 = "controllerA";
+ private static final String CONTROLLER2 = "controllerB";
+ private static final String CONTROLLER_DISABLED = "controllerDisabled";
+ private static final String CONTROLLER_EX = "controllerException";
+ private static final String CONTROLLER_UNKNOWN = "controllerUnknown";
+
+ private static final String TOPIC1 = "topic.one";
+ private static final String TOPIC2 = "topic.two";
+
+ private static final String EVENT1 = "event.one";
+ private static final String EVENT2 = "event.two";
+
+ private static final Object OBJECT1 = new Object();
+ private static final Object OBJECT2 = new Object();
+
+ /**
+ * Saved from PoolingFeature and restored on exit from this test class.
+ */
+ private static Factory saveFactory;
+
+ private PolicyController controller1;
+ private PolicyController controller2;
+ private PolicyController controllerDisabled;
+ private PolicyController controllerException;
+ private PolicyController controllerUnknown;
+ private DroolsController drools1;
+ private DroolsController drools2;
+ private DroolsController droolsDisabled;
+ private List<Pair<PoolingManagerImpl, PoolingProperties>> managers;
+ private PoolingManagerImpl mgr1;
+ private PoolingManagerImpl mgr2;
+ private Factory factory;
+
+ private PoolingFeature pool;
+
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ saveFactory = PoolingFeature.getFactory();
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() {
+ PoolingFeature.setFactory(saveFactory);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ factory = mock(Factory.class);
+ controller1 = mock(PolicyController.class);
+ controller2 = mock(PolicyController.class);
+ controllerDisabled = mock(PolicyController.class);
+ controllerException = mock(PolicyController.class);
+ controllerUnknown = mock(PolicyController.class);
+ drools1 = mock(DroolsController.class);
+ drools2 = mock(DroolsController.class);
+ droolsDisabled = mock(DroolsController.class);
+ managers = new LinkedList<>();
+
+ PoolingFeature.setFactory(factory);
+
+ when(controller1.getName()).thenReturn(CONTROLLER1);
+ when(controller2.getName()).thenReturn(CONTROLLER2);
+ when(controllerDisabled.getName()).thenReturn(CONTROLLER_DISABLED);
+ when(controllerException.getName()).thenReturn(CONTROLLER_EX);
+ when(controllerUnknown.getName()).thenReturn(CONTROLLER_UNKNOWN);
+
+ when(factory.getController(drools1)).thenReturn(controller1);
+ when(factory.getController(drools2)).thenReturn(controller2);
+ when(factory.getController(droolsDisabled)).thenReturn(controllerDisabled);
+
+ when(factory.makeManager(any(), any())).thenAnswer(args -> {
+ PoolingProperties props = args.getArgumentAt(1, PoolingProperties.class);
+
+ PoolingManagerImpl mgr = mock(PoolingManagerImpl.class);
+
+ managers.add(new Pair<>(mgr, props));
+
+ return mgr;
+ });
+
+ pool = new PoolingFeature();
+
+ pool.globalInit(null, CONFIG_DIR);
+
+ pool.afterCreate(controller1);
+ pool.afterCreate(controller2);
+
+ mgr1 = managers.get(0).first();
+ mgr2 = managers.get(1).first();
+ }
+
+ @Test
+ public void test() {
+ assertEquals(2, managers.size());
+ }
+
+ @Test
+ public void testGetSequenceNumber() {
+ assertEquals(0, pool.getSequenceNumber());
+ }
+
+ @Test
+ public void testGlobalInit() {
+ pool = new PoolingFeature();
+
+ pool.globalInit(null, CONFIG_DIR);
+ }
+
+ @Test(expected = PoolingFeatureRtException.class)
+ public void testGlobalInit_NotFound() {
+ pool = new PoolingFeature();
+
+ pool.globalInit(null, CONFIG_DIR + "/unknown");
+ }
+
+ @Test
+ public void testAfterCreate() {
+ managers.clear();
+ pool = new PoolingFeature();
+ pool.globalInit(null, CONFIG_DIR);
+
+ assertFalse(pool.afterCreate(controller1));
+ assertEquals(1, managers.size());
+
+ // duplicate
+ assertFalse(pool.afterCreate(controller1));
+ assertEquals(1, managers.size());
+
+ // second controller
+ assertFalse(pool.afterCreate(controller2));
+ assertEquals(2, managers.size());
+ }
+
+ @Test
+ public void testAfterCreate_NotEnabled() {
+ managers.clear();
+ pool = new PoolingFeature();
+ pool.globalInit(null, CONFIG_DIR);
+
+ assertFalse(pool.afterCreate(controllerDisabled));
+ assertTrue(managers.isEmpty());
+ }
+
+ @Test(expected = PoolingFeatureRtException.class)
+ public void testAfterCreate_PropertyEx() {
+ managers.clear();
+ pool = new PoolingFeature();
+ pool.globalInit(null, CONFIG_DIR);
+
+ pool.afterCreate(controllerException);
+ }
+
+ @Test(expected = PoolingFeatureRtException.class)
+ public void testAfterCreate_NoProps() {
+ pool = new PoolingFeature();
+
+ // did not perform globalInit, which is an error
+
+ pool.afterCreate(controller1);
+ }
+
+ @Test
+ public void testAfterCreate_NoFeatProps() {
+ managers.clear();
+ pool = new PoolingFeature();
+ pool.globalInit(null, CONFIG_DIR);
+
+ assertFalse(pool.afterCreate(controllerUnknown));
+ assertTrue(managers.isEmpty());
+ }
+
+ @Test
+ public void testBeforeStart() throws Exception {
+ assertFalse(pool.beforeStart(controller1));
+ verify(mgr1).beforeStart();
+
+ // ensure it's still in the map by re-invoking
+ assertFalse(pool.beforeStart(controller1));
+ verify(mgr1, times(2)).beforeStart();
+
+ assertFalse(pool.beforeStart(controllerDisabled));
+ }
+
+ @Test
+ public void testAfterStart() {
+ assertFalse(pool.afterStart(controller1));
+ verify(mgr1).afterStart();
+
+ // ensure it's still in the map by re-invoking
+ assertFalse(pool.afterStart(controller1));
+ verify(mgr1, times(2)).afterStart();
+
+ assertFalse(pool.afterStart(controllerDisabled));
+ }
+
+ @Test
+ public void testBeforeStop() {
+ assertFalse(pool.beforeStop(controller1));
+ verify(mgr1).beforeStop();
+
+ // ensure it's still in the map by re-invoking
+ assertFalse(pool.beforeStop(controller1));
+ verify(mgr1, times(2)).beforeStop();
+
+ assertFalse(pool.beforeStop(controllerDisabled));
+ }
+
+ @Test
+ public void testAfterStop() {
+ assertFalse(pool.afterStop(controller1));
+ verify(mgr1).afterStop();
+
+ // ensure it has been removed from the map by re-invoking
+ assertFalse(pool.afterStop(controller1));
+
+ // count should be unchanged
+ verify(mgr1).afterStop();
+
+ assertFalse(pool.afterStop(controllerDisabled));
+ }
+
+ @Test
+ public void testBeforeLock() {
+ assertFalse(pool.beforeLock(controller1));
+ verify(mgr1).beforeLock();
+
+ // ensure it's still in the map by re-invoking
+ assertFalse(pool.beforeLock(controller1));
+ verify(mgr1, times(2)).beforeLock();
+
+ assertFalse(pool.beforeLock(controllerDisabled));
+ }
+
+ @Test
+ public void testAfterUnlock() {
+ assertFalse(pool.afterUnlock(controller1));
+ verify(mgr1).afterUnlock();
+
+ // ensure it's still in the map by re-invoking
+ assertFalse(pool.afterUnlock(controller1));
+ verify(mgr1, times(2)).afterUnlock();
+
+ assertFalse(pool.afterUnlock(controllerDisabled));
+ }
+
+ @Test
+ public void testBeforeOffer() {
+ assertFalse(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1));
+ verify(mgr1).beforeOffer(CommInfrastructure.UEB, TOPIC1, EVENT1);
+
+ // ensure that the args were captured
+ pool.beforeInsert(drools1, OBJECT1);
+ verify(mgr1).beforeInsert(CommInfrastructure.UEB, TOPIC1, EVENT1, OBJECT1);
+
+
+ // ensure it's still in the map by re-invoking
+ assertFalse(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2));
+ verify(mgr1).beforeOffer(CommInfrastructure.UEB, TOPIC2, EVENT2);
+
+ // ensure that the new args were captured
+ pool.beforeInsert(drools1, OBJECT2);
+ verify(mgr1).beforeInsert(CommInfrastructure.UEB, TOPIC2, EVENT2, OBJECT2);
+
+
+ assertFalse(pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC1, EVENT1));
+ }
+
+ @Test
+ public void testBeforeOffer_NotFound() {
+ assertFalse(pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC1, EVENT1));
+ }
+
+ @Test
+ public void testBeforeOffer_MgrTrue() {
+
+ // manager will return true
+ when(mgr1.beforeOffer(any(), any(), any())).thenReturn(true);
+
+ assertTrue(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1));
+ verify(mgr1).beforeOffer(CommInfrastructure.UEB, TOPIC1, EVENT1);
+
+ // ensure it's still in the map by re-invoking
+ assertTrue(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2));
+ verify(mgr1).beforeOffer(CommInfrastructure.UEB, TOPIC2, EVENT2);
+
+ assertFalse(pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC1, EVENT1));
+ }
+
+ @Test
+ public void testBeforeInsert() {
+ pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1);
+ assertFalse(pool.beforeInsert(drools1, OBJECT1));
+ verify(mgr1).beforeInsert(CommInfrastructure.UEB, TOPIC1, EVENT1, OBJECT1);
+
+ // ensure it's still in the map by re-invoking
+ pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2);
+ assertFalse(pool.beforeInsert(drools1, OBJECT2));
+ verify(mgr1).beforeInsert(CommInfrastructure.UEB, TOPIC2, EVENT2, OBJECT2);
+
+ pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC2, EVENT2);
+ assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1));
+ }
+
+ @Test
+ public void testBeforeInsert_NoArgs() {
+
+ // call beforeInsert without beforeOffer
+ assertFalse(pool.beforeInsert(drools1, OBJECT1));
+ verify(mgr1, never()).beforeInsert(any(), any(), any(), any());
+
+ assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1));
+ verify(mgr1, never()).beforeInsert(any(), any(), any(), any());
+ }
+
+ @Test
+ public void testBeforeInsert_ArgEx() {
+
+ // generate exception
+ doThrow(new IllegalArgumentException()).when(factory).getController(any());
+
+ pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1);
+ assertFalse(pool.beforeInsert(drools1, OBJECT1));
+ verify(mgr1, never()).beforeInsert(any(), any(), any(), any());
+ }
+
+ @Test
+ public void testBeforeInsert_StateEx() {
+
+ // generate exception
+ doThrow(new IllegalStateException()).when(factory).getController(any());
+
+ pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1);
+ assertFalse(pool.beforeInsert(drools1, OBJECT1));
+ verify(mgr1, never()).beforeInsert(any(), any(), any(), any());
+ }
+
+ @Test
+ public void testBeforeInsert_NullController() {
+
+ // return null controller
+ when(factory.getController(any())).thenReturn(null);
+
+ pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1);
+ assertFalse(pool.beforeInsert(drools1, OBJECT1));
+ verify(mgr1, never()).beforeInsert(any(), any(), any(), any());
+ }
+
+ @Test
+ public void testBeforeInsert_NotFound() {
+
+ pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC2, EVENT2);
+ assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1));
+ }
+
+ @Test
+ public void testAfterOffer() {
+ // this will create OfferArgs
+ pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1);
+
+ // this should clear them
+ assertFalse(pool.afterOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2, true));
+
+ assertFalse(pool.beforeInsert(drools1, OBJECT1));
+ verify(mgr1, never()).beforeInsert(any(), any(), any(), any());
+
+
+ assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1));
+ }
+
+ @Test
+ public void testDoManager() throws Exception {
+ assertFalse(pool.beforeStart(controller1));
+ verify(mgr1).beforeStart();
+
+ // ensure it's still in the map by re-invoking
+ assertFalse(pool.beforeStart(controller1));
+ verify(mgr1, times(2)).beforeStart();
+
+
+ // different controller
+ assertFalse(pool.beforeStart(controller2));
+ verify(mgr2).beforeStart();
+
+ // ensure it's still in the map by re-invoking
+ assertFalse(pool.beforeStart(controller2));
+ verify(mgr2, times(2)).beforeStart();
+
+
+ assertFalse(pool.beforeStart(controllerDisabled));
+ }
+
+ @Test
+ public void testDoManager_NotFound() {
+ assertFalse(pool.beforeStart(controllerDisabled));
+ }
+
+ @Test(expected = PoolingFeatureRtException.class)
+ public void testDoManager_Ex() throws Exception {
+
+ // generate exception
+ doThrow(new PoolingFeatureException()).when(mgr1).beforeStart();
+
+ pool.beforeStart(controller1);
+ }
+
+ @Test
+ public void testDoDeleteManager() {
+ assertFalse(pool.afterStop(controller1));
+ verify(mgr1).afterStop();
+
+ // ensure it has been removed from the map by re-invoking
+ assertFalse(pool.afterStop(controller1));
+
+ // count should be unchanged
+ verify(mgr1).afterStop();
+
+
+ // different controller
+ assertFalse(pool.afterStop(controller2));
+ verify(mgr2).afterStop();
+
+ // ensure it has been removed from the map by re-invoking
+ assertFalse(pool.afterStop(controller2));
+
+ // count should be unchanged
+ verify(mgr2).afterStop();
+
+
+ assertFalse(pool.afterStop(controllerDisabled));
+ }
+
+ @Test
+ public void testDoDeleteManager_NotFound() {
+ assertFalse(pool.afterStop(controllerDisabled));
+ }
+
+ @Test(expected = PoolingFeatureRtException.class)
+ public void testDoDeleteManager_Ex() {
+
+ // generate exception
+ doThrow(new PoolingFeatureRtException()).when(mgr1).afterStop();
+
+ pool.afterStop(controller1);
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java
new file mode 100644
index 00000000..01ee61ef
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java
@@ -0,0 +1,1342 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+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.util.LinkedList;
+import java.util.Properties;
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.drools.event.comm.TopicListener;
+import org.onap.policy.drools.pooling.PoolingManagerImpl.Factory;
+import org.onap.policy.drools.pooling.extractor.ClassExtractors;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Forward;
+import org.onap.policy.drools.pooling.message.Heartbeat;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.state.ActiveState;
+import org.onap.policy.drools.pooling.state.IdleState;
+import org.onap.policy.drools.pooling.state.InactiveState;
+import org.onap.policy.drools.pooling.state.QueryState;
+import org.onap.policy.drools.pooling.state.StartState;
+import org.onap.policy.drools.pooling.state.State;
+import org.onap.policy.drools.system.PolicyController;
+
+public class PoolingManagerImplTest {
+
+ protected static final long STD_HEARTBEAT_WAIT_MS = 10;
+ protected static final long STD_REACTIVATE_WAIT_MS = STD_HEARTBEAT_WAIT_MS + 1;
+ protected static final long STD_IDENTIFICATION_MS = STD_REACTIVATE_WAIT_MS + 1;
+ protected static final long STD_ACTIVE_HEARTBEAT_MS = STD_IDENTIFICATION_MS + 1;
+ protected static final long STD_INTER_HEARTBEAT_MS = STD_ACTIVE_HEARTBEAT_MS + 1;
+
+ private static final String HOST2 = "other.host";
+
+ private static final String MY_CONTROLLER = "my.controller";
+ private static final String MY_TOPIC = "my.topic";
+
+ private static final String TOPIC2 = "topic.two";
+
+ private static final String THE_EVENT = "the event";
+
+ private static final Object DECODED_EVENT = new Object();
+ private static final String REQUEST_ID = "my.request.id";
+
+ /**
+ * Number of dmaap.publish() invocations that should be issued when the
+ * manager is started.
+ */
+ private static final int START_PUB = 1;
+
+ /**
+ * Saved from PoolingManagerImpl and restored on exit from this test class.
+ */
+ private static Factory saveFactory;
+
+ /**
+ * Futures that have been allocated due to calls to scheduleXxx().
+ */
+ private Queue<ScheduledFuture<?>> futures;
+
+ private Properties plainProps;
+ private PoolingProperties poolProps;
+ private ListeningController controller;
+ private EventQueue eventQueue;
+ private ClassExtractors extractors;
+ private DmaapManager dmaap;
+ private ScheduledThreadPoolExecutor sched;
+ private DroolsController drools;
+ private Serializer ser;
+ private Factory factory;
+
+ private PoolingManagerImpl mgr;
+
+ @BeforeClass
+ public static void setUpBeforeClass() {
+ saveFactory = PoolingManagerImpl.getFactory();
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() {
+ PoolingManagerImpl.setFactory(saveFactory);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ plainProps = new Properties();
+
+ poolProps = mock(PoolingProperties.class);
+ when(poolProps.getSource()).thenReturn(plainProps);
+ when(poolProps.getPoolingTopic()).thenReturn(MY_TOPIC);
+ when(poolProps.getStartHeartbeatMs()).thenReturn(STD_HEARTBEAT_WAIT_MS);
+ when(poolProps.getReactivateMs()).thenReturn(STD_REACTIVATE_WAIT_MS);
+ when(poolProps.getIdentificationMs()).thenReturn(STD_IDENTIFICATION_MS);
+ when(poolProps.getActiveHeartbeatMs()).thenReturn(STD_ACTIVE_HEARTBEAT_MS);
+ when(poolProps.getInterHeartbeatMs()).thenReturn(STD_INTER_HEARTBEAT_MS);
+
+ futures = new LinkedList<>();
+ ser = new Serializer();
+
+ factory = mock(Factory.class);
+ eventQueue = mock(EventQueue.class);
+ extractors = mock(ClassExtractors.class);
+ dmaap = mock(DmaapManager.class);
+ controller = mock(ListeningController.class);
+ sched = mock(ScheduledThreadPoolExecutor.class);
+ drools = mock(DroolsController.class);
+
+ when(factory.makeEventQueue(any())).thenReturn(eventQueue);
+ when(factory.makeClassExtractors(any())).thenReturn(extractors);
+ when(factory.makeDmaapManager(any())).thenReturn(dmaap);
+ when(factory.makeScheduler()).thenReturn(sched);
+ when(factory.canDecodeEvent(drools, TOPIC2)).thenReturn(true);
+ when(factory.decodeEvent(drools, TOPIC2, THE_EVENT)).thenReturn(DECODED_EVENT);
+
+ when(extractors.extract(DECODED_EVENT)).thenReturn(REQUEST_ID);
+
+ when(controller.getName()).thenReturn(MY_CONTROLLER);
+ when(controller.getDrools()).thenReturn(drools);
+ when(controller.isAlive()).thenReturn(true);
+
+ when(sched.schedule(any(Runnable.class), any(Long.class), any(TimeUnit.class))).thenAnswer(args -> {
+ ScheduledFuture<?> fut = mock(ScheduledFuture.class);
+ futures.add(fut);
+
+ return fut;
+ });
+
+ when(sched.scheduleWithFixedDelay(any(Runnable.class), any(Long.class), any(Long.class), any(TimeUnit.class)))
+ .thenAnswer(args -> {
+ ScheduledFuture<?> fut = mock(ScheduledFuture.class);
+ futures.add(fut);
+
+ return fut;
+ });
+
+ PoolingManagerImpl.setFactory(factory);
+
+ mgr = new PoolingManagerImpl(controller, poolProps);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ }
+
+ @Test
+ public void testPoolingManagerImpl() {
+ mgr = new PoolingManagerImpl(controller, poolProps);
+
+ State st = mgr.getCurrent();
+ assertTrue(st instanceof IdleState);
+
+ // ensure the state is attached to the manager
+ assertEquals(mgr.getHost(), st.getHost());
+ }
+
+ @Test
+ public void testPoolingManagerImpl_ClassEx() {
+ /*
+ * this controller does not implement TopicListener, which should cause
+ * a ClassCastException
+ */
+ PolicyController ctlr = mock(PolicyController.class);
+
+ PoolingFeatureRtException ex = expectException(PoolingFeatureRtException.class,
+ xxx -> new PoolingManagerImpl(ctlr, poolProps));
+ assertNotNull(ex.getCause());
+ assertTrue(ex.getCause() instanceof ClassCastException);
+ }
+
+ @Test
+ public void testPoolingManagerImpl_PoolEx() throws PoolingFeatureException {
+ // throw an exception when we try to create the dmaap manager
+ PoolingFeatureException ex = new PoolingFeatureException();
+ when(factory.makeDmaapManager(any())).thenThrow(ex);
+
+ PoolingFeatureRtException ex2 = expectException(PoolingFeatureRtException.class,
+ xxx -> new PoolingManagerImpl(controller, poolProps));
+ assertEquals(ex, ex2.getCause());
+ }
+
+ @Test
+ public void testGetHost() {
+ String host = mgr.getHost();
+ assertNotNull(host);
+
+ // create another manager and ensure it generates a different host
+ mgr = new PoolingManagerImpl(controller, poolProps);
+
+ assertNotNull(mgr.getHost());
+ assertFalse(host.equals(mgr.getHost()));
+ }
+
+ @Test
+ public void testGetTopic() {
+ assertEquals(MY_TOPIC, mgr.getTopic());
+ }
+
+ @Test
+ public void testGetProperties() {
+ assertEquals(poolProps, mgr.getProperties());
+ }
+
+ @Test
+ public void testBeforeStart() throws Exception {
+ // not running yet
+ mgr.beforeStart();
+
+ verify(dmaap).startPublisher();
+
+ verify(factory).makeScheduler();
+ verify(sched).setMaximumPoolSize(1);
+ verify(sched).setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+
+
+ // try again - nothing should happen
+ mgr.beforeStart();
+
+ verify(dmaap).startPublisher();
+
+ verify(factory).makeScheduler();
+ verify(sched).setMaximumPoolSize(1);
+ verify(sched).setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+ }
+
+ @Test
+ public void testBeforeStart_DmaapEx() throws Exception {
+ // generate an exception
+ PoolingFeatureException ex = new PoolingFeatureException();
+ doThrow(ex).when(dmaap).startPublisher();
+
+ PoolingFeatureException ex2 = expectException(PoolingFeatureException.class, xxx -> mgr.beforeStart());
+ assertEquals(ex, ex2);
+
+ // should never start the scheduler
+ verify(factory, never()).makeScheduler();
+ }
+
+ @Test
+ public void testAfterStart() throws Exception {
+ startMgr();
+
+ verify(dmaap).startConsumer(mgr);
+
+ State st = mgr.getCurrent();
+ assertTrue(st instanceof StartState);
+
+ // ensure the state is attached to the manager
+ assertEquals(mgr.getHost(), st.getHost());
+
+ ArgumentCaptor<Long> timeCap = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<TimeUnit> unitCap = ArgumentCaptor.forClass(TimeUnit.class);
+ verify(sched).schedule(any(Runnable.class), timeCap.capture(), unitCap.capture());
+
+ assertEquals(STD_HEARTBEAT_WAIT_MS, timeCap.getValue().longValue());
+ assertEquals(TimeUnit.MILLISECONDS, unitCap.getValue());
+
+
+ // already started - nothing else happens
+ mgr.afterStart();
+
+ verify(dmaap).startConsumer(mgr);
+
+ assertTrue(mgr.getCurrent() instanceof StartState);
+
+ verify(sched).schedule(any(Runnable.class), any(Long.class), any(TimeUnit.class));
+ }
+
+ @Test
+ public void testBeforeStop() throws Exception {
+ startMgr();
+
+ mgr.beforeStop();
+
+ verify(dmaap).stopConsumer(mgr);
+ verify(sched).shutdownNow();
+
+ assertTrue(mgr.getCurrent() instanceof IdleState);
+ }
+
+ @Test
+ public void testBeforeStop_NotRunning() throws Exception {
+ State st = mgr.getCurrent();
+
+ mgr.beforeStop();
+
+ verify(dmaap, never()).stopConsumer(any());
+ verify(sched, never()).shutdownNow();
+
+ // hasn't changed states either
+ assertEquals(st, mgr.getCurrent());
+ }
+
+ @Test
+ public void testBeforeStop_AfterPartialStart() throws Exception {
+ // call beforeStart but not afterStart
+ mgr.beforeStart();
+
+ State st = mgr.getCurrent();
+
+ mgr.beforeStop();
+
+ // should still shut the scheduler down
+ verify(sched).shutdownNow();
+
+ verify(dmaap, never()).stopConsumer(any());
+
+ // hasn't changed states
+ assertEquals(st, mgr.getCurrent());
+ }
+
+ @Test
+ public void testAfterStop() throws Exception {
+ startMgr();
+ mgr.beforeStop();
+
+ when(eventQueue.isEmpty()).thenReturn(false);
+ when(eventQueue.size()).thenReturn(3);
+
+ mgr.afterStop();
+
+ verify(eventQueue).clear();
+ verify(dmaap).stopPublisher();
+ }
+
+ @Test
+ public void testAfterStop_EmptyQueue() throws Exception {
+ startMgr();
+ mgr.beforeStop();
+
+ when(eventQueue.isEmpty()).thenReturn(true);
+ when(eventQueue.size()).thenReturn(0);
+
+ mgr.afterStop();
+
+ verify(eventQueue, never()).clear();
+ verify(dmaap).stopPublisher();
+ }
+
+ @Test
+ public void testBeforeLock() throws Exception {
+ startMgr();
+
+ mgr.beforeLock();
+
+ assertTrue(mgr.getCurrent() instanceof IdleState);
+ }
+
+ @Test
+ public void testAfterUnlock_AliveIdle() throws Exception {
+ // this really shouldn't happen
+
+ lockMgr();
+
+ mgr.afterUnlock();
+
+ // stays in idle state, because it has no scheduler
+ assertTrue(mgr.getCurrent() instanceof IdleState);
+ }
+
+ @Test
+ public void testAfterUnlock_AliveStarted() throws Exception {
+ startMgr();
+ lockMgr();
+
+ mgr.afterUnlock();
+
+ assertTrue(mgr.getCurrent() instanceof StartState);
+ }
+
+ @Test
+ public void testAfterUnlock_StoppedIdle() throws Exception {
+ startMgr();
+ lockMgr();
+
+ // controller is stopped
+ when(controller.isAlive()).thenReturn(false);
+
+ mgr.afterUnlock();
+
+ assertTrue(mgr.getCurrent() instanceof IdleState);
+ }
+
+ @Test
+ public void testAfterUnlock_StoppedStarted() throws Exception {
+ startMgr();
+
+ // Note: don't lockMgr()
+
+ // controller is stopped
+ when(controller.isAlive()).thenReturn(false);
+
+ mgr.afterUnlock();
+
+ assertTrue(mgr.getCurrent() instanceof StartState);
+ }
+
+ @Test
+ public void testChangeState() throws Exception {
+ // start should invoke changeState()
+ startMgr();
+
+ int ntimes = 0;
+
+ // should have set the filter for the StartState
+ verify(dmaap, times(++ntimes)).setFilter(any());
+
+ /*
+ * now go offline while it's locked
+ */
+ lockMgr();
+
+ // should have set the new filter
+ verify(dmaap, times(++ntimes)).setFilter(any());
+
+ // should have cancelled the timer
+ assertEquals(1, futures.size());
+ verify(futures.poll()).cancel(false);
+
+ /*
+ * now go back online
+ */
+ unlockMgr();
+
+ // should have set the new filter
+ verify(dmaap, times(++ntimes)).setFilter(any());
+
+ // timer should still be active
+ assertEquals(1, futures.size());
+ verify(futures.poll(), never()).cancel(false);
+ }
+
+ @Test
+ public void testSetFilter() throws Exception {
+ // start should cause a filter to be set
+ startMgr();
+
+ verify(dmaap).setFilter(any());
+ }
+
+ @Test
+ public void testSetFilter_DmaapEx() throws Exception {
+
+ // generate an exception
+ doThrow(new PoolingFeatureException()).when(dmaap).setFilter(any());
+
+ // start should invoke setFilter()
+ startMgr();
+
+ // no exception, means success
+ }
+
+ @Test
+ public void testInternalTopicFailed() throws Exception {
+ startMgr();
+
+ CountDownLatch latch = mgr.internalTopicFailed();
+
+ // wait for the thread to complete
+ assertTrue(latch.await(2, TimeUnit.SECONDS));
+
+ verify(controller).stop();
+ }
+
+ @Test
+ public void testSchedule() throws Exception {
+ // must start the scheduler
+ startMgr();
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ mgr.schedule(STD_ACTIVE_HEARTBEAT_MS, xxx -> {
+ latch.countDown();
+ return null;
+ });
+
+ // capture the task
+ ArgumentCaptor<Runnable> taskCap = ArgumentCaptor.forClass(Runnable.class);
+ ArgumentCaptor<Long> timeCap = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<TimeUnit> unitCap = ArgumentCaptor.forClass(TimeUnit.class);
+
+ verify(sched, times(2)).schedule(taskCap.capture(), timeCap.capture(), unitCap.capture());
+
+ assertEquals(STD_ACTIVE_HEARTBEAT_MS, timeCap.getValue().longValue());
+ assertEquals(TimeUnit.MILLISECONDS, unitCap.getValue());
+
+ // execute it
+ taskCap.getValue().run();
+
+ assertEquals(0, latch.getCount());
+ }
+
+ @Test
+ public void testScheduleWithFixedDelay() throws Exception {
+ // must start the scheduler
+ startMgr();
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ mgr.scheduleWithFixedDelay(STD_HEARTBEAT_WAIT_MS, STD_ACTIVE_HEARTBEAT_MS, xxx -> {
+ latch.countDown();
+ return null;
+ });
+
+ // capture the task
+ ArgumentCaptor<Runnable> taskCap = ArgumentCaptor.forClass(Runnable.class);
+ ArgumentCaptor<Long> initCap = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> timeCap = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<TimeUnit> unitCap = ArgumentCaptor.forClass(TimeUnit.class);
+
+ verify(sched).scheduleWithFixedDelay(taskCap.capture(), initCap.capture(), timeCap.capture(),
+ unitCap.capture());
+
+ assertEquals(STD_HEARTBEAT_WAIT_MS, initCap.getValue().longValue());
+ assertEquals(STD_ACTIVE_HEARTBEAT_MS, timeCap.getValue().longValue());
+ assertEquals(TimeUnit.MILLISECONDS, unitCap.getValue());
+
+ // execute it
+ taskCap.getValue().run();
+
+ assertEquals(0, latch.getCount());
+ }
+
+ @Test
+ public void testPublishAdmin() throws Exception {
+ Offline msg = new Offline(mgr.getHost());
+ mgr.publishAdmin(msg);
+
+ assertEquals(Message.ADMIN, msg.getChannel());
+
+ verify(dmaap).publish(any());
+ }
+
+ @Test
+ public void testPublish() throws Exception {
+ Offline msg = new Offline(mgr.getHost());
+ mgr.publish("my.channel", msg);
+
+ assertEquals("my.channel", msg.getChannel());
+
+ verify(dmaap).publish(any());
+ }
+
+ @Test
+ public void testPublish_InvalidMsg() throws Exception {
+ // message is missing data
+ mgr.publish(Message.ADMIN, new Offline());
+
+ // should not have attempted to publish it
+ verify(dmaap, never()).publish(any());
+ }
+
+ @Test
+ public void testPublish_DmaapEx() throws Exception {
+
+ // generate exception
+ doThrow(new PoolingFeatureException()).when(dmaap).publish(any());
+
+ mgr.publish(Message.ADMIN, new Offline(mgr.getHost()));
+ }
+
+ @Test
+ public void testOnTopicEvent() throws Exception {
+ startMgr();
+
+ StartState st = (StartState) mgr.getCurrent();
+
+ /*
+ * give it its heart beat, that should cause it to transition to the
+ * Query state.
+ */
+ Heartbeat hb = new Heartbeat(mgr.getHost(), st.getHbTimestampMs());
+ hb.setChannel(Message.ADMIN);
+
+ String msg = ser.encodeMsg(hb);
+
+ mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, msg);
+
+ assertTrue(mgr.getCurrent() instanceof QueryState);
+ }
+
+ @Test
+ public void testOnTopicEvent_NullEvent() throws Exception {
+ startMgr();
+
+ mgr.onTopicEvent(CommInfrastructure.UEB, TOPIC2, null);
+ }
+
+ @Test
+ public void testBeforeOffer_Unlocked_NoIntercept() throws Exception {
+ startMgr();
+
+ assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT));
+ }
+
+ @Test
+ public void testBeforeOffer_Locked_NoIntercept() throws Exception {
+ startMgr();
+
+ lockMgr();
+
+ assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT));
+ }
+
+ @Test
+ public void testBeforeOffer_Locked_Intercept() throws Exception {
+ startMgr();
+ lockMgr();
+
+ // route the message to this host
+ mgr.startDistributing(makeAssignments(true));
+
+ CountDownLatch latch = catchRecursion(false);
+
+ Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID);
+ mgr.handle(msg);
+
+ verify(dmaap, times(START_PUB)).publish(any());
+ verify(controller).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT);
+
+ // ensure we made it past both beforeXxx() methods
+ assertEquals(0, latch.getCount());
+ }
+
+ @Test
+ public void testBeforeInsert_Intercept() throws Exception {
+ startMgr();
+ lockMgr();
+
+ // route the message to this host
+ mgr.startDistributing(makeAssignments(true));
+
+ CountDownLatch latch = catchRecursion(true);
+
+ Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID);
+ mgr.handle(msg);
+
+ verify(dmaap, times(START_PUB)).publish(any());
+ verify(controller).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT);
+
+ // ensure we made it past both beforeXxx() methods
+ assertEquals(0, latch.getCount());
+ }
+
+ @Test
+ public void testBeforeInsert_NoIntercept() throws Exception {
+ startMgr();
+
+ long tbegin = System.currentTimeMillis();
+
+ assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+ ArgumentCaptor<Forward> msgCap = ArgumentCaptor.forClass(Forward.class);
+ verify(eventQueue).add(msgCap.capture());
+
+ validateMessageContent(tbegin, msgCap.getValue());
+ }
+
+ @Test
+ public void testHandleExternalCommInfrastructureStringStringString_NullReqId() throws Exception {
+ startMgr();
+
+ when(extractors.extract(any())).thenReturn(null);
+
+ assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+ }
+
+ @Test
+ public void testHandleExternalCommInfrastructureStringStringString_EmptyReqId() throws Exception {
+ startMgr();
+
+ when(extractors.extract(any())).thenReturn("");
+
+ assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+ }
+
+ @Test
+ public void testHandleExternalCommInfrastructureStringStringString_InvalidMsg() throws Exception {
+ startMgr();
+
+ assertTrue(mgr.beforeInsert(null, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+ // should not have tried to enqueue a message
+ verify(eventQueue, never()).add(any());
+ }
+
+ @Test
+ public void testHandleExternalCommInfrastructureStringStringString() throws Exception {
+ startMgr();
+
+ long tbegin = System.currentTimeMillis();
+
+ assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+ ArgumentCaptor<Forward> msgCap = ArgumentCaptor.forClass(Forward.class);
+ verify(eventQueue).add(msgCap.capture());
+
+ validateMessageContent(tbegin, msgCap.getValue());
+ }
+
+ @Test
+ public void testHandleExternalForward_NoAssignments() throws Exception {
+ startMgr();
+
+ long tbegin = System.currentTimeMillis();
+
+ assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+ ArgumentCaptor<Forward> msgCap = ArgumentCaptor.forClass(Forward.class);
+ verify(eventQueue).add(msgCap.capture());
+
+ validateMessageContent(tbegin, msgCap.getValue());
+ }
+
+ @Test
+ public void testHandleExternalForward() throws Exception {
+ startMgr();
+
+ // route the message to this host
+ mgr.startDistributing(makeAssignments(true));
+
+ assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+ }
+
+ @Test
+ public void testHandleEvent_NullTarget() throws Exception {
+ startMgr();
+
+ // buckets have null targets
+ mgr.startDistributing(new BucketAssignments(new String[] {null, null}));
+
+ assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+ verify(dmaap, times(START_PUB)).publish(any());
+ }
+
+ @Test
+ public void testHandleEvent_SameHost() throws Exception {
+ startMgr();
+
+ // route the message to this host
+ mgr.startDistributing(makeAssignments(true));
+
+ assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+ verify(dmaap, times(START_PUB)).publish(any());
+ }
+
+ @Test
+ public void testHandleEvent_DiffHost_TooManyHops() throws Exception {
+ startMgr();
+
+ // route the message to this host
+ mgr.startDistributing(makeAssignments(false));
+
+ Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID);
+ msg.setNumHops(PoolingManagerImpl.MAX_HOPS + 1);
+ mgr.handle(msg);
+
+ // shouldn't publish
+ verify(dmaap, times(START_PUB)).publish(any());
+ verify(controller, never()).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT);
+ }
+
+ @Test
+ public void testHandleEvent_DiffHost_Forward() throws Exception {
+ startMgr();
+
+ // route the message to the *OTHER* host
+ mgr.startDistributing(makeAssignments(false));
+
+ assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+ verify(dmaap, times(START_PUB + 1)).publish(any());
+ }
+
+ @Test
+ public void testExtractRequestId_NullEvent() throws Exception {
+ startMgr();
+
+ assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, null));
+ }
+
+ @Test
+ public void testExtractRequestId_NullReqId() throws Exception {
+ startMgr();
+
+ when(extractors.extract(any())).thenReturn(null);
+
+ assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+ }
+
+ @Test
+ public void testExtractRequestId() throws Exception {
+ startMgr();
+
+ // route the message to the *OTHER* host
+ mgr.startDistributing(makeAssignments(false));
+
+ assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+ }
+
+ @Test
+ public void testDecodeEvent_CannotDecode() throws Exception {
+ startMgr();
+
+ when(controller.isLocked()).thenReturn(true);
+
+ // create assignments, though they are irrelevant
+ mgr.startDistributing(makeAssignments(false));
+
+ when(factory.canDecodeEvent(drools, TOPIC2)).thenReturn(false);
+
+ assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT));
+ }
+
+ @Test
+ public void testDecodeEvent_UnsuppEx() throws Exception {
+ startMgr();
+
+ when(controller.isLocked()).thenReturn(true);
+
+ // create assignments, though they are irrelevant
+ mgr.startDistributing(makeAssignments(false));
+
+ // generate exception
+ doThrow(new UnsupportedOperationException()).when(factory).decodeEvent(drools, TOPIC2, THE_EVENT);
+
+ assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT));
+ }
+
+ @Test
+ public void testDecodeEvent_ArgEx() throws Exception {
+ startMgr();
+
+ when(controller.isLocked()).thenReturn(true);
+
+ // create assignments, though they are irrelevant
+ mgr.startDistributing(makeAssignments(false));
+
+ // generate exception
+ doThrow(new IllegalArgumentException()).when(factory).decodeEvent(drools, TOPIC2, THE_EVENT);
+
+ assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT));
+ }
+
+ @Test
+ public void testDecodeEvent_StateEx() throws Exception {
+ startMgr();
+
+ when(controller.isLocked()).thenReturn(true);
+
+ // create assignments, though they are irrelevant
+ mgr.startDistributing(makeAssignments(false));
+
+ // generate exception
+ doThrow(new IllegalStateException()).when(factory).decodeEvent(drools, TOPIC2, THE_EVENT);
+
+ assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT));
+ }
+
+ @Test
+ public void testDecodeEvent() throws Exception {
+ startMgr();
+
+ when(controller.isLocked()).thenReturn(true);
+
+ // route to another host
+ mgr.startDistributing(makeAssignments(false));
+
+ assertTrue(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT));
+ }
+
+ @Test
+ public void testMakeForward() throws Exception {
+ startMgr();
+
+ long tbegin = System.currentTimeMillis();
+
+ assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+ ArgumentCaptor<Forward> msgCap = ArgumentCaptor.forClass(Forward.class);
+ verify(eventQueue).add(msgCap.capture());
+
+ validateMessageContent(tbegin, msgCap.getValue());
+ }
+
+ @Test
+ public void testMakeForward_InvalidMsg() throws Exception {
+ startMgr();
+
+ assertTrue(mgr.beforeInsert(null, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+ // should not have tried to enqueue a message
+ verify(eventQueue, never()).add(any());
+ }
+
+ @Test
+ public void testHandle_SameHost() throws Exception {
+ startMgr();
+
+ // route the message to this host
+ mgr.startDistributing(makeAssignments(true));
+
+ Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID);
+ mgr.handle(msg);
+
+ verify(dmaap, times(START_PUB)).publish(any());
+ verify(controller).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT);
+ }
+
+ @Test
+ public void testHandle_DiffHost() throws Exception {
+ startMgr();
+
+ // route the message to this host
+ mgr.startDistributing(makeAssignments(false));
+
+ Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID);
+ mgr.handle(msg);
+
+ verify(dmaap, times(START_PUB + 1)).publish(any());
+ verify(controller, never()).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT);
+ }
+
+ @Test
+ public void testInject() throws Exception {
+ startMgr();
+
+ // route the message to this host
+ mgr.startDistributing(makeAssignments(true));
+
+ CountDownLatch latch = catchRecursion(true);
+
+ Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID);
+ mgr.handle(msg);
+
+ verify(dmaap, times(START_PUB)).publish(any());
+ verify(controller).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT);
+
+ // ensure we made it past both beforeXxx() methods
+ assertEquals(0, latch.getCount());
+ }
+
+ @Test
+ public void testHandleInternal() throws Exception {
+ startMgr();
+
+ StartState st = (StartState) mgr.getCurrent();
+
+ /*
+ * give it its heart beat, that should cause it to transition to the
+ * Query state.
+ */
+ Heartbeat hb = new Heartbeat(mgr.getHost(), st.getHbTimestampMs());
+ hb.setChannel(Message.ADMIN);
+
+ String msg = ser.encodeMsg(hb);
+
+ mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, msg);
+
+ assertTrue(mgr.getCurrent() instanceof QueryState);
+ }
+
+ @Test
+ public void testHandleInternal_IOEx() throws Exception {
+ startMgr();
+
+ mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, "invalid message");
+
+ assertTrue(mgr.getCurrent() instanceof StartState);
+ }
+
+ @Test
+ public void testHandleInternal_PoolEx() throws Exception {
+ startMgr();
+
+ StartState st = (StartState) mgr.getCurrent();
+
+ Heartbeat hb = new Heartbeat(mgr.getHost(), st.getHbTimestampMs());
+
+ /*
+ * do NOT set the channel - this will cause the message to be invalid,
+ * triggering an exception
+ */
+
+ String msg = ser.encodeMsg(hb);
+
+ mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, msg);
+
+ assertTrue(mgr.getCurrent() instanceof StartState);
+ }
+
+ @Test
+ public void testStartDistributing() throws Exception {
+ startMgr();
+
+ // route the message to this host
+ mgr.startDistributing(makeAssignments(true));
+
+ assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+
+ // null assignments should be ignored
+ mgr.startDistributing(null);
+
+ assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+
+
+ // route the message to the other host
+ mgr.startDistributing(makeAssignments(false));
+
+ assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT));
+ }
+
+ @Test
+ public void testStartDistributing_EventsInQueue_ProcessLocally() throws Exception {
+ startMgr();
+
+ // put items in the queue
+ LinkedList<Forward> lst = new LinkedList<>();
+ lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID));
+ lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID));
+ lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID));
+
+ when(eventQueue.poll()).thenAnswer(args -> lst.poll());
+
+ // route the messages to this host
+ mgr.startDistributing(makeAssignments(true));
+
+ // all of the events should have been processed locally
+ verify(dmaap, times(START_PUB)).publish(any());
+ verify(controller, times(3)).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT);
+ }
+
+ @Test
+ public void testStartDistributing_EventsInQueue_Forward() throws Exception {
+ startMgr();
+
+ // put items in the queue
+ LinkedList<Forward> lst = new LinkedList<>();
+ lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID));
+ lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID));
+ lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID));
+
+ when(eventQueue.poll()).thenAnswer(args -> lst.poll());
+
+ // route the messages to the OTHER host
+ mgr.startDistributing(makeAssignments(false));
+
+ // all of the events should have been forwarded
+ verify(dmaap, times(4)).publish(any());
+ verify(controller, never()).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT);
+ }
+
+ @Test
+ public void testGoStart() {
+ State st = mgr.goStart();
+ assertTrue(st instanceof StartState);
+ assertEquals(mgr.getHost(), st.getHost());
+ }
+
+ @Test
+ public void testGoQuery() {
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST2});
+ mgr.startDistributing(asgn);
+
+ State st = mgr.goQuery();
+
+ assertTrue(st instanceof QueryState);
+ assertEquals(mgr.getHost(), st.getHost());
+ assertEquals(asgn, mgr.getAssignments());
+ }
+
+ @Test
+ public void testGoActive() {
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST2});
+ mgr.startDistributing(asgn);
+
+ State st = mgr.goActive();
+
+ assertTrue(st instanceof ActiveState);
+ assertEquals(mgr.getHost(), st.getHost());
+ assertEquals(asgn, mgr.getAssignments());
+ }
+
+ @Test
+ public void testGoInactive() {
+ State st = mgr.goInactive();
+ assertTrue(st instanceof InactiveState);
+ assertEquals(mgr.getHost(), st.getHost());
+ }
+
+ @Test
+ public void testTimerActionRun() throws Exception {
+ // must start the scheduler
+ startMgr();
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ mgr.schedule(STD_ACTIVE_HEARTBEAT_MS, xxx -> {
+ latch.countDown();
+ return null;
+ });
+
+ // capture the task
+ ArgumentCaptor<Runnable> taskCap = ArgumentCaptor.forClass(Runnable.class);
+
+ verify(sched, times(2)).schedule(taskCap.capture(), any(Long.class), any(TimeUnit.class));
+
+ // execute it
+ taskCap.getValue().run();
+
+ assertEquals(0, latch.getCount());
+ }
+
+ @Test
+ public void testTimerActionRun_DiffState() throws Exception {
+ // must start the scheduler
+ startMgr();
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ mgr.schedule(STD_ACTIVE_HEARTBEAT_MS, xxx -> {
+ latch.countDown();
+ return null;
+ });
+
+ // capture the task
+ ArgumentCaptor<Runnable> taskCap = ArgumentCaptor.forClass(Runnable.class);
+
+ verify(sched, times(2)).schedule(taskCap.capture(), any(Long.class), any(TimeUnit.class));
+
+ // give it a heartbeat so that it transitions to the query state
+ StartState st = (StartState) mgr.getCurrent();
+ Heartbeat hb = new Heartbeat(mgr.getHost(), st.getHbTimestampMs());
+ hb.setChannel(Message.ADMIN);
+
+ String msg = ser.encodeMsg(hb);
+
+ mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, msg);
+
+ assertTrue(mgr.getCurrent() instanceof QueryState);
+
+ // execute it
+ taskCap.getValue().run();
+
+ // it should NOT have counted down
+ assertEquals(1, latch.getCount());
+ }
+
+ /**
+ * Validates the message content.
+ *
+ * @param tbegin creation time stamp must be no less than this
+ * @param msg message to be validated
+ */
+ private void validateMessageContent(long tbegin, Forward msg) {
+ assertEquals(0, msg.getNumHops());
+ assertTrue(msg.getCreateTimeMs() >= tbegin);
+ assertEquals(mgr.getHost(), msg.getSource());
+ assertEquals(CommInfrastructure.UEB, msg.getProtocol());
+ assertEquals(TOPIC2, msg.getTopic());
+ assertEquals(THE_EVENT, msg.getPayload());
+ assertEquals(REQUEST_ID, msg.getRequestId());
+ }
+
+ /**
+ * Configure the mock controller to act like a real controller, invoking
+ * beforeOffer and then beforeInsert, so we can make sure they pass through.
+ * We'll keep count to ensure we don't get into infinite recursion.
+ *
+ * @param invokeBeforeInsert {@code true} if beforeInsert() should be
+ * invoked, {@code false} if it should be skipped
+ *
+ * @return a latch that will be counted down if both beforeXxx() methods
+ * return false
+ */
+ private CountDownLatch catchRecursion(boolean invokeBeforeInsert) {
+ CountDownLatch recursion = new CountDownLatch(3);
+ CountDownLatch latch = new CountDownLatch(1);
+
+ doAnswer(args -> {
+
+ recursion.countDown();
+ if (recursion.getCount() == 0) {
+ fail("recursive calls to onTopicEvent");
+ }
+
+ int iarg = 0;
+ CommInfrastructure proto = args.getArgumentAt(iarg++, CommInfrastructure.class);
+ String topic = args.getArgumentAt(iarg++, String.class);
+ String event = args.getArgumentAt(iarg++, String.class);
+
+ if (mgr.beforeOffer(proto, topic, event)) {
+ return null;
+ }
+
+ if (invokeBeforeInsert && mgr.beforeInsert(proto, topic, event, DECODED_EVENT)) {
+ return null;
+ }
+
+ latch.countDown();
+
+ return null;
+ }).when(controller).onTopicEvent(any(), any(), any());
+
+ return latch;
+ }
+
+ /**
+ * Makes an assignment with two buckets.
+ *
+ * @param sameHost {@code true} if the {@link #REQUEST_ID} should has to the
+ * manager's bucket, {@code false} if it should hash to the other
+ * host's bucket
+ * @return a new bucket assignment
+ */
+ private BucketAssignments makeAssignments(boolean sameHost) {
+ int slot = REQUEST_ID.hashCode() % 2;
+
+ // slot numbers are 0 and 1 - reverse them if it's for a different host
+ if (!sameHost) {
+ slot = 1 - slot;
+ }
+
+ String[] asgn = new String[2];
+ asgn[slot] = mgr.getHost();
+ asgn[1 - slot] = HOST2;
+
+ return new BucketAssignments(asgn);
+ }
+
+ /**
+ * Invokes methods necessary to start the manager.
+ *
+ * @throws PoolingFeatureException if an error occurs
+ */
+ private void startMgr() throws PoolingFeatureException {
+ mgr.beforeStart();
+ mgr.afterStart();
+ }
+
+ /**
+ * Invokes methods necessary to lock the manager.
+ */
+ private void lockMgr() {
+ mgr.beforeLock();
+ }
+
+ /**
+ * Invokes methods necessary to unlock the manager.
+ */
+ private void unlockMgr() {
+ mgr.afterUnlock();
+ }
+
+ /**
+ * Used to create a mock object that implements both super interfaces.
+ */
+ private static interface ListeningController extends TopicListener, PolicyController {
+
+ }
+
+ /**
+ * Invokes a method that is expected to throw an exception.
+ *
+ * @param exClass class of exception that is expected
+ * @param func function to invoke
+ * @return the exception that was thrown
+ * @throws AssertionError if no exception was thrown
+ */
+ private <T extends Exception> T expectException(Class<T> exClass, ExFunction<T> func) {
+ try {
+ func.apply(null);
+ throw new AssertionError("missing exception");
+
+ } catch (Exception e) {
+ return exClass.cast(e);
+ }
+ }
+
+ /**
+ * Function that is expected to throw an exception.
+ *
+ * @param <T> type of exception the function is expected to throw
+ */
+ @FunctionalInterface
+ private static interface ExFunction<T extends Exception> {
+
+ /**
+ * Invokes the function.
+ *
+ * @param arg always {@code null}
+ * @throws T if an error occurs
+ */
+ public void apply(Void arg) throws T;
+
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java
new file mode 100644
index 00000000..63eb59d4
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java
@@ -0,0 +1,178 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.onap.policy.common.utils.properties.SpecPropertyConfiguration.generalize;
+import static org.onap.policy.common.utils.properties.SpecPropertyConfiguration.specialize;
+import static org.onap.policy.drools.pooling.PoolingProperties.ACTIVE_HEARTBEAT_MS;
+import static org.onap.policy.drools.pooling.PoolingProperties.FEATURE_ENABLED;
+import static org.onap.policy.drools.pooling.PoolingProperties.IDENTIFICATION_MS;
+import static org.onap.policy.drools.pooling.PoolingProperties.INTER_HEARTBEAT_MS;
+import static org.onap.policy.drools.pooling.PoolingProperties.OFFLINE_AGE_MS;
+import static org.onap.policy.drools.pooling.PoolingProperties.OFFLINE_LIMIT;
+import static org.onap.policy.drools.pooling.PoolingProperties.POOLING_TOPIC;
+import static org.onap.policy.drools.pooling.PoolingProperties.REACTIVATE_MS;
+import static org.onap.policy.drools.pooling.PoolingProperties.START_HEARTBEAT_MS;
+import java.util.Properties;
+import java.util.function.Function;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.utils.properties.exception.PropertyException;
+
+public class PoolingPropertiesTest {
+
+ private static final String CONTROLLER = "a.controller";
+
+ private static final String STD_POOLING_TOPIC = "my.topic";
+ public static final boolean STD_FEATURE_ENABLED = true;
+ public static final int STD_OFFLINE_LIMIT = 10;
+ public static final long STD_OFFLINE_AGE_MS = 1000L;
+ public static final long STD_START_HEARTBEAT_MS = 2000L;
+ public static final long STD_REACTIVATE_MS = 3000L;
+ public static final long STD_IDENTIFICATION_MS = 4000L;
+ public static final long STD_LEADER_MS = 5000L;
+ public static final long STD_ACTIVE_HEARTBEAT_MS = 6000L;
+ public static final long STD_INTER_HEARTBEAT_MS = 7000L;
+
+ private Properties plain;
+ private PoolingProperties pooling;
+
+ @Before
+ public void setUp() throws Exception {
+ plain = makeProperties();
+
+ pooling = new PoolingProperties(CONTROLLER, plain);
+ }
+
+ @Test
+ public void testPoolingProperties() throws PropertyException {
+ // ensure no exceptions
+ new PoolingProperties(CONTROLLER, plain);
+ }
+
+ @Test
+ public void testGetSource() {
+ assertEquals(plain, pooling.getSource());
+ }
+
+ @Test
+ public void testGetPoolingTopic() {
+ assertEquals(STD_POOLING_TOPIC, pooling.getPoolingTopic());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetPoolingTopic_Generalize() {
+ // shouldn't be able to generalize the topic
+ generalize(POOLING_TOPIC);
+ }
+
+ @Test
+ public void testGetOfflineLimit() throws PropertyException {
+ doTest(OFFLINE_LIMIT, STD_OFFLINE_LIMIT, 1000, xxx -> pooling.getOfflineLimit());
+ }
+
+ @Test
+ public void testGetOfflineAgeMs() throws PropertyException {
+ doTest(OFFLINE_AGE_MS, STD_OFFLINE_AGE_MS, 60000L, xxx -> pooling.getOfflineAgeMs());
+ }
+
+ @Test
+ public void testGetStartHeartbeatMs() throws PropertyException {
+ doTest(START_HEARTBEAT_MS, STD_START_HEARTBEAT_MS, 50000L, xxx -> pooling.getStartHeartbeatMs());
+ }
+
+ @Test
+ public void testGetReactivateMs() throws PropertyException {
+ doTest(REACTIVATE_MS, STD_REACTIVATE_MS, 50000L, xxx -> pooling.getReactivateMs());
+ }
+
+ @Test
+ public void testGetIdentificationMs() throws PropertyException {
+ doTest(IDENTIFICATION_MS, STD_IDENTIFICATION_MS, 50000L, xxx -> pooling.getIdentificationMs());
+ }
+
+ @Test
+ public void testGetActiveHeartbeatMs() throws PropertyException {
+ doTest(ACTIVE_HEARTBEAT_MS, STD_ACTIVE_HEARTBEAT_MS, 50000L, xxx -> pooling.getActiveHeartbeatMs());
+ }
+
+ @Test
+ public void testGetInterHeartbeatMs() throws PropertyException {
+ doTest(INTER_HEARTBEAT_MS, STD_INTER_HEARTBEAT_MS, 15000L, xxx -> pooling.getInterHeartbeatMs());
+ }
+
+ /**
+ * Tests a particular property. Verifies that the correct value is returned
+ * if the specialized property has a value or the property has no value.
+ * Also verifies that the property name can be generalized.
+ *
+ * @param propnm name of the property of interest
+ * @param specValue expected specialized value
+ * @param dfltValue expected default value
+ * @param func function to get the field
+ * @throws PropertyException if an error occurs
+ */
+ private <T> void doTest(String propnm, T specValue, T dfltValue, Function<Void, T> func) throws PropertyException {
+ /*
+ * With specialized property
+ */
+ pooling = new PoolingProperties(CONTROLLER, plain);
+ assertEquals("special " + propnm, specValue, func.apply(null));
+
+ /*
+ * Ensure the property supports generalization - this will throw an
+ * exception if it does not.
+ */
+ assertFalse(propnm.equals(generalize(propnm)));
+
+ /*
+ * Without the property - should use the default value.
+ */
+ plain.remove(specialize(propnm, CONTROLLER));
+ plain.remove(generalize(propnm));
+ pooling = new PoolingProperties(CONTROLLER, plain);
+ assertEquals("default " + propnm, dfltValue, func.apply(null));
+ }
+
+ /**
+ * Makes a set of properties, where all of the properties are specialized
+ * for the controller.
+ *
+ * @return a new property set
+ */
+ private Properties makeProperties() {
+ Properties props = new Properties();
+
+ props.setProperty(specialize(POOLING_TOPIC, CONTROLLER), STD_POOLING_TOPIC);
+ props.setProperty(specialize(FEATURE_ENABLED, CONTROLLER), "" + STD_FEATURE_ENABLED);
+ props.setProperty(specialize(OFFLINE_LIMIT, CONTROLLER), "" + STD_OFFLINE_LIMIT);
+ props.setProperty(specialize(OFFLINE_AGE_MS, CONTROLLER), "" + STD_OFFLINE_AGE_MS);
+ props.setProperty(specialize(START_HEARTBEAT_MS, CONTROLLER), "" + STD_START_HEARTBEAT_MS);
+ props.setProperty(specialize(REACTIVATE_MS, CONTROLLER), "" + STD_REACTIVATE_MS);
+ props.setProperty(specialize(IDENTIFICATION_MS, CONTROLLER), "" + STD_IDENTIFICATION_MS);
+ props.setProperty(specialize(ACTIVE_HEARTBEAT_MS, CONTROLLER), "" + STD_ACTIVE_HEARTBEAT_MS);
+ props.setProperty(specialize(INTER_HEARTBEAT_MS, CONTROLLER), "" + STD_INTER_HEARTBEAT_MS);
+
+ return props;
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java
new file mode 100644
index 00000000..4206a836
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java
@@ -0,0 +1,96 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeAnd;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeEquals;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeOr;
+import java.util.Map;
+import java.util.TreeMap;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Query;
+
+public class SerializerTest {
+
+ @Test
+ public void testSerializer() {
+ new Serializer();
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testEncodeFilter() throws Exception {
+ Serializer ser = new Serializer();
+
+ /*
+ * Ensure raw maps serialize as expected. Use a TreeMap so the field
+ * order is predictable.
+ */
+ Map<String, Object> top = new TreeMap<>();
+ Map<String, Object> inner = new TreeMap<>();
+ top.put("abc", 20);
+ top.put("def", inner);
+ top.put("ghi", true);
+ inner.put("xyz", 30);
+ assertEquals("{'abc':20,'def':{'xyz':30},'ghi':true}".replace('\'', '"'), ser.encodeFilter(top));
+
+ /*
+ * Ensure we can encode a complicated filter without throwing an
+ * exception
+ */
+ Map<String, Object> complexFilter = makeAnd(makeEquals("fieldC", "valueC"),
+ makeOr(makeEquals("fieldA", "valueA"), makeEquals("fieldB", "valueB")));
+ String val = ser.encodeFilter(complexFilter);
+ assertFalse(val.isEmpty());
+ }
+
+ @Test
+ public void testEncodeMsg_testDecodeMsg() throws Exception {
+ Serializer ser = new Serializer();
+
+ Query msg = new Query("hostA");
+ msg.setChannel("channelB");
+
+ String encoded = ser.encodeMsg(msg);
+ assertNotNull(encoded);
+
+ Message decoded = ser.decodeMsg(encoded);
+ assertEquals(Query.class, decoded.getClass());
+
+ assertEquals(msg.getSource(), decoded.getSource());
+ assertEquals(msg.getChannel(), decoded.getChannel());
+
+ // should work a second time, too
+ encoded = ser.encodeMsg(msg);
+ assertNotNull(encoded);
+
+ decoded = ser.decodeMsg(encoded);
+ assertEquals(Query.class, decoded.getClass());
+
+ assertEquals(msg.getSource(), decoded.getSource());
+ assertEquals(msg.getChannel(), decoded.getChannel());
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SpecPropertiesTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SpecPropertiesTest.java
new file mode 100644
index 00000000..8b495099
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SpecPropertiesTest.java
@@ -0,0 +1,186 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import java.util.Properties;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SpecPropertiesTest {
+
+ /**
+ * Property prefix of interest.
+ */
+ private static final String MY_PREFIX = "my.prefix";
+
+ /**
+ * Specialization, which follows the prefix.
+ */
+ private static final String MY_SPEC = "my.spec";
+
+ /**
+ * Generalized prefix (i.e., without the spec).
+ */
+ private static final String PREFIX_GEN = MY_PREFIX + ".";
+
+ /**
+ * Specialized prefix (i.e., with the spec).
+ */
+ private static final String PREFIX_SPEC = PREFIX_GEN + MY_SPEC + ".";
+
+ /**
+ * Suffix to add to property names to generate names of properties that are
+ * not populated.
+ */
+ private static final String SUFFIX = ".suffix";
+
+ /**
+ * Property name without a prefix.
+ */
+ private static final String PROP_NO_PREFIX = "other";
+
+ /**
+ * Generalized property name (i.e., without the spec).
+ */
+ private static final String PROP_GEN = PREFIX_GEN + "generalized";
+
+ // property names that include the spec
+ private static final String PROP_SPEC = PREFIX_SPEC + "specialized";
+ private static final String PROP_UNKNOWN = PREFIX_SPEC + "unknown";
+
+ // property values
+ private static final String VAL_NO_PREFIX = "no-prefix";
+ private static final String VAL_GEN = "gen";
+ private static final String VAL_SPEC = "spec";
+
+ private static final String VAL_DEFAULT = "default value";
+
+ private Properties supportingProps;
+ private SpecProperties props;
+
+ @Before
+ public void setUp() {
+ supportingProps = new Properties();
+
+ supportingProps.setProperty(PROP_NO_PREFIX, VAL_NO_PREFIX);
+ supportingProps.setProperty(PROP_GEN, VAL_GEN);
+ supportingProps.setProperty(PROP_SPEC, VAL_SPEC);
+
+ props = new SpecProperties(MY_PREFIX, MY_SPEC);
+
+ props.putAll(supportingProps);
+ }
+
+ @Test
+ public void testSpecPropertiesStringString() {
+
+ // no supporting properties
+ props = new SpecProperties(MY_PREFIX, MY_SPEC);
+
+ assertEquals(PREFIX_GEN, props.getPrefix());
+ assertEquals(PREFIX_SPEC, props.getSpecPrefix());
+
+ // everything is null
+ assertNull(props.getProperty(gen(PROP_NO_PREFIX)));
+ assertNull(props.getProperty(gen(PROP_GEN)));
+ assertNull(props.getProperty(gen(PROP_SPEC)));
+ assertNull(props.getProperty(gen(PROP_UNKNOWN)));
+ }
+
+ @Test
+ public void testSpecPropertiesStringStringProperties() {
+
+ // use supportingProps as default properties
+ props = new SpecProperties(MY_PREFIX, MY_SPEC, supportingProps);
+
+ assertEquals(PREFIX_GEN, props.getPrefix());
+ assertEquals(PREFIX_SPEC, props.getSpecPrefix());
+
+ assertEquals(VAL_NO_PREFIX, props.getProperty(gen(PROP_NO_PREFIX)));
+ assertEquals(VAL_GEN, props.getProperty(gen(PROP_GEN)));
+ assertEquals(VAL_SPEC, props.getProperty(gen(PROP_SPEC)));
+ assertNull(props.getProperty(gen(PROP_UNKNOWN)));
+ }
+
+ @Test
+ public void testWithTrailingDot() {
+ // neither has trailing dot
+ assertEquals(PREFIX_GEN, props.getPrefix());
+ assertEquals(PREFIX_SPEC, props.getSpecPrefix());
+
+ // both have trailing dot
+ props = new SpecProperties(PREFIX_GEN, MY_SPEC + ".");
+ assertEquals(PREFIX_GEN, props.getPrefix());
+ assertEquals(PREFIX_SPEC, props.getSpecPrefix());
+ }
+
+ @Test
+ public void testGetPropertyString() {
+ // the key does contain the prefix
+ assertEquals(VAL_NO_PREFIX, props.getProperty(gen(PROP_NO_PREFIX)));
+ assertNull(props.getProperty(gen(PROP_NO_PREFIX + SUFFIX)));
+
+ // specialized value exists
+ assertEquals(VAL_GEN, props.getProperty(gen(PROP_GEN)));
+ assertNull(props.getProperty(gen(PROP_GEN + SUFFIX)));
+
+ // generalized value exists
+ assertEquals(VAL_SPEC, props.getProperty(gen(PROP_SPEC)));
+ assertNull(props.getProperty(gen(PROP_SPEC + SUFFIX)));
+
+ // not found
+ assertNull(props.getProperty(gen(PROP_UNKNOWN)));
+ assertNull(props.getProperty(gen(PROP_UNKNOWN + SUFFIX)));
+ }
+
+ @Test
+ public void testGetPropertyStringString() {
+ // the key does contain the prefix
+ assertEquals(VAL_NO_PREFIX, props.getProperty(gen(PROP_NO_PREFIX), VAL_DEFAULT));
+ assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_NO_PREFIX + SUFFIX), VAL_DEFAULT));
+
+ // specialized value exists
+ assertEquals(VAL_GEN, props.getProperty(gen(PROP_GEN), VAL_DEFAULT));
+ assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_GEN + SUFFIX), VAL_DEFAULT));
+
+ // generalized value exists
+ assertEquals(VAL_SPEC, props.getProperty(gen(PROP_SPEC), VAL_DEFAULT));
+ assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_SPEC + SUFFIX), VAL_DEFAULT));
+
+ // not found
+ assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_UNKNOWN), VAL_DEFAULT));
+ assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_UNKNOWN + SUFFIX), VAL_DEFAULT));
+
+ // can return null
+ assertNull(props.getProperty(gen(PROP_UNKNOWN), null));
+ }
+
+ private String gen(String propnm) {
+ if (propnm.startsWith(PREFIX_SPEC)) {
+ return PREFIX_GEN + propnm.substring(PREFIX_SPEC.length());
+ }
+
+ return propnm;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTest.java
new file mode 100644
index 00000000..e9246430
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTest.java
@@ -0,0 +1,440 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.function.Function;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ClassExtractorsTest {
+
+ private static final int NTIMES = 5;
+
+ private static final String MY_TYPE = "theType";
+ private static final String PROP_PREFIX = "extractor." + MY_TYPE + ".";
+
+ private static final String VALUE = "a value";
+ private static final Integer INT_VALUE = 10;
+ private static final Integer INT_VALUE2 = 20;
+
+ private Properties props;
+ private ClassExtractors map;
+
+ @Before
+ public void setUp() {
+ props = new Properties();
+
+ props.setProperty(PROP_PREFIX + Simple.class.getName(), "${intValue}");
+ props.setProperty(PROP_PREFIX + WithString.class.getName(), "${strValue}");
+
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+ }
+
+ @Test
+ public void testExtract() {
+ Simple obj = new Simple();
+ assertEquals(INT_VALUE, map.extract(obj));
+
+ // string value
+ assertEquals(VALUE, tryIt(Simple.class, "${strValue}", xxx -> new Simple()));
+
+ // null object
+ assertNull(map.extract(null));
+
+ // values from two different kinds of objects
+ props = new Properties();
+ props.setProperty(PROP_PREFIX + Simple.class.getName(), "${intValue}");
+ props.setProperty(PROP_PREFIX + WithString.class.getName(), "${strValue}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+
+ assertEquals(INT_VALUE, map.extract(new Simple()));
+ assertEquals(VALUE, map.extract(new Sub()));
+
+ // values from a superclass method, but property defined for subclass
+ props = new Properties();
+ props.setProperty(PROP_PREFIX + Sub.class.getName(), "${strValue}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+
+ assertEquals(VALUE, map.extract(new Sub()));
+
+ // values from a superclass field, but property defined for subclass
+ props = new Properties();
+ props.setProperty(PROP_PREFIX + Sub.class.getName(), "${intValue}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+
+ assertEquals(INT_VALUE, map.extract(new Sub()));
+
+
+ // prefix includes trailing "."
+ props = new Properties();
+ props.setProperty(PROP_PREFIX + Simple.class.getName(), "${intValue}");
+ map = new ClassExtractors(props, PROP_PREFIX.substring(0, PROP_PREFIX.length() - 1), MY_TYPE);
+ assertEquals(INT_VALUE, map.extract(new Simple()));
+
+
+ // values from an class in a different file
+ props = new Properties();
+ props.setProperty(PROP_PREFIX + ClassExtractorsTestSupport.class.getName(), "${nested.theValue}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+
+ assertEquals(ClassExtractorsTestSupport2.NESTED_VALUE, map.extract(new ClassExtractorsTestSupport()));
+ }
+
+ @Test
+ public void testGetExtractor() {
+ Simple obj = new Simple();
+
+ // repeat - shouldn't re-create the extractor
+ for (int x = 0; x < NTIMES; ++x) {
+ assertEquals("x=" + x, INT_VALUE, map.extract(obj));
+ assertEquals("x=" + x, 1, map.size());
+ }
+ }
+
+ @Test
+ public void testBuildExtractorClass_TopLevel() {
+ // extractor defined for top-level class
+ props = new Properties();
+ props.setProperty(PROP_PREFIX + Sub.class.getName(), "${strValue}");
+
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+ assertEquals(VALUE, map.extract(new Sub()));
+
+ // one extractor for top-level class
+ assertEquals(1, map.size());
+ }
+
+ @Test
+ public void testBuildExtractorClass_SuperClass() {
+ // extractor defined for superclass (interface)
+ assertEquals(VALUE, map.extract(new Sub()));
+
+ // one extractor for top-level class and one for interface
+ assertEquals(2, map.size());
+ }
+
+ @Test
+ public void testBuildExtractorClass_NotDefined() {
+ // no extractor defined for "this" class
+ assertNull(map.extract(this));
+
+ // one NULL extractor for top-level class
+ assertEquals(1, map.size());
+ }
+
+ @Test
+ public void testBuildExtractorClassString() {
+ // no leading "${"
+ assertNull(tryIt(Simple.class, "intValue}", xxx -> new Simple()));
+
+ // no trailing "}"
+ assertNull(tryIt(Simple.class, "${intValue", xxx -> new Simple()));
+
+ // leading "."
+ assertNull(tryIt(Sub.class, "${.simple.strValue}", xxx -> new Sub()));
+
+ // trailing "."
+ assertNull(tryIt(Sub.class, "${simple.strValue.}", xxx -> new Sub()));
+
+ // one component
+ assertEquals(VALUE, tryIt(Sub.class, "${strValue}", xxx -> new Sub()));
+
+ // two components
+ assertEquals(VALUE, tryIt(Sub.class, "${simple.strValue}", xxx -> new Sub()));
+
+ // invalid component
+ assertNull(tryIt(Sub.class, "${unknown}", xxx -> new Sub()));
+ }
+
+ @Test
+ public void testGetClassExtractor_InSuper() {
+ // field in the superclass
+ assertEquals(INT_VALUE, tryIt(Super.class, "${intValue}", xxx -> new Sub()));
+ }
+
+ @Test
+ public void testGetClassExtractor_InInterface() {
+ // defined in the interface
+ assertEquals(VALUE, map.extract(new Sub()));
+ }
+
+ @Test
+ public void testNullExtractorExtract() {
+ // empty properties - should only create NullExtractor
+ map = new ClassExtractors(new Properties(), PROP_PREFIX, MY_TYPE);
+
+ Simple obj = new Simple();
+
+ // repeat - shouldn't re-create the extractor
+ for (int x = 0; x < NTIMES; ++x) {
+ assertNull("x=" + x, map.extract(obj));
+ assertEquals("x=" + x, 1, map.size());
+ }
+ }
+
+ @Test
+ public void testComponetizedExtractor() {
+ // one component
+ assertEquals(VALUE, tryIt(Sub.class, "${strValue}", xxx -> new Sub()));
+
+ // three components
+ assertEquals(VALUE, tryIt(Sub.class, "${cont.data.strValue}", xxx -> new Sub()));
+ }
+
+ @Test
+ public void testComponetizedExtractorBuildExtractor_Method() {
+ assertEquals(INT_VALUE, tryIt(Simple.class, "${intValue}", xxx -> new Simple()));
+ }
+
+ @Test
+ public void testComponetizedExtractorBuildExtractor_Field() {
+ assertEquals(VALUE, tryIt(Simple.class, "${strValue}", xxx -> new Simple()));
+ }
+
+ @Test
+ public void testComponetizedExtractorBuildExtractor_Map() {
+ Map<String, Object> inner = new TreeMap<>();
+ inner.put("inner1", "abc1");
+ inner.put("inner2", "abc2");
+
+ Map<String, Object> outer = new TreeMap<>();
+ outer.put("outer1", "def1");
+ outer.put("outer2", inner);
+
+ Simple obj = new Simple();
+
+ props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+ assertEquals(null, map.extract(obj));
+
+ obj.mapValue = outer;
+ props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue.outer2.inner2}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+ assertEquals("abc2", map.extract(obj));
+ }
+
+ @Test
+ public void testComponetizedExtractorBuildExtractor_Unknown() {
+ assertNull(tryIt(Simple.class, "${unknown2}", xxx -> new Simple()));
+ }
+
+ @Test
+ public void testComponetizedExtractorExtract_MiddleNull() {
+ // data component is null
+ assertEquals(null, tryIt(Sub.class, "${cont.data.strValue}", xxx -> {
+ Sub obj = new Sub();
+ obj.cont.simpleValue = null;
+ return obj;
+ }));
+ }
+
+ @Test
+ public void testComponetizedExtractorGetMethodExtractor_VoidMethod() {
+ // tell it to use getVoidValue()
+ props.setProperty(PROP_PREFIX + Simple.class.getName(), "${voidValue}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+
+ Simple obj = new Simple();
+ assertNull(map.extract(obj));
+
+ assertFalse(obj.voidInvoked);
+ }
+
+ @Test
+ public void testComponetizedExtractorGetMethodExtractor() {
+ assertEquals(INT_VALUE, map.extract(new Simple()));
+ }
+
+ @Test
+ public void testComponetizedExtractorGetFieldExtractor() {
+ // use a field
+ assertEquals(VALUE, tryIt(Simple.class, "${strValue}", xxx -> new Simple()));
+ }
+
+ @Test
+ public void testComponetizedExtractorGetMapExtractor() {
+ Map<String, Object> inner = new TreeMap<>();
+ inner.put("inner1", "abc1");
+ inner.put("inner2", "abc2");
+
+ Map<String, Object> outer = new TreeMap<>();
+ outer.put("outer1", "def1");
+ outer.put("outer2", inner);
+
+ Simple obj = new Simple();
+
+ obj.mapValue = outer;
+ props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue.outer2.inner2}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+ assertEquals("abc2", map.extract(obj));
+ }
+
+ @Test
+ public void testComponetizedExtractorGetMapExtractor_MapSubclass() {
+ Map<String, Object> inner = new TreeMap<>();
+ inner.put("inner1", "abc1");
+ inner.put("inner2", "abc2");
+
+ MapSubclass outer = new MapSubclass();
+ outer.put("outer1", "def1");
+ outer.put("outer2", inner);
+
+ Simple obj = new Simple();
+
+ props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+ assertEquals(null, map.extract(obj));
+
+ obj.mapValue = outer;
+ props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue.outer2.inner2}");
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+ assertEquals("abc2", map.extract(obj));
+ }
+
+ /**
+ * Sets a property for the given class, makes an object, and then returns
+ * the value extracted.
+ *
+ * @param clazz class whose property is to be set
+ * @param propval value to which to set the property
+ * @param makeObj function to create the object whose data is to be
+ * extracted
+ * @return the extracted data, or {@code null} if nothing was extracted
+ */
+ private Object tryIt(Class<?> clazz, String propval, Function<Void, Object> makeObj) {
+ Properties props = new Properties();
+ props.setProperty(PROP_PREFIX + clazz.getName(), propval);
+
+ map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE);
+
+ return map.extract(makeObj.apply(null));
+ }
+
+ /**
+ * A Map subclass, used to verify that getMapExtractor() still handles it.
+ */
+ private static class MapSubclass extends TreeMap<String, Object> {
+ private static final long serialVersionUID = 1L;
+
+ }
+
+ /**
+ * A simple class.
+ */
+ private static class Simple {
+
+ /**
+ * This will not be used because getIntValue() will override it.
+ */
+ @SuppressWarnings("unused")
+ private int intValue = INT_VALUE2;
+
+ /**
+ * Used to verify retrieval via a field name.
+ */
+ @SuppressWarnings("unused")
+ private String strValue = VALUE;
+
+ /**
+ * Used to verify retrieval within maps.
+ */
+ @SuppressWarnings("unused")
+ private Map<String, Object> mapValue = null;
+
+ /**
+ * {@code True} if {@link #getVoidValue()} was invoked, {@code false}
+ * otherwise.
+ */
+ private boolean voidInvoked = false;
+
+ /**
+ * This function will supercede the value in the "intValue" field.
+ *
+ * @return INT_VALUE
+ */
+ @SuppressWarnings("unused")
+ public Integer getIntValue() {
+ return INT_VALUE;
+ }
+
+ /**
+ * Used to verify that void functions are not invoked.
+ */
+ @SuppressWarnings("unused")
+ public void getVoidValue() {
+ voidInvoked = true;
+ }
+ }
+
+ /**
+ * Used to verify multi-component retrieval.
+ */
+ private static class Container {
+ private Simple simpleValue = new Simple();
+
+ @SuppressWarnings("unused")
+ public Simple getData() {
+ return simpleValue;
+ }
+ }
+
+ /**
+ * Used to verify extraction when the property refers to an interface.
+ */
+ private static interface WithString {
+
+ public String getStrValue();
+ }
+
+ /**
+ * Used to verify retrieval within a superclass.
+ */
+ private static class Super implements WithString {
+
+ @SuppressWarnings("unused")
+ private int intValue = INT_VALUE;
+
+ @Override
+ public String getStrValue() {
+ return VALUE;
+ }
+ }
+
+ /**
+ * Used to verify retrieval within a subclass.
+ */
+ private static class Sub extends Super {
+
+ @SuppressWarnings("unused")
+ private Simple simple = new Simple();
+
+ /**
+ * Used to verify multi-component retrieval.
+ */
+ private Container cont = new Container();
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport.java
new file mode 100644
index 00000000..be8d6c26
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport.java
@@ -0,0 +1,40 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+/**
+ * Used to test extractors.
+ */
+public class ClassExtractorsTestSupport {
+
+ private ClassExtractorsTestSupport2 nested = new ClassExtractorsTestSupport2();
+
+ /**
+ *
+ */
+ public ClassExtractorsTestSupport() {
+ super();
+ }
+
+ protected ClassExtractorsTestSupport2 getNested() {
+ return nested;
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport2.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport2.java
new file mode 100644
index 00000000..6941d033
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport2.java
@@ -0,0 +1,32 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+/**
+ * Used to test extractors.
+ */
+public class ClassExtractorsTestSupport2 {
+
+ public static final int NESTED_VALUE = 30;
+
+ @SuppressWarnings("unused")
+ private int theValue = NESTED_VALUE;
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ExtractorExceptionTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ExtractorExceptionTest.java
new file mode 100644
index 00000000..d1458de7
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ExtractorExceptionTest.java
@@ -0,0 +1,34 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+import org.onap.policy.common.utils.test.ExceptionsTester;
+
+public class ExtractorExceptionTest extends ExceptionsTester {
+
+ @Test
+ public void test() {
+ assertEquals(5, test(ExtractorException.class));
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/FieldExtractorTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/FieldExtractorTest.java
new file mode 100644
index 00000000..6fc2e20e
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/FieldExtractorTest.java
@@ -0,0 +1,77 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import java.lang.reflect.Field;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FieldExtractorTest {
+
+ private static final String VALUE = "the value";
+ private static final Integer INT_VALUE = 10;
+
+ private Field field;
+ private FieldExtractor ext;
+
+ @Before
+ public void setUp() throws Exception {
+ field = MyClass.class.getDeclaredField("value");
+ ext = new FieldExtractor(field);
+ }
+
+ @Test
+ public void testExtract() throws Exception {
+ assertEquals(VALUE, ext.extract(new MyClass()));
+
+ // repeat
+ assertEquals(VALUE, ext.extract(new MyClass()));
+
+ // null value
+ MyClass obj = new MyClass();
+ obj.value = null;
+ assertEquals(null, ext.extract(obj));
+
+ obj.value = VALUE + "X";
+ assertEquals(VALUE + "X", ext.extract(obj));
+
+ // different value type
+ field = MyClass.class.getDeclaredField("value2");
+ ext = new FieldExtractor(field);
+ assertEquals(INT_VALUE, ext.extract(new MyClass()));
+ }
+
+ @Test
+ public void testExtract_ArgEx() {
+ // pass it the wrong class type
+ assertNull(ext.extract(this));
+ }
+
+ private static class MyClass {
+ @SuppressWarnings("unused")
+ private String value = VALUE;
+
+ @SuppressWarnings("unused")
+ private int value2 = INT_VALUE;
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MapExtractorTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MapExtractorTest.java
new file mode 100644
index 00000000..48985bf3
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MapExtractorTest.java
@@ -0,0 +1,72 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+import static org.junit.Assert.*;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MapExtractorTest {
+ private static final String KEY = "a.key";
+ private static final String VALUE = "a.value";
+
+ private MapExtractor ext;
+
+ @Before
+ public void setUp() {
+ ext = new MapExtractor(KEY);
+ }
+
+ @Test
+ public void testExtract_NotAMap() {
+
+ // object is not a map (i.e., it's a String)
+ assertNull(ext.extract(KEY));
+ }
+
+ @Test
+ public void testExtract_MissingValue() {
+
+ Map<String,Object> map = new HashMap<>();
+ map.put(KEY+"x", VALUE+"x");
+
+ // object is a map, but doesn't have the key
+ assertNull(ext.extract(map));
+ }
+
+ @Test
+ public void testExtract() {
+
+ Map<String,Object> map = new HashMap<>();
+ map.put(KEY+"x", VALUE+"x");
+ map.put(KEY, VALUE);
+
+ // object is a map and contains the key
+ assertEquals(VALUE, ext.extract(map));
+
+ // change to value to a different type
+ map.put(KEY, 20);
+ assertEquals(20, ext.extract(map));
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MethodExtractorTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MethodExtractorTest.java
new file mode 100644
index 00000000..ae5858e7
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MethodExtractorTest.java
@@ -0,0 +1,99 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.extractor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import java.lang.reflect.Method;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MethodExtractorTest {
+
+ private static final String VALUE = "the value";
+ private static final Integer INT_VALUE = 10;
+
+ private Method meth;
+ private MethodExtractor ext;
+
+ @Before
+ public void setUp() throws Exception {
+ meth = MyClass.class.getMethod("getValue");
+ ext = new MethodExtractor(meth);
+ }
+
+ @Test
+ public void testExtract() throws Exception {
+ assertEquals(VALUE, ext.extract(new MyClass()));
+
+ // repeat
+ assertEquals(VALUE, ext.extract(new MyClass()));
+
+ // null value
+ MyClass obj = new MyClass();
+ meth = MyClass.class.getMethod("getNullValue");
+ ext = new MethodExtractor(meth);
+ assertEquals(null, ext.extract(obj));
+
+ // different value type
+ meth = MyClass.class.getMethod("getIntValue");
+ ext = new MethodExtractor(meth);
+ assertEquals(INT_VALUE, ext.extract(new MyClass()));
+ }
+
+ @Test
+ public void testExtract_ArgEx() {
+ // pass it the wrong class type
+ assertNull(ext.extract(this));
+ }
+
+ @Test
+ public void testExtract_InvokeEx() throws Exception {
+ // invoke method that throws an exception
+ meth = MyClass.class.getMethod("throwException");
+ ext = new MethodExtractor(meth);
+ assertEquals(null, ext.extract(new MyClass()));
+ }
+
+ private static class MyClass {
+
+ @SuppressWarnings("unused")
+ public String getValue() {
+ return VALUE;
+ }
+
+ @SuppressWarnings("unused")
+ public int getIntValue() {
+ return INT_VALUE;
+ }
+
+ @SuppressWarnings("unused")
+ public String getNullValue() {
+ return null;
+ }
+
+ @SuppressWarnings("unused")
+ public String throwException() {
+ throw new IllegalStateException("expected");
+ }
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/feature-pooling-dmaap.properties b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/feature-pooling-dmaap.properties
new file mode 100644
index 00000000..a4b5bc76
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/feature-pooling-dmaap.properties
@@ -0,0 +1,33 @@
+
+pooling.controllerA.topic = topic.A
+pooling.controllerA.enabled = true
+pooling.controllerA.offline.queue.limit = 5
+pooling.controllerA.offline.queue.age.milliseconds = 100
+pooling.controllerA.start.heartbeat.milliseconds = 10
+pooling.controllerA.reactivate.milliseconds = 20
+pooling.controllerA.identification.milliseconds = 30
+pooling.controllerA.active.heartbeat.milliseconds = 40
+pooling.controllerA.inter.heartbeat.milliseconds = 50
+
+pooling.controllerB.topic = topic.B
+pooling.controllerB.enabled = true
+pooling.controllerB.offline.queue.limit = 6
+pooling.controllerB.offline.queue.age.milliseconds = 101
+pooling.controllerB.start.heartbeat.milliseconds = 11
+pooling.controllerB.reactivate.milliseconds = 21
+pooling.controllerB.identification.milliseconds = 31
+pooling.controllerB.active.heartbeat.milliseconds = 41
+pooling.controllerB.inter.heartbeat.milliseconds = 51
+
+pooling.controllerDisabled.enabled = false
+
+# this has an invalid property
+pooling.controllerException.topic = topic.B
+pooling.controllerException.enabled = true
+pooling.controllerException.offline.queue.limit = INVALID NUMBER
+pooling.controllerException.offline.queue.age.milliseconds = 101
+pooling.controllerException.start.heartbeat.milliseconds = 11
+pooling.controllerException.reactivate.milliseconds = 21
+pooling.controllerException.identification.milliseconds = 31
+pooling.controllerException.active.heartbeat.milliseconds = 41
+pooling.controllerException.inter.heartbeat.milliseconds = 51
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BasicMessageTester.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BasicMessageTester.java
new file mode 100644
index 00000000..69d7e67c
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BasicMessageTester.java
@@ -0,0 +1,245 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.PoolingFeatureException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Superclass used to test subclasses of {@link Message}.
+ *
+ * @param <T> type of {@link Message} subclass that this tests
+ */
+public abstract class BasicMessageTester<T extends Message> {
+ // values set by makeValidMessage()
+ public static final String VALID_HOST_PREDECESSOR = "hostA";
+ public static final String VALID_HOST = "hostB";
+ public static final String VALID_CHANNEL = "channelC";
+
+ /**
+ * Used to perform JSON serialization and de-serialization.
+ */
+ public final ObjectMapper mapper = new ObjectMapper();
+
+ /**
+ * The subclass of the type of Message being tested.
+ */
+ private final Class<T> subclazz;
+
+ /**
+ *
+ * @param subclazz subclass of {@link Message} being tested
+ */
+ public BasicMessageTester(Class<T> subclazz) {
+ this.subclazz = subclazz;
+ }
+
+ /**
+ * Creates a default Message and verifies that the source and channel are
+ * {@code null}.
+ *
+ * @return the default Message
+ */
+ @Test
+ public final void testDefaultConstructor() {
+ testDefaultConstructorFields(makeDefaultMessage());
+ }
+
+ /**
+ * Tests that the Message has the correct source, and that the channel is
+ * {@code null}.
+ *
+ * @param msg message to be checked
+ * @param expectedSource what the source is expected to be
+ */
+ @Test
+ public final void testConstructorWithArgs() {
+ testValidFields(makeValidMessage());
+ }
+
+ /**
+ * Makes a valid message and then verifies that it can be serialized and
+ * de-serialized. Verifies that the de-serialized message is of the same
+ * type, and has the same content, as the original.
+ *
+ * @throws Exception if an error occurs
+ */
+ @Test
+ public final void testJsonEncodeDecode() throws Exception {
+ T originalMsg = makeValidMessage();
+
+ Message msg = mapper.readValue(mapper.writeValueAsString(originalMsg), Message.class);
+ assertEquals(subclazz, msg.getClass());
+
+ msg.checkValidity();
+
+ testValidFields(subclazz.cast(msg));
+ }
+
+ /**
+ * Creates a valid Message and verifies that checkValidity() passes.
+ *
+ * @throws PoolingFeatureException if an error occurs
+ */
+ @Test
+ public final void testCheckValidity_Ok() throws PoolingFeatureException {
+ T msg = makeValidMessage();
+ msg.checkValidity();
+
+ testValidFields(subclazz.cast(msg));
+ }
+
+ /**
+ * Creates a default Message and verifies that checkValidity() fails. Does
+ * not throw an exception.
+ */
+ @Test
+ public final void testCheckValidity_DefaultConstructor() {
+ try {
+ makeDefaultMessage().checkValidity();
+ fail("missing exception");
+
+ } catch (PoolingFeatureException expected) {
+ // success
+ }
+ }
+
+ /**
+ * Creates a message via {@link #makeValidMessage()}, updates it via the
+ * given function, and then invokes the checkValidity() method on it. It is
+ * expected that the checkValidity() will throw an exception.
+ *
+ * @param func function to update the message prior to invoking
+ * checkValidity()
+ */
+ public void expectCheckValidityFailure(MessageUpdateFunction<T> func) {
+ try {
+ T msg = makeValidMessage();
+ func.update(msg);
+
+ msg.checkValidity();
+
+ fail("missing exception");
+
+ } catch (PoolingFeatureException expected) {
+ // success
+ }
+ }
+
+ /**
+ * Creates a message via {@link #makeValidMessage()}, updates one of its
+ * fields via the given function, and then invokes the checkValidity()
+ * method on it. It is expected that the checkValidity() will throw an
+ * exception. It checks both the case when the message's field is set to
+ * {@code null}, and when it is set to empty (i.e., "").
+ *
+ * @param func function to update the message's field prior to invoking
+ * checkValidity()
+ */
+ public void expectCheckValidityFailure_NullOrEmpty(MessageFieldUpdateFunction<T> func) {
+ expectCheckValidityFailure(msg -> func.update(msg, null));
+ expectCheckValidityFailure(msg -> func.update(msg, ""));
+ }
+
+ /**
+ * Makes a message using the default constructor.
+ *
+ * @return a new Message
+ */
+ public final T makeDefaultMessage() {
+ try {
+ return subclazz.getConstructor().newInstance();
+
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ }
+
+
+ // the remaining methods will typically be overridden
+
+ /**
+ * Makes a message that will pass the validity check. Note: this should use
+ * the non-default constructor, and the source and channel should be set to
+ * {@link VALID_HOST} and {@link VALID_CHANNEL}, respectively.
+ *
+ * @return a valid Message
+ */
+ public abstract T makeValidMessage();
+
+ /**
+ * Verifies that fields are set as expected by
+ * {@link #makeDefaultMessage()}.
+ *
+ * @param msg the default Message
+ */
+ public void testDefaultConstructorFields(T msg) {
+ assertNull(msg.getSource());
+ assertNull(msg.getChannel());
+ }
+
+ /**
+ * Verifies that fields are set as expected by {@link #makeValidMessage()}.
+ *
+ * @param msg message whose fields are to be validated
+ */
+ public void testValidFields(T msg) {
+ assertEquals(VALID_HOST, msg.getSource());
+ assertEquals(VALID_CHANNEL, msg.getChannel());
+ }
+
+ /**
+ * Function that updates a message.
+ *
+ * @param <T> type of Message the function updates
+ */
+ @FunctionalInterface
+ public static interface MessageUpdateFunction<T extends Message> {
+
+ /**
+ * Updates a message.
+ *
+ * @param msg message to be updated
+ */
+ public void update(T msg);
+ }
+
+ /**
+ * Function that updates a single field within a message.
+ *
+ * @param <T> type of Message the function updates
+ */
+ @FunctionalInterface
+ public static interface MessageFieldUpdateFunction<T extends Message> {
+
+ /**
+ * Updates a field within a message.
+ *
+ * @param msg message to be updated
+ * @param newValue new field value
+ */
+ public void update(T msg, String newValue);
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java
new file mode 100644
index 00000000..ef03d4d6
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java
@@ -0,0 +1,351 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.PoolingFeatureException;
+
+public class BucketAssignmentsTest {
+
+ @Test
+ public void testBucketAssignments() {
+ new BucketAssignments();
+ }
+
+ @Test
+ public void testBucketAssignmentsStringArray() {
+ String arr[] = {"abc", "def"};
+ BucketAssignments asgn = new BucketAssignments(arr);
+
+ assertNotNull(asgn.getHostArray());
+ assertEquals(arr.toString(), asgn.getHostArray().toString());
+ }
+
+ @Test
+ public void testGetHostArray_testSetHostArray() {
+
+ String arr[] = {"abc", "def"};
+ BucketAssignments asgn = new BucketAssignments(arr);
+
+ assertNotNull(asgn.getHostArray());
+ assertEquals(arr.toString(), asgn.getHostArray().toString());
+
+ String arr2[] = {"xyz"};
+ asgn.setHostArray(arr2);
+
+ assertNotNull(asgn.getHostArray());
+ assertEquals(arr2.toString(), asgn.getHostArray().toString());
+ }
+
+ @Test
+ public void testGetLeader() {
+ // host array is null
+ BucketAssignments asgn = new BucketAssignments();
+ assertNull(asgn.getLeader());
+
+ // array is non-null, but empty
+ asgn.setHostArray(new String[0]);
+ assertNull(asgn.getLeader());
+
+ // all entries are null
+ asgn.setHostArray(new String[5]);
+ assertNull(asgn.getLeader());
+
+ // some entries are null
+ asgn.setHostArray(new String[] {null, "abc", null});
+ assertEquals("abc", asgn.getLeader());
+
+ // only one entry
+ asgn.setHostArray(new String[] {"abc"});
+ assertEquals("abc", asgn.getLeader());
+
+ // first is least
+ asgn.setHostArray(new String[] {"Ahost", "Bhost", "Chost"});
+ assertEquals("Ahost", asgn.getLeader());
+
+ // middle is least
+ asgn.setHostArray(new String[] {"Xhost", "Bhost", "Chost"});
+ assertEquals("Bhost", asgn.getLeader());
+
+ // last is least
+ asgn.setHostArray(new String[] {"Xhost", "Yhost", "Chost"});
+ assertEquals("Chost", asgn.getLeader());
+
+ // multiple entries
+ asgn.setHostArray(new String[] {"Xhost", "Bhost", "Chost", "Bhost", "Xhost", "Chost"});
+ assertEquals("Bhost", asgn.getLeader());
+ }
+
+ @Test
+ public void testHasAssignment() {
+ // host array is null
+ BucketAssignments asgn = new BucketAssignments();
+ assertFalse(asgn.hasAssignment("abc"));
+
+ // array is non-null, but empty
+ asgn.setHostArray(new String[0]);
+ assertFalse(asgn.hasAssignment("abc"));
+
+ // all entries are null
+ asgn.setHostArray(new String[5]);
+ assertFalse(asgn.hasAssignment("abc"));
+
+ // some entries are null
+ asgn.setHostArray(new String[] {null, "abc", null});
+ assertTrue(asgn.hasAssignment("abc"));
+
+ // only one entry
+ asgn.setHostArray(new String[] {"abc"});
+ assertTrue(asgn.hasAssignment("abc"));
+
+ // appears as first entry
+ asgn.setHostArray(new String[] {"abc", "Bhost", "Chost"});
+ assertTrue(asgn.hasAssignment("abc"));
+
+ // appears in middle
+ asgn.setHostArray(new String[] {"Xhost", "abc", "Chost"});
+ assertTrue(asgn.hasAssignment("abc"));
+
+ // appears last
+ asgn.setHostArray(new String[] {"Xhost", "Yhost", "abc"});
+ assertTrue(asgn.hasAssignment("abc"));
+
+ // appears repeatedly
+ asgn.setHostArray(new String[] {"Xhost", "Bhost", "Chost", "Bhost", "Xhost", "Chost"});
+ assertTrue(asgn.hasAssignment("Bhost"));
+ }
+
+ @Test
+ public void testGetAllHosts() {
+ // host array is null
+ BucketAssignments asgn = new BucketAssignments();
+ assertEquals("[]", getSortedHosts(asgn).toString());
+
+ // array is non-null, but empty
+ asgn.setHostArray(new String[0]);
+ assertEquals("[]", getSortedHosts(asgn).toString());
+
+ // all entries are null
+ asgn.setHostArray(new String[5]);
+ assertEquals("[]", getSortedHosts(asgn).toString());
+
+ // some entries are null
+ asgn.setHostArray(new String[] {null, "abc", null});
+ assertEquals("[abc]", getSortedHosts(asgn).toString());
+
+ // only one entry
+ asgn.setHostArray(new String[] {"abc"});
+ assertEquals("[abc]", getSortedHosts(asgn).toString());
+
+ // multiple, repeated entries
+ asgn.setHostArray(new String[] {"def", "abc", "def", "ghi", "def", "def", "xyz"});
+ assertEquals("[abc, def, ghi, xyz]", getSortedHosts(asgn).toString());
+ }
+
+ /**
+ * Gets the hosts, sorted, so that the order is predictable.
+ *
+ * @param asgn assignment whose hosts are to be retrieved
+ * @return a new, sorted set of hosts
+ */
+ private SortedSet<String> getSortedHosts(BucketAssignments asgn) {
+ return new TreeSet<>(asgn.getAllHosts());
+ }
+
+ @Test
+ public void testGetAssignedHost() {
+ // host array is null
+ BucketAssignments asgn = new BucketAssignments();
+ assertNull(asgn.getAssignedHost(3));
+
+ // array is non-null, but empty
+ asgn.setHostArray(new String[0]);
+ assertNull(asgn.getAssignedHost(3));
+
+ // all entries are null
+ asgn.setHostArray(new String[5]);
+ assertNull(asgn.getAssignedHost(3));
+
+ // multiple, repeated entries
+ String[] arr = {"def", "abc", "def", "ghi", "def", "def", "xyz"};
+ asgn.setHostArray(arr);
+
+ for (int x = 0; x < arr.length; ++x) {
+ assertEquals("x=" + x, arr[x], asgn.getAssignedHost(x));
+ }
+
+ // negative
+ assertNull(asgn.getAssignedHost(-1));
+
+ // beyond end
+ assertNull(asgn.getAssignedHost(arr.length));
+ assertNull(asgn.getAssignedHost(arr.length + 1));
+ }
+
+ @Test
+ public void testSize() {
+ // host array is null
+ BucketAssignments asgn = new BucketAssignments();
+ assertEquals(0, asgn.size());
+
+ // array is non-null, but empty
+ asgn.setHostArray(new String[0]);
+ assertEquals(0, asgn.size());
+
+ // all entries are null
+ asgn.setHostArray(new String[5]);
+ assertEquals(5, asgn.size());
+
+ // multiple, repeated entries
+ String[] arr = {"def", "abc", "def", "ghi", "def", "def", "xyz"};
+ asgn.setHostArray(arr);
+ assertEquals(arr.length, asgn.size());
+ }
+
+ @Test
+ public void testCheckValidity() throws Exception {
+ // host array is null
+ BucketAssignments asgn = new BucketAssignments();
+ expectException(asgn);
+
+ // array is non-null, but empty
+ asgn.setHostArray(new String[0]);
+ expectException(asgn);
+
+ // array is too big
+ asgn.setHostArray(new String[BucketAssignments.MAX_BUCKETS + 1]);
+ expectException(asgn);
+
+ // all entries are null
+ asgn.setHostArray(new String[5]);
+ expectException(asgn);
+
+ // null at the beginning
+ asgn.setHostArray(new String[] {null, "Bhost", "Chost"});
+ expectException(asgn);
+
+ // null in the middle
+ asgn.setHostArray(new String[] {"Ahost", null, "Chost"});
+ expectException(asgn);
+
+ // null at the end
+ asgn.setHostArray(new String[] {"Ahost", "Bhost", null});
+ expectException(asgn);
+
+ // only one entry
+ asgn.setHostArray(new String[] {"abc"});
+ asgn.checkValidity();
+
+ // multiple entries
+ asgn.setHostArray(new String[] {"Ahost", "Bhost", "Chost"});
+ asgn.checkValidity();
+ }
+
+ @Test
+ public void testHashCode() {
+ // with null assignments
+ BucketAssignments asgn = new BucketAssignments();
+ asgn.hashCode();
+
+ // with empty array
+ asgn = new BucketAssignments(new String[0]);
+ asgn.hashCode();
+
+ // with null items
+ asgn = new BucketAssignments(new String[] {"abc", null, "def"});
+ asgn.hashCode();
+
+ // same assignments
+ asgn = new BucketAssignments(new String[] {"abc", null, "def"});
+ int code = asgn.hashCode();
+
+ asgn = new BucketAssignments(new String[] {"abc", null, "def"});
+ assertEquals(code, asgn.hashCode());
+
+ // slightly different values (i.e., changed "def" to "eef")
+ asgn = new BucketAssignments(new String[] {"abc", null, "eef"});
+ assertTrue(code != asgn.hashCode());
+ }
+
+ @Test
+ public void testEquals() {
+ // null object
+ BucketAssignments asgn = new BucketAssignments();
+ assertFalse(asgn.equals(null));
+
+ // same object
+ asgn = new BucketAssignments();
+ assertTrue(asgn.equals(asgn));
+
+ // different class of object
+ asgn = new BucketAssignments();
+ assertFalse(asgn.equals("not an assignment object"));
+
+ // with null assignments
+ asgn = new BucketAssignments();
+ assertTrue(asgn.equals(new BucketAssignments()));
+
+ assertFalse(asgn.equals(new BucketAssignments(new String[] {"abc"})));
+
+ // with empty array
+ asgn = new BucketAssignments(new String[0]);
+ assertTrue(asgn.equals(asgn));
+
+ assertFalse(asgn.equals(new BucketAssignments()));
+ assertFalse(asgn.equals(new BucketAssignments(new String[] {"abc"})));
+
+ // with null items
+ String[] arr = {"abc", null, "def"};
+ asgn = new BucketAssignments(arr);
+ assertTrue(asgn.equals(asgn));
+ assertTrue(asgn.equals(new BucketAssignments(arr)));
+ assertTrue(asgn.equals(new BucketAssignments(new String[] {"abc", null, "def"})));
+
+ assertFalse(asgn.equals(new BucketAssignments()));
+ assertFalse(asgn.equals(new BucketAssignments(new String[] {"abc", null, "XYZ"})));
+
+ assertFalse(asgn.equals(new BucketAssignments()));
+ }
+
+ /**
+ * Expects an exception when checkValidity() is called.
+ *
+ * @param asgn assignments to be checked
+ */
+ private void expectException(BucketAssignments asgn) {
+ try {
+ asgn.checkValidity();
+ fail("missing exception");
+
+ } catch (PoolingFeatureException expected) {
+ // success
+ }
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/ForwardTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/ForwardTest.java
new file mode 100644
index 00000000..c56caca8
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/ForwardTest.java
@@ -0,0 +1,217 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import org.junit.Test;
+import org.onap.policy.drools.event.comm.Topic.CommInfrastructure;
+
+public class ForwardTest extends BasicMessageTester<Forward> {
+ // values set by makeValidMessage()
+ public static final CommInfrastructure VALID_PROTOCOL = CommInfrastructure.UEB;
+ public static final int VALID_HOPS = 0;
+ public static final String VALID_TOPIC = "topicA";
+ public static final String VALID_PAYLOAD = "payloadA";
+ public static final String VALID_REQUEST_ID = "requestIdA";
+
+ /**
+ * Time, in milliseconds, after which the most recent message was created.
+ */
+ private static long tcreateMs;
+
+ public ForwardTest() {
+ super(Forward.class);
+ }
+
+ @Test
+ public void testBumpNumHops() {
+ Forward msg = makeValidMessage();
+
+ for (int x = 0; x < 3; ++x) {
+ assertEquals("x=" + x, x, msg.getNumHops());
+ msg.bumpNumHops();
+ }
+ }
+
+ @Test
+ public void testGetNumHops_testSetNumHops() {
+ Forward msg = makeValidMessage();
+
+ // from constructor
+ assertEquals(VALID_HOPS, msg.getNumHops());
+
+ msg.setNumHops(5);
+ assertEquals(5, msg.getNumHops());
+
+ msg.setNumHops(7);
+ assertEquals(7, msg.getNumHops());
+ }
+
+ @Test
+ public void testGetCreateTimeMs_testSetCreateTimeMs() {
+ Forward msg = makeValidMessage();
+
+ // from constructor
+ assertTrue(msg.getCreateTimeMs() >= tcreateMs);
+
+ msg.setCreateTimeMs(1000L);
+ assertEquals(1000L, msg.getCreateTimeMs());
+
+ msg.setCreateTimeMs(2000L);
+ assertEquals(2000L, msg.getCreateTimeMs());
+ }
+
+ @Test
+ public void testGetProtocol_testSetProtocol() {
+ Forward msg = makeValidMessage();
+
+ // from constructor
+ assertEquals(CommInfrastructure.UEB, msg.getProtocol());
+
+ msg.setProtocol(CommInfrastructure.DMAAP);
+ assertEquals(CommInfrastructure.DMAAP, msg.getProtocol());
+
+ msg.setProtocol(CommInfrastructure.UEB);
+ assertEquals(CommInfrastructure.UEB, msg.getProtocol());
+ }
+
+ @Test
+ public void testGetTopic_testSetTopic() {
+ Forward msg = makeValidMessage();
+
+ // from constructor
+ assertEquals(VALID_TOPIC, msg.getTopic());
+
+ msg.setTopic("topicX");
+ assertEquals("topicX", msg.getTopic());
+
+ msg.setTopic("topicY");
+ assertEquals("topicY", msg.getTopic());
+ }
+
+ @Test
+ public void testGetPayload_testSetPayload() {
+ Forward msg = makeValidMessage();
+
+ // from constructor
+ assertEquals(VALID_PAYLOAD, msg.getPayload());
+
+ msg.setPayload("payloadX");
+ assertEquals("payloadX", msg.getPayload());
+
+ msg.setPayload("payloadY");
+ assertEquals("payloadY", msg.getPayload());
+ }
+
+ @Test
+ public void testGetRequestId_testSetRequestId() {
+ Forward msg = makeValidMessage();
+
+ // from constructor
+ assertEquals(VALID_REQUEST_ID, msg.getRequestId());
+
+ msg.setRequestId("reqX");
+ assertEquals("reqX", msg.getRequestId());
+
+ msg.setRequestId("reqY");
+ assertEquals("reqY", msg.getRequestId());
+ }
+
+ @Test
+ public void testIsExpired() {
+ Forward msg = makeValidMessage();
+
+ long tcreate = msg.getCreateTimeMs();
+ assertTrue(msg.isExpired(tcreate + 1));
+ assertTrue(msg.isExpired(tcreate + 10));
+
+ assertFalse(msg.isExpired(tcreate));
+ assertFalse(msg.isExpired(tcreate - 1));
+ assertFalse(msg.isExpired(tcreate - 10));
+ }
+
+ @Test
+ public void testCheckValidity_InvalidFields() throws Exception {
+ // null source (i.e., superclass field)
+ expectCheckValidityFailure(msg -> msg.setSource(null));
+
+ // null protocol
+ expectCheckValidityFailure(msg -> msg.setProtocol(null));
+
+ // null or empty topic
+ expectCheckValidityFailure_NullOrEmpty((msg, value) -> msg.setTopic(value));
+
+ // null payload
+ expectCheckValidityFailure(msg -> msg.setPayload(null));
+
+ // empty payload should NOT throw an exception
+ Forward forward = makeValidMessage();
+ forward.setPayload("");
+ forward.checkValidity();
+
+ // null or empty requestId
+ expectCheckValidityFailure_NullOrEmpty((msg, value) -> msg.setRequestId(value));
+
+ // invalid hop count
+ expectCheckValidityFailure(msg -> msg.setNumHops(-1));
+ }
+
+ /**
+ * Makes a message that will pass the validity check.
+ *
+ * @return a valid Message
+ */
+ public Forward makeValidMessage() {
+ tcreateMs = System.currentTimeMillis();
+
+ Forward msg = new Forward(VALID_HOST, VALID_PROTOCOL, VALID_TOPIC, VALID_PAYLOAD, VALID_REQUEST_ID);
+ msg.setChannel(VALID_CHANNEL);
+
+ return msg;
+ }
+
+ @Override
+ public void testDefaultConstructorFields(Forward msg) {
+ super.testDefaultConstructorFields(msg);
+
+ assertEquals(VALID_HOPS, msg.getNumHops());
+ assertEquals(0, msg.getCreateTimeMs());
+ assertNull(msg.getPayload());
+ assertNull(msg.getProtocol());
+ assertNull(msg.getRequestId());
+ assertNull(msg.getTopic());
+ }
+
+ @Override
+ public void testValidFields(Forward msg) {
+ super.testValidFields(msg);
+
+ assertEquals(VALID_HOPS, msg.getNumHops());
+ assertTrue(msg.getCreateTimeMs() >= tcreateMs);
+ assertEquals(VALID_PAYLOAD, msg.getPayload());
+ assertEquals(VALID_PROTOCOL, msg.getProtocol());
+ assertEquals(VALID_REQUEST_ID, msg.getRequestId());
+ assertEquals(VALID_TOPIC, msg.getTopic());
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java
new file mode 100644
index 00000000..da78dbe3
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java
@@ -0,0 +1,62 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import static org.junit.Assert.assertEquals;
+
+public class HeartbeatTest extends BasicMessageTester<Heartbeat> {
+
+ /**
+ * Sequence number to validate time stamps within the heart beat.
+ */
+ private long sequence = 0;
+
+ public HeartbeatTest() {
+ super(Heartbeat.class);
+ }
+
+ /**
+ * Makes a message that will pass the validity check.
+ *
+ * @return a valid Message
+ */
+ public Heartbeat makeValidMessage() {
+ Heartbeat msg = new Heartbeat(VALID_HOST, ++sequence);
+ msg.setChannel(VALID_CHANNEL);
+
+ return msg;
+ }
+
+ @Override
+ public void testDefaultConstructorFields(Heartbeat msg) {
+ super.testDefaultConstructorFields(msg);
+
+ assertEquals(sequence, msg.getTimestampMs());
+ }
+
+ @Override
+ public void testValidFields(Heartbeat msg) {
+ super.testValidFields(msg);
+
+ assertEquals(sequence, msg.getTimestampMs());
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java
new file mode 100644
index 00000000..8255034f
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java
@@ -0,0 +1,77 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class IdentificationTest extends MessageWithAssignmentsTester<Identification> {
+
+ public IdentificationTest() {
+ super(Identification.class);
+ }
+
+ @Before
+ public void setUp() {
+ setNullAssignments(false);
+ }
+
+ /**
+ * The superclass will already invoke testJsonEncodeDecode() to verify that
+ * things work with a fully populated message. This verifies that it also
+ * works if the assignments are null.
+ *
+ * @throws Exception if an error occurs
+ */
+ @Test
+ public final void testJsonEncodeDecode_WithNullAssignments() throws Exception {
+ setNullAssignments(true);
+ testJsonEncodeDecode();
+ }
+
+ /**
+ * The superclass will already invoke testCheckValidity() to
+ * verify that things work with a fully populated message. This verifies
+ * that it also works if the assignments are null.
+ *
+ * @throws Exception if an error occurs
+ */
+ @Test
+ public void testCheckValidity_NullAssignments() throws Exception {
+ // null assignments are OK
+ Identification msg = makeValidMessage();
+ msg.setAssignments(null);
+ msg.checkValidity();
+ }
+
+ /**
+ * Makes a message that will pass the validity check.
+ *
+ * @return a valid Message
+ */
+ public Identification makeValidMessage() {
+ Identification msg = new Identification(VALID_HOST, (isNullAssignments() ? null : VALID_ASGN));
+ msg.setChannel(VALID_CHANNEL);
+
+ return msg;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java
new file mode 100644
index 00000000..0f58e224
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java
@@ -0,0 +1,77 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class LeaderTest extends MessageWithAssignmentsTester<Leader> {
+
+ public LeaderTest() {
+ super(Leader.class);
+ }
+
+ @Before
+ public void setUp() {
+ setNullAssignments(false);
+ }
+
+ /**
+ * The superclass will already invoke testCheckValidity_InvalidFields() to
+ * verify that things work with a fully populated message. This verifies
+ * that it also works if the assignments are null.
+ *
+ * @throws Exception if an error occurs
+ */
+ @Test
+ public void testCheckValidity_InvalidFields_NullAssignments() throws Exception {
+ // null assignments are invalid
+ expectCheckValidityFailure(msg -> msg.setAssignments(null));
+ }
+
+ @Test
+ public void testCheckValidity_SourceIsNotLeader() throws Exception {
+ Leader ldr = makeValidMessage();
+
+ ldr.setSource("xyz");
+
+ // the source does not have an assignment
+ BucketAssignments asgnUnassigned = new BucketAssignments(new String[] {"abc", "def"});
+ expectCheckValidityFailure(msg -> msg.setAssignments(asgnUnassigned));
+
+ // the source is not the smallest UUID in this assignment
+ BucketAssignments asgnNotSmallest = new BucketAssignments(new String[] {VALID_HOST_PREDECESSOR, VALID_HOST});
+ expectCheckValidityFailure(msg -> msg.setAssignments(asgnNotSmallest));
+ }
+
+ /**
+ * Makes a message that will pass the validity check.
+ *
+ * @return a valid Message
+ */
+ public Leader makeValidMessage() {
+ Leader msg = new Leader(VALID_HOST, (isNullAssignments() ? null : VALID_ASGN));
+ msg.setChannel(VALID_CHANNEL);
+
+ return msg;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java
new file mode 100644
index 00000000..432dcc3c
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java
@@ -0,0 +1,80 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import org.junit.Test;
+
+public class MessageTest extends BasicMessageTester<Message> {
+
+ public MessageTest() {
+ super(Message.class);
+ }
+
+ @Test
+ public void testGetSource_testSetSource() {
+ Message msg = new Message();
+
+ msg.setSource("hello");
+ assertEquals("hello", msg.getSource());
+ assertNull(msg.getChannel());
+
+ msg.setSource("world");
+ assertEquals("world", msg.getSource());
+ assertNull(msg.getChannel());
+ }
+
+ @Test
+ public void testGetChannel_testSetChannel() {
+ Message msg = new Message();
+
+ msg.setChannel("hello");
+ assertEquals("hello", msg.getChannel());
+ assertNull(msg.getSource());
+
+ msg.setChannel("world");
+ assertEquals("world", msg.getChannel());
+ assertNull(msg.getSource());
+ }
+
+ @Test
+ public void testCheckValidity_InvalidFields() {
+ // null or empty source
+ expectCheckValidityFailure_NullOrEmpty((msg, value) -> msg.setSource(value));
+
+ // null or empty channel
+ expectCheckValidityFailure_NullOrEmpty((msg, value) -> msg.setChannel(value));
+ }
+
+ /**
+ * Makes a message that will pass the validity check.
+ *
+ * @return a valid Message
+ */
+ public Message makeValidMessage() {
+ Message msg = new Message(VALID_HOST);
+ msg.setChannel(VALID_CHANNEL);
+
+ return msg;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageWithAssignmentsTester.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageWithAssignmentsTester.java
new file mode 100644
index 00000000..2b670dcc
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageWithAssignmentsTester.java
@@ -0,0 +1,110 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import org.junit.Test;
+
+/**
+ * Superclass used to test subclasses of {@link MessageWithAssignments}.
+ *
+ * @param <T> type of {@link MessageWithAssignments} subclass that this tests
+ */
+public abstract class MessageWithAssignmentsTester<T extends MessageWithAssignments> extends BasicMessageTester<T> {
+ // values set by makeValidMessage()
+ public static final String[] VALID_ARRAY = {VALID_HOST, VALID_HOST+"_xxx"};
+ public static final BucketAssignments VALID_ASGN = new BucketAssignments(VALID_ARRAY);
+
+ /**
+ * {@code True} if {@code null} assignments are allowed, {@code false}
+ * otherwise.
+ */
+ private boolean nullAssignments;
+
+ /**
+ *
+ * @param subclazz subclass of {@link MessageWithAssignments} being tested
+ */
+ public MessageWithAssignmentsTester(Class<T> subclazz) {
+ super(subclazz);
+ }
+
+ /**
+ * Indicates whether or not {@code null} assignments should be used for the
+ * remaining tests.
+ *
+ * @param nullAssignments {@code true} to use {@code null} assignments,
+ * {@code false} otherwise
+ */
+ public void setNullAssignments(boolean nullAssignments) {
+ this.nullAssignments = nullAssignments;
+ }
+
+ public boolean isNullAssignments() {
+ return nullAssignments;
+ }
+
+ @Test
+ public void testCheckValidity_InvalidFields() throws Exception {
+ // null source (i.e., superclass field)
+ expectCheckValidityFailure(msg -> msg.setSource(null));
+
+ // empty assignments
+ expectCheckValidityFailure(msg -> msg.setAssignments(new BucketAssignments(new String[0])));
+
+ // invalid assignment
+ String[] invalidAssignment = {"abc", null};
+ expectCheckValidityFailure(msg -> msg.setAssignments(new BucketAssignments(invalidAssignment)));
+ }
+
+ @Test
+ public void testGetAssignments_testSetAssignments() {
+ MessageWithAssignments msg = makeValidMessage();
+
+ // from constructor
+ assertEquals(VALID_ASGN, msg.getAssignments());
+
+ BucketAssignments asgn = new BucketAssignments();
+ msg.setAssignments(asgn);
+ assertEquals(asgn, msg.getAssignments());
+ }
+
+ @Override
+ public void testDefaultConstructorFields(T msg) {
+ super.testDefaultConstructorFields(msg);
+
+ assertNull(msg.getAssignments());
+ }
+
+ @Override
+ public void testValidFields(T msg) {
+ super.testValidFields(msg);
+
+ if (nullAssignments) {
+ assertNull(msg.getAssignments());
+
+ } else {
+ assertEquals(VALID_ASGN, msg.getAssignments());
+ }
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java
new file mode 100644
index 00000000..8d0f4a6f
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java
@@ -0,0 +1,41 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+public class OfflineTest extends BasicMessageTester<Offline> {
+
+ public OfflineTest() {
+ super(Offline.class);
+ }
+
+ /**
+ * Makes a message that will pass the validity check.
+ *
+ * @return a valid Message
+ */
+ public Offline makeValidMessage() {
+ Offline msg = new Offline(VALID_HOST);
+ msg.setChannel(VALID_CHANNEL);
+
+ return msg;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java
new file mode 100644
index 00000000..0b2a986d
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java
@@ -0,0 +1,41 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+public class QueryTest extends BasicMessageTester<Query> {
+
+ public QueryTest() {
+ super(Query.class);
+ }
+
+ /**
+ * Makes a message that will pass the validity check.
+ *
+ * @return a valid Message
+ */
+ public Query makeValidMessage() {
+ Query msg = new Query(VALID_HOST);
+ msg.setChannel(VALID_CHANNEL);
+
+ return msg;
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/Trial.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/Trial.java
new file mode 100644
index 00000000..428b5853
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/Trial.java
@@ -0,0 +1,41 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ================================================================================
+ * 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.drools.pooling.message;
+
+import org.junit.Test;
+import org.onap.policy.drools.event.comm.Topic.CommInfrastructure;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class Trial {
+
+ @Test
+ public void test() throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+
+ Message msg = new Forward("me", CommInfrastructure.DMAAP, "my topic", "a message", "my req");
+
+ String enc = mapper.writeValueAsString(msg);
+ System.out.println("enc=" + enc);
+
+ Message msg2 = mapper.readValue(enc, Message.class);
+ System.out.println("class=" + msg2.getClass());
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java
new file mode 100644
index 00000000..7997a4ee
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java
@@ -0,0 +1,441 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.atLeast;
+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.util.Arrays;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Heartbeat;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.message.Query;
+
+public class ActiveStateTest extends BasicStateTester {
+
+ private ActiveState state;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ state = new ActiveState(mgr);
+ }
+
+ @Test
+ public void testStart() {
+ state.start();
+
+ // ensure the timers were created
+ verify(mgr, atLeast(1)).scheduleWithFixedDelay(anyLong(), anyLong(), any(StateTimerTask.class));
+
+ // ensure a heart beat was generated
+ Pair<String, Heartbeat> msg = capturePublishedMessage(Heartbeat.class);
+ assertEquals(MY_HOST, msg.second.getSource());
+ }
+
+ @Test
+ public void testGetFilter() {
+ Map<String, Object> filter = state.getFilter();
+
+ FilterUtilsTest utils = new FilterUtilsTest();
+
+ utils.checkArray(FilterUtils.CLASS_OR, 2, filter);
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0));
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1));
+ }
+
+ @Test
+ public void testProcessHeartbeat_NullHost() {
+ assertNull(state.process(new Heartbeat()));
+
+ assertFalse(state.isMyHeartbeatSeen());
+ assertFalse(state.isPredHeartbeatSeen());
+
+ verify(mgr, never()).goInactive();
+ verify(mgr, never()).goQuery();
+ }
+
+ @Test
+ public void testProcessHeartbeat_MyHost() {
+ assertNull(state.process(new Heartbeat(MY_HOST, 0L)));
+
+ assertTrue(state.isMyHeartbeatSeen());
+ assertFalse(state.isPredHeartbeatSeen());
+
+ verify(mgr, never()).goInactive();
+ verify(mgr, never()).goQuery();
+ }
+
+ @Test
+ public void testProcessHeartbeat_Predecessor() {
+ assertNull(state.process(new Heartbeat(HOST2, 0L)));
+
+ assertFalse(state.isMyHeartbeatSeen());
+ assertTrue(state.isPredHeartbeatSeen());
+
+ verify(mgr, never()).goInactive();
+ verify(mgr, never()).goQuery();
+ }
+
+ @Test
+ public void testProcessHeartbeat_OtherHost() {
+ assertNull(state.process(new Heartbeat(HOST3, 0L)));
+
+ assertFalse(state.isMyHeartbeatSeen());
+ assertFalse(state.isPredHeartbeatSeen());
+
+ verify(mgr, never()).goInactive();
+ verify(mgr, never()).goQuery();
+ }
+
+ @Test
+ public void testProcessOffline_NullHost() {
+ // should be ignored
+ assertNull(state.process(new Offline()));
+ }
+
+ @Test
+ public void testProcessOffline_UnassignedHost() {
+ // HOST4 is not in the assignment list - should be ignored
+ assertNull(state.process(new Offline(HOST4)));
+ }
+
+ @Test
+ public void testProcessOffline_IAmLeader() {
+ // configure the next state
+ State next = mock(State.class);
+ when(mgr.goActive()).thenReturn(next);
+
+ // one of the assigned hosts went offline
+ assertEquals(next, state.process(new Offline(HOST1)));
+
+ // should have sent a new Leader message
+ Leader msg = captureAdminMessage(Leader.class);
+
+ assertEquals(MY_HOST, msg.getSource());
+
+ // check new bucket assignments
+ assertEquals(Arrays.asList(MY_HOST, MY_HOST, HOST2), Arrays.asList(msg.getAssignments().getHostArray()));
+ }
+
+ @Test
+ public void testProcessOffline_PredecessorIsLeaderNowOffline() {
+ // configure the next state
+ State next = mock(State.class);
+ when(mgr.goActive()).thenReturn(next);
+
+ // I am not the leader, but my predecessor was
+ mgr.startDistributing(new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST1}));
+ state = new ActiveState(mgr);
+
+ // my predecessor went offline
+ assertEquals(next, state.process(new Offline(PREV_HOST)));
+
+ // should have sent a new Leader message
+ Leader msg = captureAdminMessage(Leader.class);
+
+ assertEquals(MY_HOST, msg.getSource());
+
+ // check new bucket assignments
+ assertEquals(Arrays.asList(MY_HOST, MY_HOST, HOST1), Arrays.asList(msg.getAssignments().getHostArray()));
+ }
+
+ @Test
+ public void testProcessOffline__PredecessorIsNotLeaderNowOffline() {
+ // I am not the leader, and neither is my predecessor
+ mgr.startDistributing(new BucketAssignments(new String[] {PREV_HOST, MY_HOST, PREV_HOST2}));
+ state = new ActiveState(mgr);
+
+ /*
+ *
+ * PREV_HOST2 has buckets and is my predecessor, but it isn't the leader
+ * thus should be ignored.
+ */
+ assertNull(state.process(new Offline(PREV_HOST2)));
+ }
+
+ @Test
+ public void testProcessOffline_OtherAssignedHostOffline() {
+ // I am not the leader
+ mgr.startDistributing(new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST1}));
+ state = new ActiveState(mgr);
+
+ /*
+ * HOST1 has buckets, but it isn't the leader and it isn't my
+ * predecessor, thus should be ignored.
+ */
+ assertNull(state.process(new Offline(HOST1)));
+ }
+
+ @Test
+ public void testProcessQuery() {
+ State next = mock(State.class);
+ when(mgr.goQuery()).thenReturn(next);
+
+ assertEquals(next, state.process(new Query()));
+
+ Identification ident = captureAdminMessage(Identification.class);
+ assertEquals(MY_HOST, ident.getSource());
+ assertEquals(ASGN3, ident.getAssignments());
+ }
+
+ @Test
+ public void testActiveState() {
+ assertEquals(MY_HOST, state.getLeader());
+ assertEquals(ASGN3, state.getAssignments());
+
+ // verify that it determined its neighbors
+ assertEquals(HOST1, state.getSuccHost());
+ assertEquals(HOST2, state.getPredHost());
+ }
+
+ @Test
+ public void testDetmNeighbors() {
+ // if only one host (i.e., itself)
+ mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST, MY_HOST}));
+ state = new ActiveState(mgr);
+ assertEquals(null, state.getSuccHost());
+ assertEquals("", state.getPredHost());
+
+ // two hosts
+ mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST, HOST2}));
+ state = new ActiveState(mgr);
+ assertEquals(HOST2, state.getSuccHost());
+ assertEquals(HOST2, state.getPredHost());
+
+ // three hosts
+ mgr.startDistributing(new BucketAssignments(new String[] {HOST3, MY_HOST, HOST2}));
+ state = new ActiveState(mgr);
+ assertEquals(HOST2, state.getSuccHost());
+ assertEquals(HOST3, state.getPredHost());
+
+ // more hosts
+ mgr.startDistributing(new BucketAssignments(new String[] {HOST3, MY_HOST, HOST2, HOST4}));
+ state = new ActiveState(mgr);
+ assertEquals(HOST2, state.getSuccHost());
+ assertEquals(HOST4, state.getPredHost());
+ }
+
+ @Test
+ public void testAddTimers_WithPredecessor() {
+ // invoke start() to add the timers
+ state.start();
+
+ assertEquals(3, repeatedFutures.size());
+
+ Triple<Long, Long, StateTimerTask> timer;
+
+ // heart beat generator
+ timer = repeatedTasks.remove();
+ assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.first.longValue());
+ assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.second.longValue());
+
+ // my heart beat checker
+ timer = repeatedTasks.remove();
+ assertEquals(STD_INTER_HEARTBEAT_MS, timer.first.longValue());
+ assertEquals(STD_INTER_HEARTBEAT_MS, timer.second.longValue());
+
+ // predecessor's heart beat checker
+ timer = repeatedTasks.remove();
+ assertEquals(STD_INTER_HEARTBEAT_MS, timer.first.longValue());
+ assertEquals(STD_INTER_HEARTBEAT_MS, timer.second.longValue());
+ }
+
+ @Test
+ public void testAddTimers_SansPredecessor() {
+ // only one host, thus no predecessor
+ mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST, MY_HOST}));
+ state = new ActiveState(mgr);
+
+ // invoke start() to add the timers
+ state.start();
+
+ assertEquals(2, repeatedFutures.size());
+
+ Triple<Long, Long, StateTimerTask> timer;
+
+ // heart beat generator
+ timer = repeatedTasks.remove();
+ assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.first.longValue());
+ assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.second.longValue());
+
+ // my heart beat checker
+ timer = repeatedTasks.remove();
+ assertEquals(STD_INTER_HEARTBEAT_MS, timer.first.longValue());
+ assertEquals(STD_INTER_HEARTBEAT_MS, timer.second.longValue());
+ }
+
+ @Test
+ public void testAddTimers_HeartbeatGenerator() {
+ // only one host so we only have to look at one heart beat at a time
+ mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST}));
+ state = new ActiveState(mgr);
+
+ // invoke start() to add the timers
+ state.start();
+
+ Triple<Long, Long, StateTimerTask> task = repeatedTasks.remove();
+
+ verify(mgr).publish(anyString(), any(Heartbeat.class));
+
+ // fire the task
+ assertNull(task.third.fire(null));
+
+ // should have generated a second pair of heart beats
+ verify(mgr, times(2)).publish(anyString(), any(Heartbeat.class));
+
+ Pair<String, Heartbeat> msg = capturePublishedMessage(Heartbeat.class);
+ assertEquals(MY_HOST, msg.first);
+ assertEquals(MY_HOST, msg.second.getSource());
+ }
+
+ @Test
+ public void testAddTimers_MyHeartbeatSeen() {
+ // invoke start() to add the timers
+ state.start();
+
+ Triple<Long, Long, StateTimerTask> task = repeatedTasks.get(1);
+
+ // indicate that this host is still alive
+ state.process(new Heartbeat(MY_HOST, 0L));
+
+ // set up next state
+ State next = mock(State.class);
+ when(mgr.goInactive()).thenReturn(next);
+
+ // fire the task - should not transition
+ assertNull(task.third.fire(null));
+
+ verify(mgr, never()).publishAdmin(any(Query.class));
+ }
+
+ @Test
+ public void testAddTimers_MyHeartbeatMissed() {
+ // invoke start() to add the timers
+ state.start();
+
+ Triple<Long, Long, StateTimerTask> task = repeatedTasks.get(1);
+
+ // set up next state
+ State next = mock(State.class);
+ when(mgr.goInactive()).thenReturn(next);
+
+ // fire the task - should transition
+ assertEquals(next, task.third.fire(null));
+
+ // should indicate failure
+ verify(mgr).internalTopicFailed();
+
+ // should publish an offline message
+ Offline msg = captureAdminMessage(Offline.class);
+ assertEquals(MY_HOST, msg.getSource());
+ }
+
+ @Test
+ public void testAddTimers_PredecessorHeartbeatSeen() {
+ // invoke start() to add the timers
+ state.start();
+
+ Triple<Long, Long, StateTimerTask> task = repeatedTasks.get(2);
+
+ // indicate that the predecessor is still alive
+ state.process(new Heartbeat(HOST2, 0L));
+
+ // set up next state, just in case
+ State next = mock(State.class);
+ when(mgr.goQuery()).thenReturn(next);
+
+ // fire the task - should NOT transition
+ assertNull(task.third.fire(null));
+
+ verify(mgr, never()).publishAdmin(any(Query.class));
+ }
+
+ @Test
+ public void testAddTimers_PredecessorHeartbeatMissed() {
+ // invoke start() to add the timers
+ state.start();
+
+ Triple<Long, Long, StateTimerTask> task = repeatedTasks.get(2);
+
+ // set up next state
+ State next = mock(State.class);
+ when(mgr.goQuery()).thenReturn(next);
+
+ // fire the task - should transition
+ assertEquals(next, task.third.fire(null));
+
+ verify(mgr).publishAdmin(any(Query.class));
+ }
+
+ @Test
+ public void testGenHeartbeat_OneHost() {
+ // only one host (i.e., itself)
+ mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST}));
+ state = new ActiveState(mgr);
+
+ state.start();
+
+ verify(mgr, times(1)).publish(any(), any());
+
+ Pair<String, Heartbeat> msg = capturePublishedMessage(Heartbeat.class);
+ assertEquals(MY_HOST, msg.first);
+ assertEquals(MY_HOST, msg.second.getSource());
+ }
+
+ @Test
+ public void testGenHeartbeat_MultipleHosts() {
+ state.start();
+
+ verify(mgr, times(2)).publish(any(), any());
+
+ Pair<String, Heartbeat> msg;
+ int index = 0;
+
+ // this message should go to itself
+ msg = capturePublishedMessage(Heartbeat.class, index++);
+ assertEquals(MY_HOST, msg.first);
+ assertEquals(MY_HOST, msg.second.getSource());
+
+ // this message should go to its successor
+ msg = capturePublishedMessage(Heartbeat.class, index++);
+ assertEquals(HOST1, msg.first);
+ assertEquals(MY_HOST, msg.second.getSource());
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/BasicStateTester.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/BasicStateTester.java
new file mode 100644
index 00000000..e48742f7
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/BasicStateTester.java
@@ -0,0 +1,318 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import org.onap.policy.drools.pooling.PoolingManager;
+import org.onap.policy.drools.pooling.PoolingProperties;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+
+/**
+ * Superclass used to test subclasses of {@link Message}.
+ */
+public class BasicStateTester {
+
+ protected static final long STD_HEARTBEAT_WAIT_MS = 10;
+ protected static final long STD_REACTIVATE_WAIT_MS = STD_HEARTBEAT_WAIT_MS + 1;
+ protected static final long STD_IDENTIFICATION_MS = STD_REACTIVATE_WAIT_MS + 1;
+ protected static final long STD_ACTIVE_HEARTBEAT_MS = STD_IDENTIFICATION_MS + 1;
+ protected static final long STD_INTER_HEARTBEAT_MS = STD_ACTIVE_HEARTBEAT_MS + 1;
+
+ protected static final String MY_TOPIC = "myTopic";
+
+ protected static final String PREV_HOST = "prevHost";
+ protected static final String PREV_HOST2 = PREV_HOST + "A";
+
+ // this follows PREV_HOST, alphabetically
+ protected static final String MY_HOST = PREV_HOST + "X";
+
+ // these follow MY_HOST, alphabetically
+ protected static final String HOST1 = MY_HOST + "1";
+ protected static final String HOST2 = MY_HOST + "2";
+ protected static final String HOST3 = MY_HOST + "3";
+ protected static final String HOST4 = MY_HOST + "4";
+
+ protected static final String LEADER = HOST1;
+
+ protected static final String[] HOST_ARR3 = {HOST1, MY_HOST, HOST2};
+
+ protected static final BucketAssignments EMPTY_ASGN = new BucketAssignments();
+ protected static final BucketAssignments ASGN3 = new BucketAssignments(HOST_ARR3);
+
+ /**
+ * Futures returned by schedule().
+ */
+ protected LinkedList<ScheduledFuture<?>> onceFutures;
+
+ /**
+ * Tasks captured via schedule().
+ */
+ protected LinkedList<Pair<Long, StateTimerTask>> onceTasks;
+
+ /**
+ * Futures returned by scheduleWithFixedDelay().
+ */
+ protected LinkedList<ScheduledFuture<?>> repeatedFutures;
+
+ /**
+ * Tasks captured via scheduleWithFixedDelay().
+ */
+ protected LinkedList<Triple<Long, Long, StateTimerTask>> repeatedTasks;
+
+ /**
+ * Messages captured via publish().
+ */
+ protected LinkedList<Pair<String, Message>> published;
+
+ /**
+ * Messages captured via publishAdmin().
+ */
+ protected LinkedList<Message> admin;
+
+ protected PoolingManager mgr;
+ protected PoolingProperties props;
+ protected State prevState;
+
+ public BasicStateTester() {
+ super();
+ }
+
+ public void setUp() throws Exception {
+ onceFutures = new LinkedList<>();
+ onceTasks = new LinkedList<>();
+
+ repeatedFutures = new LinkedList<>();
+ repeatedTasks = new LinkedList<>();
+
+ published = new LinkedList<>();
+ admin = new LinkedList<>();
+
+ mgr = mock(PoolingManager.class);
+ props = mock(PoolingProperties.class);
+
+ when(mgr.getHost()).thenReturn(MY_HOST);
+ when(mgr.getTopic()).thenReturn(MY_TOPIC);
+ when(mgr.getProperties()).thenReturn(props);
+
+ when(props.getStartHeartbeatMs()).thenReturn(STD_HEARTBEAT_WAIT_MS);
+ when(props.getReactivateMs()).thenReturn(STD_REACTIVATE_WAIT_MS);
+ when(props.getIdentificationMs()).thenReturn(STD_IDENTIFICATION_MS);
+ when(props.getActiveHeartbeatMs()).thenReturn(STD_ACTIVE_HEARTBEAT_MS);
+ when(props.getInterHeartbeatMs()).thenReturn(STD_INTER_HEARTBEAT_MS);
+
+ prevState = new State(mgr) {
+ @Override
+ public Map<String, Object> getFilter() {
+ throw new UnsupportedOperationException("cannot filter");
+ }
+ };
+
+ // capture publish() arguments
+ doAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ published.add(new Pair<>((String) args[0], (Message) args[1]));
+
+ return null;
+ }).when(mgr).publish(anyString(), any(Message.class));
+
+ // capture publishAdmin() arguments
+ doAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ admin.add((Message) args[0]);
+
+ return null;
+ }).when(mgr).publishAdmin(any(Message.class));
+
+ // capture schedule() arguments, and return a new future
+ when(mgr.schedule(anyLong(), any(StateTimerTask.class))).thenAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ onceTasks.add(new Pair<>((Long) args[0], (StateTimerTask) args[1]));
+
+ ScheduledFuture<?> fut = mock(ScheduledFuture.class);
+ onceFutures.add(fut);
+ return fut;
+ });
+
+ // capture scheduleWithFixedDelay() arguments, and return a new future
+ when(mgr.scheduleWithFixedDelay(anyLong(), anyLong(), any(StateTimerTask.class))).thenAnswer(invocation -> {
+ Object[] args = invocation.getArguments();
+ repeatedTasks.add(new Triple<>((Long) args[0], (Long) args[1], (StateTimerTask) args[2]));
+
+ ScheduledFuture<?> fut = mock(ScheduledFuture.class);
+ repeatedFutures.add(fut);
+ return fut;
+ });
+
+ // get/set assignments in the manager
+ AtomicReference<BucketAssignments> asgn = new AtomicReference<>(ASGN3);
+
+ when(mgr.getAssignments()).thenAnswer(args -> asgn.get());
+
+ doAnswer(args -> {
+ asgn.set(args.getArgumentAt(0, BucketAssignments.class));
+ return null;
+ }).when(mgr).startDistributing(any());
+ }
+
+ /**
+ * Makes a sorted set of hosts.
+ *
+ * @param hosts the hosts to be sorted
+ * @return the set of hosts, sorted
+ */
+ protected SortedSet<String> sortHosts(String... hosts) {
+ return new TreeSet<>(Arrays.asList(hosts));
+ }
+
+ /**
+ * Captures the host array from the Leader message published to the admin
+ * channel.
+ *
+ * @return the host array, as a list
+ */
+ protected List<String> captureHostList() {
+ return Arrays.asList(captureHostArray());
+ }
+
+ /**
+ * Captures the host array from the Leader message published to the admin
+ * channel.
+ *
+ * @return the host array
+ */
+ protected String[] captureHostArray() {
+ BucketAssignments asgn = captureAssignments();
+
+ String[] arr = asgn.getHostArray();
+ assertNotNull(arr);
+
+ return arr;
+ }
+
+ /**
+ * Captures the assignments from the Leader message published to the admin
+ * channel.
+ *
+ * @return the bucket assignments
+ */
+ protected BucketAssignments captureAssignments() {
+ Leader msg = captureAdminMessage(Leader.class);
+
+ BucketAssignments asgn = msg.getAssignments();
+ assertNotNull(asgn);
+ return asgn;
+ }
+
+ /**
+ * Captures the message published to the admin channel.
+ *
+ * @param clazz type of {@link Message} to capture
+ * @return the message that was published
+ */
+ protected <T extends Message> T captureAdminMessage(Class<T> clazz) {
+ return captureAdminMessage(clazz, 0);
+ }
+
+ /**
+ * Captures the message published to the admin channel.
+ *
+ * @param clazz type of {@link Message} to capture
+ * @param index index of the item to be captured
+ * @return the message that was published
+ */
+ protected <T extends Message> T captureAdminMessage(Class<T> clazz, int index) {
+ return clazz.cast(admin.get(index));
+ }
+
+ /**
+ * Captures the message published to the non-admin channels.
+ *
+ * @param clazz type of {@link Message} to capture
+ * @return the (channel,message) pair that was published
+ */
+ protected <T extends Message> Pair<String, T> capturePublishedMessage(Class<T> clazz) {
+ return capturePublishedMessage(clazz, 0);
+ }
+
+ /**
+ * Captures the message published to the non-admin channels.
+ *
+ * @param clazz type of {@link Message} to capture
+ * @param index index of the item to be captured
+ * @return the (channel,message) pair that was published
+ */
+ protected <T extends Message> Pair<String, T> capturePublishedMessage(Class<T> clazz, int index) {
+ Pair<String, Message> msg = published.get(index);
+ return new Pair<>(msg.first, clazz.cast(msg.second));
+ }
+
+ /**
+ * Pair of values.
+ *
+ * @param <F> first value's type
+ * @param <S> second value's type
+ */
+ public static class Pair<F, S> {
+ public final F first;
+ public final S second;
+
+ public Pair(F first, S second) {
+ this.first = first;
+ this.second = second;
+ }
+ }
+
+ /**
+ * Pair of values.
+ *
+ * @param <F> first value's type
+ * @param <S> second value's type
+ * @param <T> third value's type
+ */
+ public static class Triple<F, S, T> {
+ public final F first;
+ public final S second;
+ public final T third;
+
+ public Triple(F first, S second, T third) {
+ this.first = first;
+ this.second = second;
+ this.third = third;
+ }
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/FilterUtilsTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/FilterUtilsTest.java
new file mode 100644
index 00000000..ba517194
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/FilterUtilsTest.java
@@ -0,0 +1,109 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.junit.Assert.assertEquals;
+import static org.onap.policy.drools.pooling.state.FilterUtils.CLASS_AND;
+import static org.onap.policy.drools.pooling.state.FilterUtils.CLASS_EQUALS;
+import static org.onap.policy.drools.pooling.state.FilterUtils.CLASS_OR;
+import static org.onap.policy.drools.pooling.state.FilterUtils.JSON_CLASS;
+import static org.onap.policy.drools.pooling.state.FilterUtils.JSON_FIELD;
+import static org.onap.policy.drools.pooling.state.FilterUtils.JSON_FILTERS;
+import static org.onap.policy.drools.pooling.state.FilterUtils.JSON_VALUE;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeAnd;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeEquals;
+import static org.onap.policy.drools.pooling.state.FilterUtils.makeOr;
+import java.util.Map;
+import org.junit.Test;
+
+public class FilterUtilsTest {
+
+ @Test
+ public void testMakeEquals() {
+ checkEquals("abc", "def", makeEquals("abc", "def"));
+ }
+
+ @Test
+ public void testMakeAnd() {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> filter =
+ makeAnd(makeEquals("an1", "av1"), makeEquals("an2", "av2"), makeEquals("an3", "av3"));
+
+ checkArray(CLASS_AND, 3, filter);
+ checkEquals("an1", "av1", getItem(filter, 0));
+ checkEquals("an2", "av2", getItem(filter, 1));
+ checkEquals("an3", "av3", getItem(filter, 2));
+ }
+
+ @Test
+ public void testMakeOr() {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> filter =
+ makeOr(makeEquals("on1", "ov1"), makeEquals("on2", "ov2"), makeEquals("on3", "ov3"));
+
+ checkArray(CLASS_OR, 3, filter);
+ checkEquals("on1", "ov1", getItem(filter, 0));
+ checkEquals("on2", "ov2", getItem(filter, 1));
+ checkEquals("on3", "ov3", getItem(filter, 2));
+ }
+
+ /**
+ * Checks that the filter contains an array.
+ *
+ * @param expectedClassName type of filter this should represent
+ * @param expectedCount number of items expected in the array
+ * @param filter filter to be examined
+ */
+ protected void checkArray(String expectedClassName, int expectedCount, Map<String, Object> filter) {
+ assertEquals(expectedClassName, filter.get(JSON_CLASS));
+
+ Object[] val = (Object[]) filter.get(JSON_FILTERS);
+ assertEquals(expectedCount, val.length);
+ }
+
+ /**
+ * Checks that a map represents an "equals".
+ *
+ * @param name name of the field on the left side of the equals
+ * @param value value on the right side of the equals
+ * @param map map whose content is to be examined
+ */
+ protected void checkEquals(String name, String value, Map<String, Object> map) {
+ assertEquals(CLASS_EQUALS, map.get(JSON_CLASS));
+ assertEquals(name, map.get(JSON_FIELD));
+ assertEquals(value, map.get(JSON_VALUE));
+ }
+
+ /**
+ * Gets a particular sub-filter from the array contained within a filter.
+ *
+ * @param filter containing filter
+ * @param index index of the sub-filter of interest
+ * @return the sub-filter with the given index
+ */
+ @SuppressWarnings("unchecked")
+ protected Map<String, Object> getItem(Map<String, Object> filter, int index) {
+ Object[] val = (Object[]) filter.get(JSON_FILTERS);
+
+ return (Map<String, Object>) val[index];
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java
new file mode 100644
index 00000000..96c59719
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java
@@ -0,0 +1,121 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+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.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Forward;
+import org.onap.policy.drools.pooling.message.Heartbeat;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.message.Query;
+
+public class IdleStateTest extends BasicStateTester {
+
+ private IdleState state;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ state = new IdleState(mgr);
+ }
+
+ @Test
+ public void testGetFilter() {
+ Map<String, Object> filter = state.getFilter();
+
+ FilterUtilsTest utils = new FilterUtilsTest();
+
+ utils.checkArray(FilterUtils.CLASS_OR, 2, filter);
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0));
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1));
+ }
+
+ @Test
+ public void testStop() {
+ state.stop();
+ verifyNothingPublished();
+ }
+
+ @Test
+ public void testProcessForward() {
+ Forward msg = new Forward();
+ assertNull(state.process(msg));
+
+ verify(mgr).handle(msg);
+ }
+
+ @Test
+ public void testProcessHeartbeat() {
+ assertNull(state.process(new Heartbeat(PREV_HOST, 0L)));
+ verifyNothingPublished();
+ }
+
+ @Test
+ public void testProcessIdentification() {
+ assertNull(state.process(new Identification(PREV_HOST, null)));
+ verifyNothingPublished();
+ }
+
+ @Test
+ public void testProcessLeader() {
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST2, PREV_HOST, MY_HOST});
+ Leader msg = new Leader(PREV_HOST, asgn);
+
+ State next = mock(State.class);
+ when(mgr.goActive()).thenReturn(next);
+
+ // should stay in current state, but start distributing
+ assertNull(state.process(msg));
+ verify(mgr).startDistributing(asgn);
+ }
+
+ @Test
+ public void testProcessOffline() {
+ assertNull(state.process(new Offline(PREV_HOST)));
+ verifyNothingPublished();
+ }
+
+ @Test
+ public void testProcessQuery() {
+ assertNull(state.process(new Query()));
+ verifyNothingPublished();
+ }
+
+ /**
+ * Verifies that nothing was published on either channel.
+ */
+ private void verifyNothingPublished() {
+ verify(mgr, never()).publish(any(), any());
+ verify(mgr, never()).publishAdmin(any());
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java
new file mode 100644
index 00000000..48d5b1ed
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java
@@ -0,0 +1,83 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.message.Message;
+
+public class InactiveStateTest extends BasicStateTester {
+
+ private InactiveState state;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ state = new InactiveState(mgr);
+ }
+
+ @Test
+ public void testGetFilter() {
+ Map<String, Object> filter = state.getFilter();
+
+ FilterUtilsTest utils = new FilterUtilsTest();
+
+ utils.checkArray(FilterUtils.CLASS_OR, 2, filter);
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0));
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1));
+ }
+
+ @Test
+ public void testGoInatcive() {
+ assertNull(state.goInactive());
+ }
+
+ @Test
+ public void testStart() {
+ state.start();
+
+ Pair<Long, StateTimerTask> timer = onceTasks.remove();
+
+ assertEquals(STD_REACTIVATE_WAIT_MS, timer.first.longValue());
+
+ // invoke the task - it should go to the state returned by the mgr
+ State next = mock(State.class);
+ when(mgr.goStart()).thenReturn(next);
+
+ assertEquals(next, timer.second.fire(null));
+ }
+
+ @Test
+ public void testInactiveState() {
+ /*
+ * Prove the state is attached to the manager by invoking getHost(),
+ * which delegates to the manager.
+ */
+ assertEquals(MY_HOST, state.getHost());
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java
new file mode 100644
index 00000000..d60ad2ea
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java
@@ -0,0 +1,328 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+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.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Query;
+
+public class ProcessingStateTest extends BasicStateTester {
+
+ private ProcessingState state;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ state = new ProcessingState(mgr, MY_HOST);
+ }
+
+ @Test
+ public void testGetFilter() {
+ Map<String, Object> filter = state.getFilter();
+
+ FilterUtilsTest utils = new FilterUtilsTest();
+
+ utils.checkArray(FilterUtils.CLASS_OR, 2, filter);
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0));
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1));
+ }
+
+ @Test
+ public void testProcessQuery() {
+ State next = mock(State.class);
+ when(mgr.goQuery()).thenReturn(next);
+
+ assertEquals(next, state.process(new Query()));
+
+ Identification ident = captureAdminMessage(Identification.class);
+ assertEquals(MY_HOST, ident.getSource());
+ assertEquals(ASGN3, ident.getAssignments());
+ }
+
+ @Test
+ public void testProcessingState() {
+ /*
+ * Null assignments should be OK.
+ */
+ when(mgr.getAssignments()).thenReturn(null);
+ state = new ProcessingState(mgr, LEADER);
+
+ /*
+ * Empty assignments should be OK.
+ */
+ when(mgr.getAssignments()).thenReturn(EMPTY_ASGN);
+ state = new ProcessingState(mgr, LEADER);
+ assertEquals(MY_HOST, state.getHost());
+ assertEquals(LEADER, state.getLeader());
+ assertEquals(EMPTY_ASGN, state.getAssignments());
+
+ /*
+ * Now try something with assignments.
+ */
+ when(mgr.getAssignments()).thenReturn(ASGN3);
+ state = new ProcessingState(mgr, LEADER);
+
+ /*
+ * Prove the state is attached to the manager by invoking getHost(),
+ * which delegates to the manager.
+ */
+ assertEquals(MY_HOST, state.getHost());
+
+ assertEquals(LEADER, state.getLeader());
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testProcessingState_NullLeader() {
+ when(mgr.getAssignments()).thenReturn(EMPTY_ASGN);
+ state = new ProcessingState(mgr, null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testProcessingState_ZeroLengthHostArray() {
+ when(mgr.getAssignments()).thenReturn(new BucketAssignments(new String[] {}));
+ state = new ProcessingState(mgr, LEADER);
+ }
+
+ @Test
+ public void testMakeIdentification() {
+ Identification ident = state.makeIdentification();
+ assertEquals(MY_HOST, ident.getSource());
+ assertEquals(ASGN3, ident.getAssignments());
+ }
+
+ @Test
+ public void testGetAssignments() {
+ // assignments from constructor
+ assertEquals(ASGN3, state.getAssignments());
+
+ // null assignments - no effect
+ state.setAssignments(null);
+ assertEquals(ASGN3, state.getAssignments());
+
+ // empty assignments
+ state.setAssignments(EMPTY_ASGN);
+ assertEquals(EMPTY_ASGN, state.getAssignments());
+
+ // non-empty assignments
+ state.setAssignments(ASGN3);
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+ @Test
+ public void testSetAssignments() {
+ state.setAssignments(null);
+ verify(mgr, never()).startDistributing(any());
+
+ state.setAssignments(ASGN3);
+ verify(mgr).startDistributing(ASGN3);
+ }
+
+ @Test
+ public void testGetLeader() {
+ // check value from constructor
+ assertEquals(MY_HOST, state.getLeader());
+
+ state.setLeader(HOST2);
+ assertEquals(HOST2, state.getLeader());
+
+ state.setLeader(HOST3);
+ assertEquals(HOST3, state.getLeader());
+ }
+
+ @Test
+ public void testSetLeader() {
+ state.setLeader(MY_HOST);
+ assertEquals(MY_HOST, state.getLeader());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetLeader_Null() {
+ state.setLeader(null);
+ }
+
+ @Test
+ public void testIsLeader() {
+ state.setLeader(MY_HOST);
+ assertTrue(state.isLeader());
+
+ state.setLeader(HOST2);
+ assertFalse(state.isLeader());
+ }
+
+ @Test
+ public void testBecomeLeader() {
+ State next = mock(State.class);
+ when(mgr.goActive()).thenReturn(next);
+
+ assertEquals(next, state.becomeLeader(sortHosts(MY_HOST, HOST2)));
+
+ Leader msg = captureAdminMessage(Leader.class);
+
+ verify(mgr).startDistributing(msg.getAssignments());
+ verify(mgr).goActive();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBecomeLeader_NotFirstAlive() {
+ // alive list contains something before my host name
+ state.becomeLeader(sortHosts(PREV_HOST, MY_HOST));
+ }
+
+ @Test
+ public void testMakeLeader() throws Exception {
+ state.becomeLeader(sortHosts(MY_HOST, HOST2));
+
+ Leader msg = captureAdminMessage(Leader.class);
+
+ // need a channel before invoking checkValidity()
+ msg.setChannel(Message.ADMIN);
+
+ msg.checkValidity();
+
+ assertEquals(MY_HOST, msg.getSource());
+ assertNotNull(msg.getAssignments());
+ assertTrue(msg.getAssignments().hasAssignment(MY_HOST));
+ assertTrue(msg.getAssignments().hasAssignment(HOST2));
+
+ // this one wasn't in the list of hosts, so it should have been removed
+ assertFalse(msg.getAssignments().hasAssignment(HOST1));
+ }
+
+ @Test
+ public void testMakeAssignments() throws Exception {
+ state.becomeLeader(sortHosts(MY_HOST, HOST2));
+
+ captureAssignments().checkValidity();
+ }
+
+ @Test
+ public void testMakeBucketArray_NullAssignments() {
+ when(mgr.getAssignments()).thenReturn(null);
+ state = new ProcessingState(mgr, MY_HOST);
+ state.becomeLeader(sortHosts(MY_HOST));
+
+ String[] arr = captureHostArray();
+
+ assertEquals(BucketAssignments.MAX_BUCKETS, arr.length);
+
+ assertTrue(Arrays.asList(arr).stream().allMatch(host -> MY_HOST.equals(host)));
+ }
+
+ @Test
+ public void testMakeBucketArray_ZeroAssignments() {
+ // bucket assignment with a zero-length array
+ state.setAssignments(new BucketAssignments(new String[0]));
+
+ state.becomeLeader(sortHosts(MY_HOST));
+
+ String[] arr = captureHostArray();
+
+ assertEquals(BucketAssignments.MAX_BUCKETS, arr.length);
+
+ assertTrue(Arrays.asList(arr).stream().allMatch(host -> MY_HOST.equals(host)));
+ }
+
+ @Test
+ public void testMakeBucketArray() {
+ /*
+ * All hosts are still alive, so it should have the exact same
+ * assignments as it had to start.
+ */
+ state.setAssignments(ASGN3);
+ state.becomeLeader(sortHosts(HOST_ARR3));
+
+ String[] arr = captureHostArray();
+
+ assertTrue(arr != HOST_ARR3);
+ assertEquals(Arrays.asList(HOST_ARR3), Arrays.asList(arr));
+ }
+
+ @Test
+ public void testRemoveExcessHosts() {
+ /**
+ * All hosts are still alive, plus some others.
+ */
+ state.setAssignments(ASGN3);
+ state.becomeLeader(sortHosts(MY_HOST, HOST1, HOST2, HOST3, HOST4));
+
+ // assignments should be unchanged
+ assertEquals(Arrays.asList(HOST_ARR3), captureHostList());
+ }
+
+ @Test
+ public void testAddIndicesToHostBuckets() {
+ // some are null, some hosts are no longer alive
+ String[] asgn = {null, MY_HOST, HOST3, null, HOST4, HOST1, HOST2};
+
+ state.setAssignments(new BucketAssignments(asgn));
+ state.becomeLeader(sortHosts(MY_HOST, HOST1, HOST2));
+
+ // every bucket should be assigned to one of the three hosts
+ String[] expected = {MY_HOST, MY_HOST, HOST1, HOST2, MY_HOST, HOST1, HOST2};
+ assertEquals(Arrays.asList(expected), captureHostList());
+ }
+
+ @Test
+ public void testAssignNullBuckets() {
+ /*
+ * Ensure buckets are assigned to the host with the fewest buckets.
+ */
+ String[] asgn = {MY_HOST, HOST1, MY_HOST, null, null, null, null, null, MY_HOST};
+
+ state.setAssignments(new BucketAssignments(asgn));
+ state.becomeLeader(sortHosts(MY_HOST, HOST1, HOST2));
+
+ String[] expected = {MY_HOST, HOST1, MY_HOST, HOST2, HOST1, HOST2, HOST1, HOST2, MY_HOST};
+ assertEquals(Arrays.asList(expected), captureHostList());
+ }
+
+ @Test
+ public void testRebalanceBuckets() {
+ /**
+ * Some are very lopsided.
+ */
+ String[] asgn = {MY_HOST, HOST1, MY_HOST, MY_HOST, MY_HOST, MY_HOST, HOST1, HOST2, HOST1, HOST3};
+
+ state.setAssignments(new BucketAssignments(asgn));
+ state.becomeLeader(sortHosts(MY_HOST, HOST1, HOST2, HOST3));
+
+ String[] expected = {HOST2, HOST1, HOST3, MY_HOST, MY_HOST, MY_HOST, HOST1, HOST2, HOST1, HOST3};
+ assertEquals(Arrays.asList(expected), captureHostList());
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java
new file mode 100644
index 00000000..d714d5cc
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java
@@ -0,0 +1,462 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+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.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.message.Query;
+
+public class QueryStateTest extends BasicStateTester {
+
+ private QueryState state;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ state = new QueryState(mgr);
+ }
+
+ @Test
+ public void testGetFilter() {
+ Map<String, Object> filter = state.getFilter();
+
+ FilterUtilsTest utils = new FilterUtilsTest();
+
+ utils.checkArray(FilterUtils.CLASS_OR, 2, filter);
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0));
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1));
+ }
+
+ @Test
+ public void testStart() {
+ state.start();
+
+ Pair<Long, StateTimerTask> timer = onceTasks.remove();
+
+ assertEquals(STD_IDENTIFICATION_MS, timer.first.longValue());
+ assertNotNull(timer.second);
+ }
+
+ @Test
+ public void testGoQuery() {
+ assertNull(state.process(new Query()));
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+ @Test
+ public void testProcessIdentification_NullSource() {
+ assertNull(state.process(new Identification()));
+
+ assertEquals(MY_HOST, state.getLeader());
+ }
+
+ @Test
+ public void testProcessIdentification_NewLeader() {
+ assertNull(state.process(new Identification(PREV_HOST, null)));
+
+ assertEquals(PREV_HOST, state.getLeader());
+ }
+
+ @Test
+ public void testProcessIdentification_NotNewLeader() {
+ assertNull(state.process(new Identification(HOST2, null)));
+
+ assertEquals(MY_HOST, state.getLeader());
+ }
+
+ @Test
+ public void testProcessLeader_NullAssignment() {
+ Leader msg = new Leader(PREV_HOST, null);
+
+ // should stay in the same state, and not start distributing
+ assertNull(state.process(msg));
+ verify(mgr, never()).startDistributing(any());
+ verify(mgr, never()).goActive();
+ verify(mgr, never()).goInactive();
+
+ // info should be unchanged
+ assertEquals(MY_HOST, state.getLeader());
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+ @Test
+ public void testProcessLeader_NullSource() {
+ String[] arr = {HOST2, PREV_HOST, MY_HOST};
+ BucketAssignments asgn = new BucketAssignments(arr);
+ Leader msg = new Leader(null, asgn);
+
+ // should stay in the same state, and not start distributing
+ assertNull(state.process(msg));
+ verify(mgr, never()).startDistributing(any());
+ verify(mgr, never()).goActive();
+ verify(mgr, never()).goInactive();
+
+ // info should be unchanged
+ assertEquals(MY_HOST, state.getLeader());
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+ @Test
+ public void testProcessLeader_SourceIsNotAssignmentLeader() {
+ String[] arr = {HOST2, PREV_HOST, MY_HOST};
+ BucketAssignments asgn = new BucketAssignments(arr);
+ Leader msg = new Leader(HOST2, asgn);
+
+ // should stay in the same state, and not start distributing
+ assertNull(state.process(msg));
+ verify(mgr, never()).startDistributing(any());
+ verify(mgr, never()).goActive();
+ verify(mgr, never()).goInactive();
+
+ // info should be unchanged
+ assertEquals(MY_HOST, state.getLeader());
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+ @Test
+ public void testProcessLeader_EmptyAssignment() {
+ Leader msg = new Leader(PREV_HOST, new BucketAssignments());
+
+ // should stay in the same state, and not start distributing
+ assertNull(state.process(msg));
+ verify(mgr, never()).startDistributing(any());
+ verify(mgr, never()).goActive();
+ verify(mgr, never()).goInactive();
+
+ // info should be unchanged
+ assertEquals(MY_HOST, state.getLeader());
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+ @Test
+ public void testProcessLeader_BetterLeader() {
+ String[] arr = {HOST2, PREV_HOST, MY_HOST};
+ BucketAssignments asgn = new BucketAssignments(arr);
+ Leader msg = new Leader(PREV_HOST, asgn);
+
+ State next = mock(State.class);
+ when(mgr.goActive()).thenReturn(next);
+
+ // should go Active and start distributing
+ assertEquals(next, state.process(msg));
+ verify(mgr).startDistributing(asgn);
+ verify(mgr, never()).goInactive();
+ }
+
+ @Test
+ public void testProcessLeader_NotABetterLeader() {
+ // no assignments yet
+ mgr.startDistributing(null);
+ state = new QueryState(mgr);
+
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2});
+ Leader msg = new Leader(HOST1, asgn);
+
+ State next = mock(State.class);
+ when(mgr.goInactive()).thenReturn(next);
+
+ // should stay in the same state
+ assertNull(state.process(msg));
+ verify(mgr, never()).goActive();
+ verify(mgr, never()).goInactive();
+
+ // should have started distributing
+ verify(mgr).startDistributing(asgn);
+
+ // this host should still be the leader
+ assertEquals(MY_HOST, state.getLeader());
+
+ // new assignments
+ assertEquals(asgn, state.getAssignments());
+ }
+
+ @Test
+ public void testProcessOffline_NullHost() {
+ assertNull(state.process(new Offline()));
+ assertEquals(MY_HOST, state.getLeader());
+ }
+
+ @Test
+ public void testProcessOffline_SameHost() {
+ assertNull(state.process(new Offline(MY_HOST)));
+ assertEquals(MY_HOST, state.getLeader());
+ }
+
+ @Test
+ public void testProcessOffline_DiffHost() {
+ BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, HOST1});
+ mgr.startDistributing(asgn);
+ state = new QueryState(mgr);
+
+ // tell it that the hosts are alive
+ state.process(new Identification(PREV_HOST, asgn));
+ state.process(new Identification(HOST1, asgn));
+
+ // #2 goes offline
+ assertNull(state.process(new Offline(HOST1)));
+
+ // #1 should still be the leader
+ assertEquals(PREV_HOST, state.getLeader());
+
+ // #1 goes offline
+ assertNull(state.process(new Offline(PREV_HOST)));
+
+ // this should still be the leader now
+ assertEquals(MY_HOST, state.getLeader());
+ }
+
+ @Test
+ public void testProcessQuery() {
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2});
+ mgr.startDistributing(asgn);
+ state = new QueryState(mgr);
+
+ State next = mock(State.class);
+ when(mgr.goQuery()).thenReturn(next);
+
+ assertEquals(null, state.process(new Query()));
+
+ verify(mgr).publishAdmin(any(Identification.class));
+ }
+
+ @Test
+ public void testQueryState() {
+ /*
+ * Prove the state is attached to the manager by invoking getHost(),
+ * which delegates to the manager.
+ */
+ assertEquals(MY_HOST, state.getHost());
+ }
+
+ @Test
+ public void testAwaitIdentification_Leader() {
+ state.start();
+
+ Pair<Long, StateTimerTask> timer = onceTasks.remove();
+
+ assertEquals(STD_IDENTIFICATION_MS, timer.first.longValue());
+ assertNotNull(timer.second);
+
+ State next = mock(State.class);
+ when(mgr.goActive()).thenReturn(next);
+
+ assertEquals(next, timer.second.fire(null));
+
+ // should have published a Leader message
+ Leader msg = captureAdminMessage(Leader.class);
+ assertEquals(MY_HOST, msg.getSource());
+ assertTrue(msg.getAssignments().hasAssignment(MY_HOST));
+ }
+
+ @Test
+ public void testAwaitIdentification_HasAssignment() {
+ // not the leader, but has an assignment
+ BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST2});
+ mgr.startDistributing(asgn);
+ state = new QueryState(mgr);
+
+ state.start();
+
+ // tell it the leader is still active
+ state.process(new Identification(PREV_HOST, asgn));
+
+ Pair<Long, StateTimerTask> timer = onceTasks.remove();
+
+ assertEquals(STD_IDENTIFICATION_MS, timer.first.longValue());
+ assertNotNull(timer.second);
+
+ // set up active state, as that's what it should return
+ State next = mock(State.class);
+ when(mgr.goActive()).thenReturn(next);
+
+ assertEquals(next, timer.second.fire(null));
+
+ // should NOT have published a Leader message
+ assertTrue(admin.isEmpty());
+
+ // should have gone active with the current assignments
+ verify(mgr).goActive();
+ }
+
+ @Test
+ public void testAwaitIdentification_NoAssignment() {
+ // not the leader and no assignment
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2});
+ mgr.startDistributing(asgn);
+ state = new QueryState(mgr);
+
+ state.start();
+
+ // tell it the leader is still active
+ state.process(new Identification(PREV_HOST, asgn));
+
+ Pair<Long, StateTimerTask> timer = onceTasks.remove();
+
+ assertEquals(STD_IDENTIFICATION_MS, timer.first.longValue());
+ assertNotNull(timer.second);
+
+ // set up inactive state, as that's what it should return
+ State next = mock(State.class);
+ when(mgr.goInactive()).thenReturn(next);
+
+ assertEquals(next, timer.second.fire(null));
+
+ // should NOT have published a Leader message
+ assertTrue(admin.isEmpty());
+ }
+
+ @Test
+ public void testHasAssignment() {
+ // null assignment
+ mgr.startDistributing(null);
+ assertFalse(state.hasAssignment());
+
+ // not in assignments
+ state.setAssignments(new BucketAssignments(new String[] {HOST3}));
+ assertFalse(state.hasAssignment());
+
+ // it IS in the assignments
+ state.setAssignments(new BucketAssignments(new String[] {MY_HOST}));
+ assertTrue(state.hasAssignment());
+ }
+
+ @Test
+ public void testRecordInfo_NullSource() {
+ state.setAssignments(ASGN3);
+ state.setLeader(MY_HOST);
+
+ BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST2});
+ state.process(new Identification(null, asgn));
+
+ // leader unchanged
+ assertEquals(MY_HOST, state.getLeader());
+
+ // assignments still updated
+ assertEquals(asgn, state.getAssignments());
+ }
+
+ @Test
+ public void testRecordInfo_SourcePreceedsMyHost() {
+ state.setAssignments(ASGN3);
+ state.setLeader(MY_HOST);
+
+ BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST2});
+ state.process(new Identification(PREV_HOST, asgn));
+
+ // new leader
+ assertEquals(PREV_HOST, state.getLeader());
+
+ // assignments still updated
+ assertEquals(asgn, state.getAssignments());
+ }
+
+ @Test
+ public void testRecordInfo_SourceFollowsMyHost() {
+ mgr.startDistributing(null);
+ state.setLeader(MY_HOST);
+
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2});
+ state.process(new Identification(HOST1, asgn));
+
+ // leader unchanged
+ assertEquals(MY_HOST, state.getLeader());
+
+ // assignments still updated
+ assertEquals(asgn, state.getAssignments());
+ }
+
+ @Test
+ public void testRecordInfo_NewIsNull() {
+ state.setAssignments(ASGN3);
+ state.process(new Identification(HOST1, null));
+
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+ @Test
+ public void testRecordInfo_NewIsEmpty() {
+ state.setAssignments(ASGN3);
+ state.process(new Identification(PREV_HOST, new BucketAssignments()));
+
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+ @Test
+ public void testRecordInfo_OldIsNull() {
+ mgr.startDistributing(null);
+
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2});
+ state.process(new Identification(HOST1, asgn));
+
+ assertEquals(asgn, state.getAssignments());
+ }
+
+ @Test
+ public void testRecordInfo_OldIsEmpty() {
+ state.setAssignments(new BucketAssignments());
+
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2});
+ state.process(new Identification(HOST1, asgn));
+
+ assertEquals(asgn, state.getAssignments());
+ }
+
+ @Test
+ public void testRecordInfo_NewLeaderPreceedsOld() {
+ state.setAssignments(ASGN3);
+ state.setLeader(MY_HOST);
+
+ BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST2});
+ state.process(new Identification(HOST3, asgn));
+
+ assertEquals(asgn, state.getAssignments());
+ }
+
+ @Test
+ public void testRecordInfo_NewLeaderSucceedsOld() {
+ state.setAssignments(ASGN3);
+ state.setLeader(MY_HOST);
+
+ BucketAssignments asgn = new BucketAssignments(new String[] {HOST2, HOST3});
+ state.process(new Identification(HOST3, asgn));
+
+ // should be unchanged
+ assertEquals(ASGN3, state.getAssignments());
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java
new file mode 100644
index 00000000..f29d2348
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java
@@ -0,0 +1,180 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+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.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.message.Forward;
+import org.onap.policy.drools.pooling.message.Heartbeat;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.message.Query;
+
+public class StartStateTest extends BasicStateTester {
+
+ private StartState state;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ state = new StartState(mgr);
+ }
+
+ @Test
+ public void testGetFilter() {
+ Map<String, Object> filter = state.getFilter();
+
+ FilterUtilsTest utils = new FilterUtilsTest();
+
+ utils.checkArray(FilterUtils.CLASS_OR, 2, filter);
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0));
+
+ // get the sub-filter
+ filter = utils.getItem(filter, 1);
+
+ utils.checkArray(FilterUtils.CLASS_AND, 2, filter);
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 0));
+ utils.checkEquals(FilterUtils.MSG_TIMESTAMP, String.valueOf(state.getHbTimestampMs()),
+ utils.getItem(filter, 1));
+ }
+
+ @Test
+ public void testStart() {
+ state.start();
+
+ Pair<String, Heartbeat> msg = capturePublishedMessage(Heartbeat.class);
+
+ assertEquals(MY_HOST, msg.first);
+ assertEquals(state.getHbTimestampMs(), msg.second.getTimestampMs());
+
+ Pair<Long, StateTimerTask> timer = onceTasks.removeFirst();
+
+ assertEquals(STD_HEARTBEAT_WAIT_MS, timer.first.longValue());
+
+ // invoke the task - it should go to the state returned by the mgr
+ State next = mock(State.class);
+ when(mgr.goInactive()).thenReturn(next);
+
+ assertEquals(next, timer.second.fire(null));
+
+ verify(mgr).internalTopicFailed();
+ }
+
+ @Test
+ public void testStartStatePoolingManager() {
+ /*
+ * Prove the state is attached to the manager by invoking getHost(),
+ * which delegates to the manager.
+ */
+ assertEquals(MY_HOST, state.getHost());
+ }
+
+ @Test
+ public void testStartStateState() {
+ // create a new state from the current state
+ state = new StartState(mgr);
+
+ /*
+ * Prove the state is attached to the manager by invoking getHost(),
+ * which delegates to the manager.
+ */
+ assertEquals(MY_HOST, state.getHost());
+ }
+
+ @Test
+ public void testProcessForward() {
+ assertNull(state.process(new Forward()));
+ }
+
+ @Test
+ public void testProcessHeartbeat() {
+ Heartbeat msg = new Heartbeat();
+
+ // no matching data in heart beat
+ assertNull(state.process(msg));
+ verify(mgr, never()).publishAdmin(any());
+
+ // same source, different time stamp
+ msg.setSource(MY_HOST);
+ msg.setTimestampMs(state.getHbTimestampMs() - 1);
+ assertNull(state.process(msg));
+ verify(mgr, never()).publishAdmin(any());
+
+ // same time stamp, different source
+ msg.setSource("unknown");
+ msg.setTimestampMs(state.getHbTimestampMs());
+ assertNull(state.process(msg));
+ verify(mgr, never()).publishAdmin(any());
+
+ // matching heart beat
+ msg.setSource(MY_HOST);
+ msg.setTimestampMs(state.getHbTimestampMs());
+
+ State next = mock(State.class);
+ when(mgr.goQuery()).thenReturn(next);
+
+ assertEquals(next, state.process(msg));
+
+ verify(mgr).publishAdmin(any(Query.class));
+ }
+
+ @Test
+ public void testProcessIdentification() {
+ assertNull(state.process(new Identification(MY_HOST, null)));
+ }
+
+ @Test
+ public void testProcessLeader() {
+ assertNull(state.process(new Leader(MY_HOST, null)));
+ }
+
+ @Test
+ public void testProcessOffline() {
+ assertNull(state.process(new Offline(HOST1)));
+ }
+
+ @Test
+ public void testProcessQuery() {
+ assertNull(state.process(new Query()));
+ }
+
+ @Test
+ public void testGetHbTimestampMs() {
+ long tcurrent = System.currentTimeMillis();
+ assertTrue(new StartState(mgr).getHbTimestampMs() >= tcurrent);
+
+ tcurrent = System.currentTimeMillis();
+ assertTrue(new StartState(mgr).getHbTimestampMs() >= tcurrent);
+ }
+
+}
diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java
new file mode 100644
index 00000000..1be48e21
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java
@@ -0,0 +1,440 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * 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.drools.pooling.state;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+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.Map;
+import java.util.concurrent.ScheduledFuture;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.drools.pooling.PoolingManager;
+import org.onap.policy.drools.pooling.message.BucketAssignments;
+import org.onap.policy.drools.pooling.message.Forward;
+import org.onap.policy.drools.pooling.message.Heartbeat;
+import org.onap.policy.drools.pooling.message.Identification;
+import org.onap.policy.drools.pooling.message.Leader;
+import org.onap.policy.drools.pooling.message.Message;
+import org.onap.policy.drools.pooling.message.Offline;
+import org.onap.policy.drools.pooling.message.Query;
+
+public class StateTest extends BasicStateTester {
+
+ private State state;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ state = new MyState(mgr);
+ }
+
+ @Test
+ public void testStatePoolingManager() {
+ /*
+ * Prove the state is attached to the manager by invoking getHost(),
+ * which delegates to the manager.
+ */
+ assertEquals(MY_HOST, state.getHost());
+ }
+
+ @Test
+ public void testStateState() {
+ // allocate a new state, copying from the old state
+ state = new MyState(mgr);
+
+ /*
+ * Prove the state is attached to the manager by invoking getHost(),
+ * which delegates to the manager.
+ */
+ assertEquals(MY_HOST, state.getHost());
+ }
+
+ @Test
+ public void testCancelTimers() {
+ int delay = 100;
+ int initDelay = 200;
+
+ /*
+ * Create three tasks tasks.
+ */
+
+ StateTimerTask task1 = mock(StateTimerTask.class);
+ StateTimerTask task2 = mock(StateTimerTask.class);
+ StateTimerTask task3 = mock(StateTimerTask.class);
+
+ // two tasks via schedule()
+ state.schedule(delay, task1);
+ state.schedule(delay, task2);
+
+ // one task via scheduleWithFixedDelay()
+ state.scheduleWithFixedDelay(initDelay, delay, task3);
+
+ // ensure all were scheduled, but not yet canceled
+ verify(mgr).schedule(delay, task1);
+ verify(mgr).schedule(delay, task2);
+ verify(mgr).scheduleWithFixedDelay(initDelay, delay, task3);
+
+ ScheduledFuture<?> fut1 = onceFutures.removeFirst();
+ ScheduledFuture<?> fut2 = onceFutures.removeFirst();
+ ScheduledFuture<?> fut3 = repeatedFutures.removeFirst();
+
+ verify(fut1, never()).cancel(false);
+ verify(fut2, never()).cancel(false);
+ verify(fut3, never()).cancel(false);
+
+ /*
+ * Cancel the timers.
+ */
+ state.cancelTimers();
+
+ // verify that all were cancelled
+ verify(fut1).cancel(false);
+ verify(fut2).cancel(false);
+ verify(fut3).cancel(false);
+ }
+
+ @Test
+ public void testGetFilter() {
+ Map<String, Object> filter = state.getFilter();
+
+ FilterUtilsTest utils = new FilterUtilsTest();
+
+ utils.checkArray(FilterUtils.CLASS_OR, 2, filter);
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0));
+ utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1));
+ }
+
+ @Test
+ public void testStart() {
+ state.start();
+ }
+
+ @Test
+ public void testStop() {
+ state.stop();
+
+ assertEquals(MY_HOST, captureAdminMessage(Offline.class).getSource());
+ }
+
+ @Test
+ public void testGoStart() {
+ State next = mock(State.class);
+ when(mgr.goStart()).thenReturn(next);
+
+ State next2 = state.goStart();
+ assertEquals(next, next2);
+ }
+
+ @Test
+ public void testGoQuery() {
+ State next = mock(State.class);
+ when(mgr.goQuery()).thenReturn(next);
+
+ State next2 = state.goQuery();
+ assertEquals(next, next2);
+ }
+
+ @Test
+ public void testGoActive() {
+ State next = mock(State.class);
+ when(mgr.goActive()).thenReturn(next);
+
+ State next2 = state.goActive();
+ assertEquals(next, next2);
+ }
+
+ @Test
+ public void testGoInactive() {
+ State next = mock(State.class);
+ when(mgr.goInactive()).thenReturn(next);
+
+ State next2 = state.goInactive();
+ assertEquals(next, next2);
+ }
+
+ @Test
+ public void testProcessForward() {
+ Forward msg = new Forward();
+ assertNull(state.process(msg));
+
+ verify(mgr).handle(msg);
+ }
+
+ @Test
+ public void testProcessHeartbeat() {
+ assertNull(state.process(new Heartbeat()));
+ }
+
+ @Test
+ public void testProcessIdentification() {
+ assertNull(state.process(new Identification()));
+ }
+
+ @Test
+ public void testProcessLeader_NullAssignment() {
+ Leader msg = new Leader(PREV_HOST, null);
+
+ // should stay in the same state, and not start distributing
+ assertNull(state.process(msg));
+ verify(mgr, never()).startDistributing(any());
+ verify(mgr, never()).goActive();
+ verify(mgr, never()).goInactive();
+ }
+
+ @Test
+ public void testProcessLeader_NullSource() {
+ String[] arr = {HOST2, PREV_HOST, MY_HOST};
+ BucketAssignments asgn = new BucketAssignments(arr);
+ Leader msg = new Leader(null, asgn);
+
+ // should stay in the same state, and not start distributing
+ assertNull(state.process(msg));
+ verify(mgr, never()).startDistributing(any());
+ verify(mgr, never()).goActive();
+ verify(mgr, never()).goInactive();
+ }
+
+ @Test
+ public void testProcessLeader_EmptyAssignment() {
+ Leader msg = new Leader(PREV_HOST, new BucketAssignments());
+
+ // should stay in the same state, and not start distributing
+ assertNull(state.process(msg));
+ verify(mgr, never()).startDistributing(any());
+ verify(mgr, never()).goActive();
+ verify(mgr, never()).goInactive();
+ }
+
+ @Test
+ public void testProcessLeader_MyHostAssigned() {
+ String[] arr = {HOST2, PREV_HOST, MY_HOST};
+ BucketAssignments asgn = new BucketAssignments(arr);
+ Leader msg = new Leader(PREV_HOST, asgn);
+
+ State next = mock(State.class);
+ when(mgr.goActive()).thenReturn(next);
+
+ // should go Active and start distributing
+ assertEquals(next, state.process(msg));
+ verify(mgr).startDistributing(asgn);
+ verify(mgr, never()).goInactive();
+ }
+
+ @Test
+ public void testProcessLeader_MyHostUnassigned() {
+ String[] arr = {HOST2, HOST1};
+ BucketAssignments asgn = new BucketAssignments(arr);
+ Leader msg = new Leader(HOST1, asgn);
+
+ State next = mock(State.class);
+ when(mgr.goInactive()).thenReturn(next);
+
+ // should go Inactive and start distributing
+ assertEquals(next, state.process(msg));
+ verify(mgr).startDistributing(asgn);
+ verify(mgr, never()).goActive();
+ }
+
+ @Test
+ public void testProcessOffline() {
+ assertNull(state.process(new Offline()));
+ }
+
+ @Test
+ public void testProcessQuery() {
+ assertNull(state.process(new Query()));
+ }
+
+ @Test
+ public void testPublishIdentification() {
+ Identification msg = new Identification();
+ state.publish(msg);
+
+ verify(mgr).publishAdmin(msg);
+ }
+
+ @Test
+ public void testPublishLeader() {
+ Leader msg = new Leader();
+ state.publish(msg);
+
+ verify(mgr).publishAdmin(msg);
+ }
+
+ @Test
+ public void testPublishOffline() {
+ Offline msg = new Offline();
+ state.publish(msg);
+
+ verify(mgr).publishAdmin(msg);
+ }
+
+ @Test
+ public void testPublishQuery() {
+ Query msg = new Query();
+ state.publish(msg);
+
+ verify(mgr).publishAdmin(msg);
+ }
+
+ @Test
+ public void testPublishStringForward() {
+ String chnl = "channelF";
+ Forward msg = new Forward();
+
+ state.publish(chnl, msg);
+
+ verify(mgr).publish(chnl, msg);
+ }
+
+ @Test
+ public void testPublishStringHeartbeat() {
+ String chnl = "channelH";
+ Heartbeat msg = new Heartbeat();
+
+ state.publish(chnl, msg);
+
+ verify(mgr).publish(chnl, msg);
+ }
+
+ @Test
+ public void testStartDistributing() {
+ BucketAssignments asgn = new BucketAssignments();
+ state.startDistributing(asgn);
+
+ verify(mgr).startDistributing(asgn);
+ }
+
+ @Test
+ public void testStartDistributing_NullAssignments() {
+ state.startDistributing(null);
+
+ verify(mgr, never()).startDistributing(any());
+ }
+
+ @Test
+ public void testSchedule() {
+ int delay = 100;
+
+ StateTimerTask task = mock(StateTimerTask.class);
+
+ state.schedule(delay, task);
+
+ ScheduledFuture<?> fut = onceFutures.removeFirst();
+
+ // scheduled, but not canceled yet
+ verify(mgr).schedule(delay, task);
+ verify(fut, never()).cancel(false);
+
+ /*
+ * Ensure the state added the timer to its list by telling it to cancel
+ * its timers and then seeing if this timer was canceled.
+ */
+ state.cancelTimers();
+ verify(fut).cancel(false);
+ }
+
+ @Test
+ public void testScheduleWithFixedDelay() {
+ int initdel = 100;
+ int delay = 200;
+
+ StateTimerTask task = mock(StateTimerTask.class);
+
+ state.scheduleWithFixedDelay(initdel, delay, task);
+
+ ScheduledFuture<?> fut = repeatedFutures.removeFirst();
+
+ // scheduled, but not canceled yet
+ verify(mgr).scheduleWithFixedDelay(initdel, delay, task);
+ verify(fut, never()).cancel(false);
+
+ /*
+ * Ensure the state added the timer to its list by telling it to cancel
+ * its timers and then seeing if this timer was canceled.
+ */
+ state.cancelTimers();
+ verify(fut).cancel(false);
+ }
+
+ @Test
+ public void testInternalTopicFailed() {
+ State next = mock(State.class);
+ when(mgr.goInactive()).thenReturn(next);
+
+ State next2 = state.internalTopicFailed();
+ assertEquals(next, next2);
+
+ verify(mgr).internalTopicFailed();
+
+ Offline msg = captureAdminMessage(Offline.class);
+ assertEquals(MY_HOST, msg.getSource());
+ }
+
+ @Test
+ public void testMakeHeartbeat() {
+ long timestamp = 30000L;
+ Heartbeat msg = state.makeHeartbeat(timestamp);
+
+ assertEquals(MY_HOST, msg.getSource());
+ assertEquals(timestamp, msg.getTimestampMs());
+ }
+
+ @Test
+ public void testMakeOffline() {
+ Offline msg = state.makeOffline();
+
+ assertEquals(MY_HOST, msg.getSource());
+ }
+
+ @Test
+ public void testMakeQuery() {
+ Query msg = state.makeQuery();
+
+ assertEquals(MY_HOST, msg.getSource());
+ }
+
+ @Test
+ public void testGetHost() {
+ assertEquals(MY_HOST, state.getHost());
+ }
+
+ @Test
+ public void testGetTopic() {
+ assertEquals(MY_TOPIC, state.getTopic());
+ }
+
+ /**
+ * State used for testing purposes, with abstract methods implemented.
+ */
+ private class MyState extends State {
+
+ public MyState(PoolingManager mgr) {
+ super(mgr);
+ }
+ }
+}
diff --git a/feature-pooling-dmaap/src/test/resources/logback-test.xml b/feature-pooling-dmaap/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..6f745157
--- /dev/null
+++ b/feature-pooling-dmaap/src/test/resources/logback-test.xml
@@ -0,0 +1,17 @@
+<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>
+
+ <logger name="org.onap.policy.drools.http.server.test" level="INFO"/>
+
+ <root level="WARN">
+ <appender-ref ref="STDOUT"/>
+ </root>
+
+</configuration> \ No newline at end of file