Add junit coverage to PolicyEngine class 24/69924/2
authorJim Hahn <jrh3@att.com>
Thu, 4 Oct 2018 21:47:24 +0000 (17:47 -0400)
committerJim Hahn <jrh3@att.com>
Fri, 5 Oct 2018 16:50:59 +0000 (12:50 -0400)
Also fixed a typo in a test comment.

Change-Id: I4ad72cc7c103014e6f5920f912c862560af5a331
Issue-ID: POLICY-1148
Signed-off-by: Jim Hahn <jrh3@att.com>
policy-management/src/main/java/org/onap/policy/drools/system/PolicyEngine.java
policy-management/src/test/java/org/onap/policy/drools/system/PolicyEngineManagerTest.java [new file with mode: 0644]

index 60e5a1b..b181ff5 100644 (file)
@@ -38,6 +38,7 @@ import org.onap.policy.common.endpoints.event.comm.TopicListener;
 import org.onap.policy.common.endpoints.event.comm.TopicSink;
 import org.onap.policy.common.endpoints.event.comm.TopicSource;
 import org.onap.policy.common.endpoints.http.server.HttpServletServer;
+import org.onap.policy.common.endpoints.http.server.HttpServletServerFactory;
 import org.onap.policy.common.endpoints.properties.PolicyEndPointProperties;
 import org.onap.policy.drools.controller.DroolsController;
 import org.onap.policy.drools.core.PolicyContainer;
@@ -392,7 +393,7 @@ class PolicyEngineManager implements PolicyEngine {
     @Override
     public synchronized void boot(String[] cliArgs) {
 
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.beforeBoot(this, cliArgs)) {
                     return;
@@ -404,12 +405,12 @@ class PolicyEngineManager implements PolicyEngine {
         }
 
         try {
-            PolicyContainer.globalInit(cliArgs);
+            globalInitContainer(cliArgs);
         } catch (final Exception e) {
             logger.error("{}: cannot init policy-container because of {}", this, e.getMessage(), e);
         }
 
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.afterBoot(this)) {
                     return;
@@ -478,7 +479,7 @@ class PolicyEngineManager implements PolicyEngine {
         }
 
         /* policy-engine dispatch pre configure hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.beforeConfigure(this, properties)) {
                     return;
@@ -492,7 +493,7 @@ class PolicyEngineManager implements PolicyEngine {
         this.properties = properties;
 
         try {
-            this.sources = TopicEndpoint.manager.addTopicSources(properties);
+            this.sources = getTopicEndpointManager().addTopicSources(properties);
             for (final TopicSource source : this.sources) {
                 source.register(this);
             }
@@ -501,19 +502,19 @@ class PolicyEngineManager implements PolicyEngine {
         }
 
         try {
-            this.sinks = TopicEndpoint.manager.addTopicSinks(properties);
+            this.sinks = getTopicEndpointManager().addTopicSinks(properties);
         } catch (final IllegalArgumentException e) {
             logger.error("{}: add-sinks failed", this, e);
         }
 
         try {
-            this.httpServers = HttpServletServer.factory.build(properties);
+            this.httpServers = getServletFactory().build(properties);
         } catch (final IllegalArgumentException e) {
             logger.error("{}: add-http-servers failed", this, e);
         }
 
         /* policy-engine dispatch post configure hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.afterConfigure(this)) {
                     return;
@@ -571,7 +572,7 @@ class PolicyEngineManager implements PolicyEngine {
         }
 
         PolicyController controller;
-        for (final PolicyControllerFeatureAPI controllerFeature : PolicyControllerFeatureAPI.providers.getList()) {
+        for (final PolicyControllerFeatureAPI controllerFeature : getControllerProviders()) {
             try {
                 controller = controllerFeature.beforeCreate(tempName, properties);
                 if (controller != null) {
@@ -583,13 +584,13 @@ class PolicyEngineManager implements PolicyEngine {
             }
         }
 
-        controller = PolicyController.factory.build(tempName, properties);
+        controller = getControllerFactory().build(tempName, properties);
         if (this.isLocked()) {
             controller.lock();
         }
 
         // feature hook
-        for (final PolicyControllerFeatureAPI controllerFeature : PolicyControllerFeatureAPI.providers.getList()) {
+        for (final PolicyControllerFeatureAPI controllerFeature : getControllerProviders()) {
             try {
                 if (controllerFeature.afterCreate(controller)) {
                     return controller;
@@ -609,9 +610,7 @@ class PolicyEngineManager implements PolicyEngine {
 
         final List<PolicyController> policyControllers = new ArrayList<>();
         if (configControllers == null || configControllers.isEmpty()) {
-            if (logger.isInfoEnabled()) {
-                logger.info("No controller configuration provided: " + configControllers);
-            }
+            logger.info("No controller configuration provided: {}", configControllers);
             return policyControllers;
         }
 
@@ -656,7 +655,7 @@ class PolicyEngineManager implements PolicyEngine {
             }
 
             try {
-                policyController = PolicyController.factory.get(controllerName);
+                policyController = getControllerFactory().get(controllerName);
             } catch (final IllegalArgumentException e) {
                 // not found
                 logger.warn("Policy Controller " + controllerName + " not found", e);
@@ -674,7 +673,7 @@ class PolicyEngineManager implements PolicyEngine {
                 logger.warn("controller {} does not exist. Attempting recovery from disk", controllerName);
 
                 final Properties controllerProperties =
-                        SystemPersistence.manager.getControllerProperties(controllerName);
+                        getPersistenceManager().getControllerProperties(controllerName);
 
                 /*
                  * returned properties cannot be null (per implementation) assert (properties !=
@@ -696,18 +695,18 @@ class PolicyEngineManager implements PolicyEngine {
                 controllerProperties.setProperty(DroolsProperties.RULES_ARTIFACTID, DroolsController.NO_ARTIFACT_ID);
                 controllerProperties.setProperty(DroolsProperties.RULES_VERSION, DroolsController.NO_VERSION);
 
-                policyController = PolicyEngine.manager.createPolicyController(controllerName, controllerProperties);
+                policyController = getPolicyEngine().createPolicyController(controllerName, controllerProperties);
 
                 /* fall through to do brain update operation */
             }
 
             switch (operation) {
                 case ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_CREATE:
-                    PolicyController.factory.patch(policyController, configController.getDrools());
+                    getControllerFactory().patch(policyController, configController.getDrools());
                     break;
                 case ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_UPDATE:
                     policyController.unlock();
-                    PolicyController.factory.patch(policyController, configController.getDrools());
+                    getControllerFactory().patch(policyController, configController.getDrools());
                     break;
                 case ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_LOCK:
                     policyController.lock();
@@ -736,7 +735,7 @@ class PolicyEngineManager implements PolicyEngine {
     public synchronized boolean start() {
 
         /* policy-engine dispatch pre start hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.beforeStart(this)) {
                     return true;
@@ -792,7 +791,7 @@ class PolicyEngineManager implements PolicyEngine {
 
         /* Start Policy Controllers */
 
-        final List<PolicyController> controllers = PolicyController.factory.inventory();
+        final List<PolicyController> controllers = getControllerFactory().inventory();
         for (final PolicyController controller : controllers) {
             try {
                 if (!controller.start()) {
@@ -808,7 +807,7 @@ class PolicyEngineManager implements PolicyEngine {
         /* Start managed Topic Endpoints */
 
         try {
-            if (!TopicEndpoint.manager.start()) {
+            if (!getTopicEndpointManager().start()) {
                 success = false;
             }
         } catch (final IllegalStateException e) {
@@ -818,10 +817,10 @@ class PolicyEngineManager implements PolicyEngine {
 
         // Start the JMX listener
 
-        PdpJmxListener.start();
+        startPdpJmxListener();
 
         /* policy-engine dispatch after start hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.afterStart(this)) {
                     return success;
@@ -839,7 +838,7 @@ class PolicyEngineManager implements PolicyEngine {
     public synchronized boolean stop() {
 
         /* policy-engine dispatch pre stop hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.beforeStop(this)) {
                     return true;
@@ -859,7 +858,7 @@ class PolicyEngineManager implements PolicyEngine {
 
         this.alive = false;
 
-        final List<PolicyController> controllers = PolicyController.factory.inventory();
+        final List<PolicyController> controllers = getControllerFactory().inventory();
         for (final PolicyController controller : controllers) {
             try {
                 if (!controller.stop()) {
@@ -894,7 +893,7 @@ class PolicyEngineManager implements PolicyEngine {
         }
 
         /* stop all managed topics sources and sinks */
-        if (!TopicEndpoint.manager.stop()) {
+        if (!getTopicEndpointManager().stop()) {
             success = false;
         }
 
@@ -908,9 +907,11 @@ class PolicyEngineManager implements PolicyEngine {
                 logger.error("{}: cannot start http-server {} because of {}", this, httpServer, e.getMessage(), e);
             }
         }
+        
+        // stop JMX?
 
         /* policy-engine dispatch pre stop hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.afterStop(this)) {
                     return success;
@@ -932,42 +933,11 @@ class PolicyEngineManager implements PolicyEngine {
          * ..) are stuck
          */
 
-        Thread exitThread = new Thread(new Runnable() {
-            private static final long SHUTDOWN_MAX_GRACE_TIME = 30000L;
-
-            @Override
-            public void run() {
-                try {
-                    Thread.sleep(SHUTDOWN_MAX_GRACE_TIME);
-                    logger.warn("{}: abnormal termination - shutdown graceful time period expiration",
-                            PolicyEngineManager.this);
-                } catch (final InterruptedException e) {
-                    /* courtesy to shutdown() to allow it to return */
-                    synchronized (PolicyEngineManager.this) {
-                    }
-                    logger.info("{}: finishing a graceful shutdown ", PolicyEngineManager.this, e);
-                } finally {
-                    /*
-                     * shut down the Policy Engine owned http servers as the very last thing
-                     */
-                    for (final HttpServletServer httpServer : PolicyEngineManager.this.getHttpServers()) {
-                        try {
-                            httpServer.shutdown();
-                        } catch (final Exception e) {
-                            logger.error("{}: cannot shutdown http-server {} because of {}", PolicyEngineManager.this,
-                                    httpServer, e.getMessage(), e);
-                        }
-                    }
-
-                    logger.info("{}: exit", PolicyEngineManager.this);
-                    System.exit(0);
-                }
-            }
-        });
+        Thread exitThread = makeShutdownThread();
         exitThread.start();
 
         /* policy-engine dispatch pre shutdown hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.beforeShutdown(this)) {
                     return;
@@ -999,16 +969,16 @@ class PolicyEngineManager implements PolicyEngine {
         }
 
         /* Shutdown managed resources */
-        PolicyController.factory.shutdown();
-        TopicEndpoint.manager.shutdown();
-        HttpServletServer.factory.destroy();
+        getControllerFactory().shutdown();
+        getTopicEndpointManager().shutdown();
+        getServletFactory().destroy();
 
         // Stop the JMX listener
 
-        PdpJmxListener.stop();
+        stopPdpJmxListener();
 
         /* policy-engine dispatch post shutdown hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.afterShutdown(this)) {
                     return;
@@ -1022,6 +992,52 @@ class PolicyEngineManager implements PolicyEngine {
         exitThread.interrupt();
         logger.info("{}: normal termination", this);
     }
+    
+    /**
+     * Thread that shuts down http servers.
+     */
+    protected class ShutdownThread extends Thread {
+        private static final long SHUTDOWN_MAX_GRACE_TIME = 30000L;
+
+        @Override
+        public void run() {
+            try {
+                doSleep(SHUTDOWN_MAX_GRACE_TIME);
+                logger.warn("{}: abnormal termination - shutdown graceful time period expiration",
+                        PolicyEngineManager.this);
+            } catch (final InterruptedException e) {
+                /* courtesy to shutdown() to allow it to return */
+                synchronized (PolicyEngineManager.this) {
+                }
+                logger.info("{}: finishing a graceful shutdown ", PolicyEngineManager.this, e);
+            } finally {
+                /*
+                 * shut down the Policy Engine owned http servers as the very last thing
+                 */
+                for (final HttpServletServer httpServer : PolicyEngineManager.this.getHttpServers()) {
+                    try {
+                        httpServer.shutdown();
+                    } catch (final Exception e) {
+                        logger.error("{}: cannot shutdown http-server {} because of {}", PolicyEngineManager.this,
+                                httpServer, e.getMessage(), e);
+                    }
+                }
+
+                logger.info("{}: exit", PolicyEngineManager.this);
+                doExit(0);
+            }
+        }
+        
+        // these may be overridden by junit tests
+
+        protected void doSleep(long sleepMs) throws InterruptedException {
+            Thread.sleep(sleepMs);
+        }
+        
+        protected void doExit(int code) {
+            System.exit(code);
+        }
+    }
 
     @Override
     public boolean isAlive() {
@@ -1032,7 +1048,7 @@ class PolicyEngineManager implements PolicyEngine {
     public synchronized boolean lock() {
 
         /* policy-engine dispatch pre lock hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.beforeLock(this)) {
                     return true;
@@ -1050,7 +1066,7 @@ class PolicyEngineManager implements PolicyEngine {
         this.locked = true;
 
         boolean success = true;
-        final List<PolicyController> controllers = PolicyController.factory.inventory();
+        final List<PolicyController> controllers = getControllerFactory().inventory();
         for (final PolicyController controller : controllers) {
             try {
                 success = controller.lock() && success;
@@ -1060,10 +1076,10 @@ class PolicyEngineManager implements PolicyEngine {
             }
         }
 
-        success = TopicEndpoint.manager.lock() && success;
+        success = getTopicEndpointManager().lock() && success;
 
         /* policy-engine dispatch post lock hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.afterLock(this)) {
                     return success;
@@ -1081,7 +1097,7 @@ class PolicyEngineManager implements PolicyEngine {
     public synchronized boolean unlock() {
 
         /* policy-engine dispatch pre unlock hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.beforeUnlock(this)) {
                     return true;
@@ -1099,7 +1115,7 @@ class PolicyEngineManager implements PolicyEngine {
         this.locked = false;
 
         boolean success = true;
-        final List<PolicyController> controllers = PolicyController.factory.inventory();
+        final List<PolicyController> controllers = getControllerFactory().inventory();
         for (final PolicyController controller : controllers) {
             try {
                 success = controller.unlock() && success;
@@ -1110,10 +1126,10 @@ class PolicyEngineManager implements PolicyEngine {
             }
         }
 
-        success = TopicEndpoint.manager.unlock() && success;
+        success = getTopicEndpointManager().unlock() && success;
 
         /* policy-engine dispatch after unlock hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.afterUnlock(this)) {
                     return success;
@@ -1134,25 +1150,25 @@ class PolicyEngineManager implements PolicyEngine {
 
     @Override
     public void removePolicyController(String name) {
-        PolicyController.factory.destroy(name);
+        getControllerFactory().destroy(name);
     }
 
     @Override
     public void removePolicyController(PolicyController controller) {
-        PolicyController.factory.destroy(controller);
+        getControllerFactory().destroy(controller);
     }
 
     @JsonIgnore
     @Override
     public List<PolicyController> getPolicyControllers() {
-        return PolicyController.factory.inventory();
+        return getControllerFactory().inventory();
     }
 
     @JsonProperty("controllers")
     @Override
     public List<String> getPolicyControllerIds() {
         final List<String> controllerNames = new ArrayList<>();
-        for (final PolicyController controller : PolicyController.factory.inventory()) {
+        for (final PolicyController controller : getControllerFactory().inventory()) {
             controllerNames.add(controller.getName());
         }
         return controllerNames;
@@ -1185,7 +1201,7 @@ class PolicyEngineManager implements PolicyEngine {
     @Override
     public List<String> getFeatures() {
         final List<String> features = new ArrayList<>();
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             features.add(feature.getName());
         }
         return features;
@@ -1194,7 +1210,7 @@ class PolicyEngineManager implements PolicyEngine {
     @JsonIgnore
     @Override
     public List<PolicyEngineFeatureAPI> getFeatureProviders() {
-        return PolicyEngineFeatureAPI.providers.getList();
+        return getEngineProviders();
     }
 
     @Override
@@ -1203,7 +1219,7 @@ class PolicyEngineManager implements PolicyEngine {
             throw new IllegalArgumentException("A feature name must be provided");
         }
 
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             if (feature.getName().equals(featureName)) {
                 return feature;
             }
@@ -1246,8 +1262,8 @@ class PolicyEngineManager implements PolicyEngine {
             throw new IllegalStateException(ENGINE_LOCKED_MSG);
         }
 
-        final List<? extends TopicSink> topicSinks = TopicEndpoint.manager.getTopicSinks(topic);
-        if (topicSinks == null || topicSinks.isEmpty() || topicSinks.size() > 1) {
+        final List<? extends TopicSink> topicSinks = getTopicEndpointManager().getTopicSinks(topic);
+        if (topicSinks == null || topicSinks.size() != 1) {
             throw new IllegalStateException("Cannot ensure correct delivery on topic " + topic + ": " + topicSinks);
         }
 
@@ -1321,8 +1337,8 @@ class PolicyEngineManager implements PolicyEngine {
          * additional processing
          */
         try {
-            final DroolsController droolsController = EventProtocolCoder.manager.getDroolsController(topic, event);
-            final PolicyController controller = PolicyController.factory.get(droolsController);
+            final DroolsController droolsController = getProtocolCoder().getDroolsController(topic, event);
+            final PolicyController controller = getControllerFactory().get(droolsController);
             if (controller != null) {
                 return controller.deliver(busType, topic, event);
             }
@@ -1337,7 +1353,7 @@ class PolicyEngineManager implements PolicyEngine {
          * cannot route through the controller, send directly through the topic sink
          */
         try {
-            final String json = EventProtocolCoder.manager.encode(topic, event);
+            final String json = getProtocolCoder().encode(topic, event);
             return this.deliver(busType, topic, json);
 
         } catch (final Exception e) {
@@ -1367,7 +1383,7 @@ class PolicyEngineManager implements PolicyEngine {
         }
 
         try {
-            final TopicSink sink = TopicEndpoint.manager.getTopicSink(busType, topic);
+            final TopicSink sink = getTopicEndpointManager().getTopicSink(busType, topic);
 
             if (sink == null) {
                 throw new IllegalStateException("Inconsistent State: " + this);
@@ -1386,7 +1402,7 @@ class PolicyEngineManager implements PolicyEngine {
     public synchronized void activate() {
 
         /* policy-engine dispatch pre activate hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.beforeActivate(this)) {
                     return;
@@ -1414,7 +1430,7 @@ class PolicyEngineManager implements PolicyEngine {
         this.unlock();
 
         /* policy-engine dispatch post activate hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.afterActivate(this)) {
                     return;
@@ -1430,7 +1446,7 @@ class PolicyEngineManager implements PolicyEngine {
     public synchronized void deactivate() {
 
         /* policy-engine dispatch pre deactivate hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.beforeDeactivate(this)) {
                     return;
@@ -1453,7 +1469,7 @@ class PolicyEngineManager implements PolicyEngine {
         }
 
         /* policy-engine dispatch post deactivate hook */
-        for (final PolicyEngineFeatureAPI feature : PolicyEngineFeatureAPI.providers.getList()) {
+        for (final PolicyEngineFeatureAPI feature : getEngineProviders()) {
             try {
                 if (feature.afterDeactivate(this)) {
                     return;
@@ -1487,6 +1503,55 @@ class PolicyEngineManager implements PolicyEngine {
         return "PolicyEngineManager [alive=" + this.alive + ", locked=" + this.locked + "]";
     }
 
+    // these methods may be overridden by junit tests
+
+    protected List<PolicyEngineFeatureAPI> getEngineProviders() {
+        return PolicyEngineFeatureAPI.providers.getList();
+    }
+
+    protected List<PolicyControllerFeatureAPI> getControllerProviders() {
+        return PolicyControllerFeatureAPI.providers.getList();
+    }
+
+    protected void globalInitContainer(String[] cliArgs) {
+        PolicyContainer.globalInit(cliArgs);
+    }
+
+    protected TopicEndpoint getTopicEndpointManager() {
+        return TopicEndpoint.manager;
+    }
+
+    protected HttpServletServerFactory getServletFactory() {
+        return HttpServletServer.factory;
+    }
+
+    protected PolicyControllerFactory getControllerFactory() {
+        return PolicyController.factory;
+    }
+
+    protected void startPdpJmxListener() {
+        PdpJmxListener.start();
+    }
+
+    protected void stopPdpJmxListener() {
+        PdpJmxListener.stop();
+    }
+
+    protected Thread makeShutdownThread() {
+        return new ShutdownThread();
+    }
+
+    protected EventProtocolCoder getProtocolCoder() {
+        return EventProtocolCoder.manager;
+    }
+
+    protected SystemPersistence getPersistenceManager() {
+        return SystemPersistence.manager;
+    }
+
+    protected PolicyEngine getPolicyEngine() {
+        return PolicyEngine.manager;
+    }
 }
 
 
diff --git a/policy-management/src/test/java/org/onap/policy/drools/system/PolicyEngineManagerTest.java b/policy-management/src/test/java/org/onap/policy/drools/system/PolicyEngineManagerTest.java
new file mode 100644 (file)
index 0000000..0cc880d
--- /dev/null
@@ -0,0 +1,1742 @@
+/*
+ * ============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.system;
+
+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.Matchers.anyLong;
+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 static org.onap.policy.common.utils.test.PolicyAssert.assertThrows;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpoint;
+import org.onap.policy.common.endpoints.event.comm.TopicSink;
+import org.onap.policy.common.endpoints.event.comm.TopicSource;
+import org.onap.policy.common.endpoints.http.server.HttpServletServer;
+import org.onap.policy.common.endpoints.http.server.HttpServletServerFactory;
+import org.onap.policy.common.utils.test.PolicyAssert.RunnableWithEx;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.features.PolicyControllerFeatureAPI;
+import org.onap.policy.drools.features.PolicyEngineFeatureAPI;
+import org.onap.policy.drools.persistence.SystemPersistence;
+import org.onap.policy.drools.properties.DroolsProperties;
+import org.onap.policy.drools.protocol.coders.EventProtocolCoder;
+import org.onap.policy.drools.protocol.configuration.ControllerConfiguration;
+import org.onap.policy.drools.protocol.configuration.DroolsConfiguration;
+import org.onap.policy.drools.protocol.configuration.PdpdConfiguration;
+
+public class PolicyEngineManagerTest {
+
+    private static final String EXPECTED = "expected exception";
+
+    private static final String NOOP_STR = CommInfrastructure.NOOP.name();
+    private static final String MY_NAME = "my-name";
+    private static final String CONTROLLER1 = "controller-a";
+    private static final String CONTROLLER2 = "controller-b";
+    private static final String CONTROLLER3 = "controller-c";
+    private static final String CONTROLLER4 = "controller-d";
+    private static final String FEATURE1 = "feature-a";
+    private static final String FEATURE2 = "feature-b";
+    private static final String MY_TOPIC = "my-topic";
+    private static final String MESSAGE = "my-message";
+
+    private static final Object MY_EVENT = new Object();
+
+    private static final Gson encoder = new GsonBuilder().disableHtmlEscaping().create();
+
+    private Properties properties;
+    private PolicyEngineFeatureAPI prov1;
+    private PolicyEngineFeatureAPI prov2;
+    private List<PolicyEngineFeatureAPI> providers;
+    private PolicyControllerFeatureAPI contProv1;
+    private PolicyControllerFeatureAPI contProv2;
+    private List<PolicyControllerFeatureAPI> contProviders;
+    private String[] globalInitArgs;
+    private TopicSource source1;
+    private TopicSource source2;
+    private List<TopicSource> sources;
+    private TopicSink sink1;
+    private TopicSink sink2;
+    private List<TopicSink> sinks;
+    private HttpServletServer server1;
+    private HttpServletServer server2;
+    private List<HttpServletServer> servers;
+    private HttpServletServerFactory serverFactory;
+    private TopicEndpoint endpoint;
+    private PolicyController controller;
+    private PolicyController controller2;
+    private PolicyController controller3;
+    private PolicyController controller4;
+    private List<PolicyController> controllers;
+    private PolicyControllerFactory controllerFactory;
+    private boolean jmxStarted;
+    private boolean jmxStopped;
+    private long threadSleepMs;
+    private int threadExitCode;
+    private boolean threadStarted;
+    private boolean threadInterrupted;
+    private Thread shutdownThread;
+    private boolean shouldInterrupt;
+    private EventProtocolCoder coder;
+    private SystemPersistence persist;
+    private PolicyEngine engine;
+    private DroolsConfiguration drools3;
+    private DroolsConfiguration drools4;
+    private ControllerConfiguration config3;
+    private ControllerConfiguration config4;
+    private PdpdConfiguration pdpConfig;
+    private String pdpConfigJson;
+    private PolicyEngineManager mgr;
+
+    /**
+     * Initializes the object to be tested.
+     *
+     * @throws Exception if an error occurs
+     */
+    @Before
+    public void setUp() throws Exception {
+
+        properties = new Properties();
+        prov1 = mock(PolicyEngineFeatureAPI.class);
+        prov2 = mock(PolicyEngineFeatureAPI.class);
+        providers = Arrays.asList(prov1, prov2);
+        contProv1 = mock(PolicyControllerFeatureAPI.class);
+        contProv2 = mock(PolicyControllerFeatureAPI.class);
+        contProviders = Arrays.asList(contProv1, contProv2);
+        globalInitArgs = null;
+        source1 = mock(TopicSource.class);
+        source2 = mock(TopicSource.class);
+        sources = Arrays.asList(source1, source2);
+        sink1 = mock(TopicSink.class);
+        sink2 = mock(TopicSink.class);
+        sinks = Arrays.asList(sink1, sink2);
+        server1 = mock(HttpServletServer.class);
+        server2 = mock(HttpServletServer.class);
+        servers = Arrays.asList(server1, server2);
+        serverFactory = mock(HttpServletServerFactory.class);
+        endpoint = mock(TopicEndpoint.class);
+        controller = mock(PolicyController.class);
+        controller2 = mock(PolicyController.class);
+        controller3 = mock(PolicyController.class);
+        controller4 = mock(PolicyController.class);
+        // do NOT include controller3 or controller4 in the list
+        controllers = Arrays.asList(controller, controller2);
+        controllerFactory = mock(PolicyControllerFactory.class);
+        jmxStarted = false;
+        jmxStopped = false;
+        threadSleepMs = -1;
+        threadExitCode = -1;
+        threadStarted = false;
+        threadInterrupted = false;
+        shutdownThread = null;
+        shouldInterrupt = false;
+        coder = mock(EventProtocolCoder.class);
+        persist = mock(SystemPersistence.class);
+        engine = mock(PolicyEngine.class);
+        drools3 = new DroolsConfiguration();
+        drools4 = new DroolsConfiguration();
+        config3 = new ControllerConfiguration();
+        config4 = new ControllerConfiguration();
+        pdpConfig = new PdpdConfiguration();
+
+        when(prov1.getName()).thenReturn(FEATURE1);
+        when(prov2.getName()).thenReturn(FEATURE2);
+
+        when(controllerFactory.build(any(), any())).thenReturn(controller);
+        when(controllerFactory.inventory()).thenReturn(controllers);
+        when(controllerFactory.get(CONTROLLER1)).thenReturn(controller);
+        when(controllerFactory.get(CONTROLLER2)).thenReturn(controller2);
+        // do NOT return controller3 or controller4
+
+        when(server1.waitedStart(anyLong())).thenReturn(true);
+        when(server1.stop()).thenReturn(true);
+
+        when(server2.waitedStart(anyLong())).thenReturn(true);
+        when(server2.stop()).thenReturn(true);
+
+        when(serverFactory.build(any())).thenReturn(servers);
+
+        when(source1.start()).thenReturn(true);
+        when(source1.stop()).thenReturn(true);
+
+        when(source2.start()).thenReturn(true);
+        when(source2.stop()).thenReturn(true);
+
+        when(sink1.start()).thenReturn(true);
+        when(sink1.stop()).thenReturn(true);
+        when(sink1.send(any())).thenReturn(true);
+        when(sink1.getTopicCommInfrastructure()).thenReturn(CommInfrastructure.NOOP);
+
+        when(sink2.start()).thenReturn(true);
+        when(sink2.stop()).thenReturn(true);
+
+        when(controller.getName()).thenReturn(CONTROLLER1);
+        when(controller.start()).thenReturn(true);
+        when(controller.stop()).thenReturn(true);
+        when(controller.lock()).thenReturn(true);
+        when(controller.unlock()).thenReturn(true);
+
+        when(controller2.getName()).thenReturn(CONTROLLER2);
+        when(controller2.start()).thenReturn(true);
+        when(controller2.stop()).thenReturn(true);
+        when(controller2.lock()).thenReturn(true);
+        when(controller2.unlock()).thenReturn(true);
+
+        when(controller3.getName()).thenReturn(CONTROLLER3);
+        when(controller3.start()).thenReturn(true);
+        when(controller3.stop()).thenReturn(true);
+        when(controller3.lock()).thenReturn(true);
+        when(controller3.unlock()).thenReturn(true);
+
+        when(controller4.getName()).thenReturn(CONTROLLER4);
+        when(controller4.start()).thenReturn(true);
+        when(controller4.stop()).thenReturn(true);
+        when(controller4.lock()).thenReturn(true);
+        when(controller4.unlock()).thenReturn(true);
+
+        when(endpoint.addTopicSources(any())).thenReturn(sources);
+        when(endpoint.addTopicSinks(any())).thenReturn(sinks);
+        when(endpoint.start()).thenReturn(true);
+        when(endpoint.stop()).thenReturn(true);
+        when(endpoint.lock()).thenReturn(true);
+        when(endpoint.unlock()).thenReturn(true);
+        when(endpoint.getTopicSink(CommInfrastructure.NOOP, MY_TOPIC)).thenReturn(sink1);
+        when(endpoint.getTopicSinks(MY_TOPIC)).thenReturn(Arrays.asList(sink1));
+
+        when(coder.encode(any(), any())).thenReturn(MESSAGE);
+
+        when(persist.getControllerProperties(CONTROLLER3)).thenReturn(properties);
+        when(persist.getControllerProperties(CONTROLLER4)).thenReturn(properties);
+
+        when(engine.createPolicyController(CONTROLLER3, properties)).thenReturn(controller3);
+        when(engine.createPolicyController(CONTROLLER4, properties)).thenReturn(controller4);
+
+        config3.setName(CONTROLLER3);
+        config3.setOperation(ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_CREATE);
+        config3.setDrools(drools3);
+
+        config4.setName(CONTROLLER4);
+        config4.setOperation(ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_UPDATE);
+        config4.setDrools(drools4);
+
+        pdpConfig.getControllers().add(config3);
+        pdpConfig.getControllers().add(config4);
+        pdpConfig.setEntity(PdpdConfiguration.CONFIG_ENTITY_CONTROLLER);
+
+        pdpConfigJson = encoder.toJson(pdpConfig);
+
+        mgr = new PolicyEngineManagerImpl();
+    }
+
+    @Test
+    public void testFactory() {
+        mgr = new PolicyEngineManager();
+
+        assertNotNull(mgr.getEngineProviders());
+        assertNotNull(mgr.getControllerProviders());
+        assertNotNull(mgr.getTopicEndpointManager());
+        assertNotNull(mgr.getServletFactory());
+        assertNotNull(mgr.getControllerFactory());
+        assertNotNull(mgr.makeShutdownThread());
+        assertNotNull(mgr.getProtocolCoder());
+        assertNotNull(mgr.getPersistenceManager());
+        assertNotNull(mgr.getPolicyEngine());
+    }
+
+    @Test
+    public void testBoot() throws Exception {
+        String[] args = {"boot-a", "boot-b"};
+
+        // arrange for first provider to throw exceptions
+        when(prov1.beforeBoot(mgr, args)).thenThrow(new RuntimeException(EXPECTED));
+        when(prov1.afterBoot(mgr)).thenThrow(new RuntimeException(EXPECTED));
+
+        mgr.boot(args);
+
+        verify(prov1).beforeBoot(mgr, args);
+        verify(prov2).beforeBoot(mgr, args);
+
+        assertTrue(globalInitArgs == args);
+
+        verify(prov1).afterBoot(mgr);
+        verify(prov2).afterBoot(mgr);
+
+        // global init throws exception - still calls afterBoot
+        setUp();
+        mgr = new PolicyEngineManagerImpl() {
+            @Override
+            protected void globalInitContainer(String[] cliArgs) {
+                throw new RuntimeException(EXPECTED);
+            }
+        };
+        mgr.boot(args);
+        verify(prov2).afterBoot(mgr);
+
+        // other tests
+        checkBeforeAfter(
+            (prov, flag) -> when(prov.beforeBoot(mgr, args)).thenReturn(flag),
+            (prov, flag) -> when(prov.afterBoot(mgr)).thenReturn(flag),
+            () -> mgr.boot(args),
+            prov -> verify(prov).beforeBoot(mgr, args),
+            () -> assertTrue(globalInitArgs == args),
+            prov -> verify(prov).afterBoot(mgr));
+    }
+
+    @Test
+    public void testSetEnvironment_testGetEnvironment_testGetEnvironmentProperty_setEnvironmentProperty() {
+        Properties props1 = new Properties();
+        props1.put("prop1-a", "value1-a");
+        props1.put("prop1-b", "value1-b");
+
+        mgr.setEnvironment(props1);
+
+        Properties env = mgr.getEnvironment();
+        assertEquals(props1, env);
+
+        // add more properties
+        Properties props2 = new Properties();
+        String propKey = "prop2-a";
+        props2.put(propKey, "value2-a");
+        props2.put("prop2-b", "value2-b");
+
+        mgr.setEnvironment(props2);
+
+        assertTrue(mgr.getEnvironment() == env);
+
+        // new env should have a union of the properties
+        props1.putAll(props2);
+        assertEquals(props1, env);
+
+        assertEquals("value2-a", mgr.getEnvironmentProperty(propKey));
+
+        String newValue = "new-value";
+        mgr.setEnvironmentProperty(propKey, newValue);
+        assertEquals(newValue, mgr.getEnvironmentProperty(propKey));
+
+        props1.setProperty(propKey, newValue);
+        assertEquals(props1, env);
+
+        assertNotNull(mgr.getEnvironmentProperty("JAVA_HOME"));
+        assertNull(mgr.getEnvironmentProperty("unknown-env-property"));
+    }
+
+    @Test
+    public void testDefaultTelemetryConfig() {
+        Properties config = mgr.defaultTelemetryConfig();
+        assertNotNull(config);
+        assertFalse(config.isEmpty());
+    }
+
+    @Test
+    public void testConfigureProperties() throws Exception {
+        // arrange for first provider to throw exceptions
+        when(prov1.beforeConfigure(mgr, properties)).thenThrow(new RuntimeException(EXPECTED));
+        when(prov1.afterConfigure(mgr)).thenThrow(new RuntimeException(EXPECTED));
+
+        mgr.configure(properties);
+
+        verify(prov1).beforeConfigure(mgr, properties);
+        verify(prov2).beforeConfigure(mgr, properties);
+
+        assertTrue(mgr.getProperties() == properties);
+
+        assertEquals(sources, mgr.getSources());
+        assertEquals(sinks, mgr.getSinks());
+        assertEquals(servers, mgr.getHttpServers());
+
+        verify(source1).register(mgr);
+        verify(source2).register(mgr);
+
+        verify(prov1).afterConfigure(mgr);
+        verify(prov2).afterConfigure(mgr);
+
+        // middle stuff throws exception - still calls afterXxx
+        setUp();
+        when(endpoint.addTopicSources(properties)).thenThrow(new IllegalArgumentException(EXPECTED));
+        when(endpoint.addTopicSinks(properties)).thenThrow(new IllegalArgumentException(EXPECTED));
+        when(serverFactory.build(properties)).thenThrow(new IllegalArgumentException(EXPECTED));
+        mgr.configure(properties);
+        verify(prov2).afterConfigure(mgr);
+
+        // null properties - nothing should be invoked
+        setUp();
+        Properties nullProps = null;
+        assertThrows(IllegalArgumentException.class, () -> mgr.configure(nullProps));
+        verify(prov1, never()).beforeConfigure(mgr, properties);
+        verify(prov1, never()).afterConfigure(mgr);
+
+        // other tests
+        checkBeforeAfter(
+            (prov, flag) -> when(prov.beforeConfigure(mgr, properties)).thenReturn(flag),
+            (prov, flag) -> when(prov.afterConfigure(mgr)).thenReturn(flag),
+            () -> mgr.configure(properties),
+            prov -> verify(prov).beforeConfigure(mgr, properties),
+            () -> assertTrue(mgr.getProperties() == properties),
+            prov -> verify(prov).afterConfigure(mgr));
+    }
+
+    @Test
+    public void testConfigurePdpdConfiguration() throws Exception {
+        mgr.configure(properties);
+        assertTrue(mgr.configure(pdpConfig));
+
+        verify(controllerFactory).patch(controller3, drools3);
+        verify(controllerFactory).patch(controller4, drools4);
+
+        // invalid params
+        PdpdConfiguration nullConfig = null;
+        assertThrows(IllegalArgumentException.class, () -> mgr.configure(nullConfig));
+
+        pdpConfig.setEntity("unknown-entity");
+        assertThrows(IllegalArgumentException.class, () -> mgr.configure(pdpConfig));
+
+        // source list of size 1
+        setUp();
+        when(endpoint.addTopicSources(any())).thenReturn(Arrays.asList(source1));
+        mgr.configure(properties);
+        assertTrue(mgr.configure(pdpConfig));
+
+        verify(controllerFactory).patch(controller3, drools3);
+        verify(controllerFactory).patch(controller4, drools4);
+    }
+
+    @Test
+    public void testCreatePolicyController() throws Exception {
+        assertEquals(controller, mgr.createPolicyController(MY_NAME, properties));
+
+        verify(contProv1).beforeCreate(MY_NAME, properties);
+        verify(contProv2).beforeCreate(MY_NAME, properties);
+        verify(controller, never()).lock();
+        verify(contProv1).afterCreate(controller);
+        verify(contProv2).afterCreate(controller);
+
+        // first provider throws exceptions - same result
+        setUp();
+        when(contProv1.beforeCreate(MY_NAME, properties)).thenThrow(new RuntimeException(EXPECTED));
+        when(contProv1.afterCreate(controller)).thenThrow(new RuntimeException(EXPECTED));
+        assertEquals(controller, mgr.createPolicyController(MY_NAME, properties));
+
+        verify(contProv1).beforeCreate(MY_NAME, properties);
+        verify(contProv2).beforeCreate(MY_NAME, properties);
+        verify(controller, never()).lock();
+        verify(contProv1).afterCreate(controller);
+        verify(contProv2).afterCreate(controller);
+
+        // locked - same result, but engine locked
+        setUp();
+        mgr.lock();
+        assertEquals(controller, mgr.createPolicyController(MY_NAME, properties));
+        verify(contProv1).beforeCreate(MY_NAME, properties);
+        verify(controller, times(2)).lock();
+        verify(contProv2).afterCreate(controller);
+
+        // empty name in properties - same result
+        setUp();
+        properties.setProperty(DroolsProperties.PROPERTY_CONTROLLER_NAME, "");
+        assertEquals(controller, mgr.createPolicyController(MY_NAME, properties));
+        verify(contProv1).beforeCreate(MY_NAME, properties);
+
+        // matching name in properties - same result
+        setUp();
+        properties.setProperty(DroolsProperties.PROPERTY_CONTROLLER_NAME, MY_NAME);
+        assertEquals(controller, mgr.createPolicyController(MY_NAME, properties));
+        verify(contProv1).beforeCreate(MY_NAME, properties);
+
+        // mismatching name in properties - nothing should happen besides exception
+        setUp();
+        properties.setProperty(DroolsProperties.PROPERTY_CONTROLLER_NAME, "mistmatched-name");
+        assertThrows(IllegalStateException.class, () -> mgr.createPolicyController(MY_NAME, properties));
+        verify(contProv1, never()).beforeCreate(MY_NAME, properties);
+
+        // first provider generates controller - stops after first provider
+        setUp();
+        when(contProv1.beforeCreate(MY_NAME, properties)).thenReturn(controller2);
+        assertEquals(controller2, mgr.createPolicyController(MY_NAME, properties));
+        verify(contProv1).beforeCreate(MY_NAME, properties);
+        verify(contProv2, never()).beforeCreate(MY_NAME, properties);
+        verify(controller, never()).lock();
+        verify(contProv1, never()).afterCreate(controller);
+        verify(contProv2, never()).afterCreate(controller);
+
+        // first provider returns true - stops after first provider afterXxx
+        setUp();
+        when(contProv1.afterCreate(controller)).thenReturn(true);
+        assertEquals(controller, mgr.createPolicyController(MY_NAME, properties));
+        verify(contProv1).beforeCreate(MY_NAME, properties);
+        verify(contProv2).beforeCreate(MY_NAME, properties);
+        verify(contProv1).afterCreate(controller);
+        verify(contProv2, never()).afterCreate(controller);
+    }
+
+    @Test
+    public void testUpdatePolicyControllers() throws Exception {
+        assertEquals(Arrays.asList(controller3, controller4), mgr.updatePolicyControllers(pdpConfig.getControllers()));
+
+        // controller3 was CREATE
+        verify(controllerFactory).patch(controller3, drools3);
+        verify(controller3, never()).lock();
+        verify(controller3, never()).unlock();
+
+        // controller4 was UPDATE
+        verify(controllerFactory).patch(controller4, drools4);
+        verify(controller4, never()).lock();
+        verify(controller4).unlock();
+
+        // invalid args
+        assertTrue(mgr.updatePolicyControllers(null).isEmpty());
+        assertTrue(mgr.updatePolicyControllers(Collections.emptyList()).isEmpty());
+
+        // force exception in the first controller with invalid operation
+        setUp();
+        config3.setOperation("unknown-operation");
+        assertEquals(Arrays.asList(controller4), mgr.updatePolicyControllers(pdpConfig.getControllers()));
+
+        // controller3 should NOT have been done
+        verify(controllerFactory, never()).patch(controller3, drools3);
+
+        // controller4 should still be done
+        verify(controllerFactory).patch(controller4, drools4);
+        verify(controller4, never()).lock();
+        verify(controller4).unlock();
+    }
+
+    @Test
+    public void testUpdatePolicyController() throws Exception {
+        assertEquals(controller3, mgr.updatePolicyController(config3));
+        verify(engine).createPolicyController(CONTROLLER3, properties);
+
+        // invalid parameters
+        assertThrows(IllegalArgumentException.class, () -> mgr.updatePolicyController(null));
+
+        // invalid name
+        setUp();
+        config3.setName(null);
+        assertThrows(IllegalArgumentException.class, () -> mgr.updatePolicyController(config3));
+
+        config3.setName("");
+        assertThrows(IllegalArgumentException.class, () -> mgr.updatePolicyController(config3));
+
+        // invalid operation
+        setUp();
+        config3.setOperation(null);
+        assertThrows(IllegalArgumentException.class, () -> mgr.updatePolicyController(config3));
+
+        config3.setOperation("");
+        assertThrows(IllegalArgumentException.class, () -> mgr.updatePolicyController(config3));
+
+        config3.setOperation(ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_LOCK);
+        assertThrows(IllegalArgumentException.class, () -> mgr.updatePolicyController(config3));
+
+        config3.setOperation(ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_UNLOCK);
+        assertThrows(IllegalArgumentException.class, () -> mgr.updatePolicyController(config3));
+
+        // exception from get() - should create controller
+        setUp();
+        when(controllerFactory.get(CONTROLLER3)).thenThrow(new IllegalArgumentException(EXPECTED));
+        assertEquals(controller3, mgr.updatePolicyController(config3));
+        verify(engine).createPolicyController(CONTROLLER3, properties);
+
+        // null properties
+        setUp();
+        when(persist.getControllerProperties(CONTROLLER3)).thenReturn(null);
+        assertThrows(IllegalArgumentException.class, () -> mgr.updatePolicyController(config3));
+
+        // throw linkage error
+        setUp();
+        when(persist.getControllerProperties(CONTROLLER3)).thenThrow(new LinkageError(EXPECTED));
+        assertThrows(IllegalStateException.class, () -> mgr.updatePolicyController(config3));
+
+        /*
+         * For remaining tests, the factory will return the controller instead of creating
+         * one.
+         */
+        setUp();
+        when(controllerFactory.get(CONTROLLER3)).thenReturn(controller3);
+
+        assertEquals(controller3, mgr.updatePolicyController(config3));
+
+        // should NOT have created a new controller
+        verify(engine, never()).createPolicyController(any(), any());
+
+        int countPatch = 0;
+        int countLock = 0;
+        int countUnlock = 0;
+
+        // check different operations
+
+        // CREATE only invokes patch() (note: mgr.update() has already been called)
+        verify(controllerFactory, times(++countPatch)).patch(controller3, drools3);
+        verify(controller3, times(countLock)).lock();
+        verify(controller3, times(countUnlock)).unlock();
+
+        // UPDATE invokes unlock() and patch()
+        config3.setOperation(ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_UPDATE);
+        assertEquals(controller3, mgr.updatePolicyController(config3));
+        verify(controllerFactory, times(++countPatch)).patch(controller3, drools3);
+        verify(controller3, times(countLock)).lock();
+        verify(controller3, times(++countUnlock)).unlock();
+
+        // LOCK invokes lock()
+        config3.setOperation(ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_LOCK);
+        assertEquals(controller3, mgr.updatePolicyController(config3));
+        verify(controllerFactory, times(countPatch)).patch(controller3, drools3);
+        verify(controller3, times(++countLock)).lock();
+        verify(controller3, times(countUnlock)).unlock();
+
+        // UNLOCK invokes unlock()
+        config3.setOperation(ControllerConfiguration.CONFIG_CONTROLLER_OPERATION_UNLOCK);
+        assertEquals(controller3, mgr.updatePolicyController(config3));
+        verify(controllerFactory, times(countPatch)).patch(controller3, drools3);
+        verify(controller3, times(countLock)).lock();
+        verify(controller3, times(++countUnlock)).unlock();
+
+        // invalid operation
+        config3.setOperation("invalid-operation");
+        assertThrows(IllegalArgumentException.class, () -> mgr.updatePolicyController(config3));
+    }
+
+    @Test
+    public void testStart() throws Throwable {
+        // normal success case
+        testStart(true, () -> {
+            // arrange for first provider, server, source, and sink to throw exceptions
+            when(prov1.beforeStart(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(prov1.afterStart(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(server1.waitedStart(anyLong())).thenThrow(new RuntimeException(EXPECTED));
+            when(source1.start()).thenThrow(new RuntimeException(EXPECTED));
+            when(sink1.start()).thenThrow(new RuntimeException(EXPECTED));
+        });
+
+        // servlet wait fails - still does everything
+        testStart(false, () -> when(server1.waitedStart(anyLong())).thenReturn(false));
+
+        // topic source fails to start - still does everything
+        testStart(false, () -> when(source1.start()).thenReturn(false));
+
+        // topic sink fails to start - still does everything
+        testStart(false, () -> when(sink1.start()).thenReturn(false));
+
+        // controller fails to start - still does everything
+        testStart(false, () -> when(controller.start()).thenReturn(false));
+
+        // controller throws an exception - still does everything
+        testStart(false, () -> when(controller.start()).thenThrow(new RuntimeException(EXPECTED)));
+
+        // endpoint manager fails to start - still does everything
+        testStart(false, () -> when(endpoint.start()).thenReturn(false));
+
+        // endpoint manager throws an exception - still does everything AND succeeds
+        testStart(true, () -> when(endpoint.start()).thenThrow(new IllegalStateException(EXPECTED)));
+
+        // locked - nothing other than beforeXxx methods should be invoked
+        setUp();
+        mgr.configure(properties);
+        mgr.lock();
+        assertThrows(IllegalStateException.class, () -> mgr.start());
+        verify(prov2).beforeStart(mgr);
+        verify(server2, never()).waitedStart(anyLong());
+        verify(source2, never()).start();
+        verify(sink1, never()).start();
+        verify(controller, never()).start();
+        verify(endpoint, never()).start();
+        assertFalse(jmxStarted);
+        verify(prov1, never()).afterStart(mgr);
+
+        // other tests
+        checkBeforeAfter(
+            (prov, flag) -> when(prov.beforeStart(mgr)).thenReturn(flag),
+            (prov, flag) -> when(prov.afterStart(mgr)).thenReturn(flag),
+            () -> {
+                mgr.configure(properties);
+                assertTrue(mgr.start());
+            },
+            prov -> verify(prov).beforeStart(mgr),
+            () -> assertTrue(jmxStarted),
+            prov -> verify(prov).afterStart(mgr));
+    }
+
+    /**
+     * Tests the start() method, after setting some option.
+     *
+     * @param expectedResult what start() is expected to return
+     * @param setOption function that sets an option
+     * @throws Throwable if an error occurs during setup
+     */
+    private void testStart(boolean expectedResult, RunnableWithEx setOption) throws Throwable {
+        setUp();
+        setOption.run();
+
+        mgr.configure(properties);
+        assertEquals(expectedResult, mgr.start());
+
+        verify(prov1).beforeStart(mgr);
+        verify(prov2).beforeStart(mgr);
+
+        verify(server1).waitedStart(anyLong());
+        verify(server2).waitedStart(anyLong());
+
+        verify(source1).start();
+        verify(source2).start();
+
+        verify(sink1).start();
+        verify(sink2).start();
+
+        verify(controller).start();
+        verify(controller2).start();
+
+        verify(endpoint).start();
+
+        assertTrue(jmxStarted);
+
+        verify(prov1).afterStart(mgr);
+        verify(prov2).afterStart(mgr);
+    }
+
+    @Test
+    public void testStop() throws Throwable {
+        // normal success case
+        testStop(true, () -> {
+            // arrange for first provider, server, source, and sink to throw exceptions
+            when(prov1.beforeStop(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(prov1.afterStop(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(server1.stop()).thenThrow(new RuntimeException(EXPECTED));
+            when(source1.stop()).thenThrow(new RuntimeException(EXPECTED));
+            when(sink1.stop()).thenThrow(new RuntimeException(EXPECTED));
+        });
+
+        // not alive - shouldn't run anything besides beforeStop()
+        setUp();
+        mgr.configure(properties);
+        assertTrue(mgr.stop());
+        verify(prov1).beforeStop(mgr);
+        verify(prov2).beforeStop(mgr);
+        verify(controller, never()).stop();
+        verify(source1, never()).stop();
+        verify(sink1, never()).stop();
+        verify(endpoint, never()).stop();
+        verify(server1, never()).stop();
+        verify(prov1, never()).afterStop(mgr);
+        verify(prov2, never()).afterStop(mgr);
+
+        // controller fails to stop - still does everything
+        testStop(false, () -> when(controller.stop()).thenReturn(false));
+
+        // controller throws an exception - still does everything
+        testStop(false, () -> when(controller.stop()).thenThrow(new RuntimeException(EXPECTED)));
+
+        // topic source fails to stop - still does everything
+        testStop(false, () -> when(source1.stop()).thenReturn(false));
+
+        // topic sink fails to stop - still does everything
+        testStop(false, () -> when(sink1.stop()).thenReturn(false));
+
+        // endpoint manager fails to stop - still does everything
+        testStop(false, () -> when(endpoint.stop()).thenReturn(false));
+
+        // servlet fails to stop - still does everything
+        testStop(false, () -> when(server1.stop()).thenReturn(false));
+
+        // other tests
+        checkBeforeAfter(
+            (prov, flag) -> when(prov.beforeStop(mgr)).thenReturn(flag),
+            (prov, flag) -> when(prov.afterStop(mgr)).thenReturn(flag),
+            () -> {
+                mgr.configure(properties);
+                mgr.start();
+                assertTrue(mgr.stop());
+            },
+            prov -> verify(prov).beforeStop(mgr),
+            () -> verify(endpoint).stop(),
+            prov -> verify(prov).afterStop(mgr));
+    }
+
+    /**
+     * Tests the stop() method, after setting some option.
+     *
+     * @param expectedResult what stop() is expected to return
+     * @param setOption function that sets an option
+     * @throws Throwable if an error occurs during setup
+     */
+    private void testStop(boolean expectedResult, RunnableWithEx setOption) throws Throwable {
+        setUp();
+        setOption.run();
+
+        mgr.configure(properties);
+        mgr.start();
+        assertEquals(expectedResult, mgr.stop());
+
+        verify(prov1).beforeStop(mgr);
+        verify(prov2).beforeStop(mgr);
+
+        verify(controller).stop();
+        verify(controller2).stop();
+
+        verify(source1).stop();
+        verify(source2).stop();
+
+        verify(sink1).stop();
+        verify(sink2).stop();
+
+        verify(endpoint).stop();
+
+        verify(server1).stop();
+        verify(server2).stop();
+
+        verify(prov1).afterStop(mgr);
+        verify(prov2).afterStop(mgr);
+    }
+
+    @Test
+    public void testShutdown() throws Throwable {
+        // normal success case
+        testShutdown(() -> {
+            // arrange for first provider, source, and sink to throw exceptions
+            when(prov1.beforeShutdown(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(prov1.afterShutdown(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            doThrow(new RuntimeException(EXPECTED)).when(source1).shutdown();
+            doThrow(new RuntimeException(EXPECTED)).when(sink1).shutdown();
+        });
+
+        assertNotNull(shutdownThread);
+        assertTrue(threadStarted);
+        assertTrue(threadInterrupted);
+
+        // other tests
+        checkBeforeAfter(
+            (prov, flag) -> when(prov.beforeShutdown(mgr)).thenReturn(flag),
+            (prov, flag) -> when(prov.afterShutdown(mgr)).thenReturn(flag),
+            () -> {
+                mgr.configure(properties);
+                mgr.start();
+                mgr.shutdown();
+            },
+            prov -> verify(prov).beforeShutdown(mgr),
+            () -> assertTrue(jmxStopped),
+            prov -> verify(prov).afterShutdown(mgr));
+    }
+
+    /**
+     * Tests the shutdown() method, after setting some option.
+     *
+     * @param setOption function that sets an option
+     * @throws Throwable if an error occurs during setup
+     */
+    private void testShutdown(RunnableWithEx setOption) throws Throwable {
+        setUp();
+        setOption.run();
+
+        mgr.configure(properties);
+        mgr.start();
+        mgr.shutdown();
+
+        verify(prov1).beforeShutdown(mgr);
+        verify(prov2).beforeShutdown(mgr);
+
+        verify(source1).shutdown();
+        verify(source2).shutdown();
+
+        verify(sink1).shutdown();
+        verify(sink2).shutdown();
+
+        verify(controllerFactory).shutdown();
+        verify(endpoint).shutdown();
+        verify(serverFactory).destroy();
+
+        assertTrue(jmxStopped);
+
+        verify(prov1).afterShutdown(mgr);
+        verify(prov2).afterShutdown(mgr);
+    }
+
+    @Test
+    public void testShutdownThreadRun() throws Throwable {
+        // arrange for first server to throw exceptions
+        testShutdownThreadRun(() -> doThrow(new RuntimeException(EXPECTED)).when(server1).shutdown());
+
+        // sleep throws an exception
+        testShutdownThreadRun(() -> shouldInterrupt = true);
+    }
+
+    /**
+     * Tests the ShutdownThread.run() method, after setting some option.
+     *
+     * @param setOption function that sets an option
+     * @throws Throwable if an error occurs during setup
+     */
+    private void testShutdownThreadRun(RunnableWithEx setOption) throws Throwable {
+        setUp();
+        setOption.run();
+
+        mgr.configure(properties);
+        mgr.start();
+        mgr.shutdown();
+
+        assertNotNull(shutdownThread);
+
+        shutdownThread.run();
+
+        assertTrue(threadSleepMs >= 0);
+        assertEquals(0, threadExitCode);
+
+        verify(server1).shutdown();
+        verify(server2).shutdown();
+    }
+
+    @Test
+    public void testIsAlive() {
+        mgr.configure(properties);
+        assertFalse(mgr.isAlive());
+
+        mgr.start();
+        assertTrue(mgr.isAlive());
+
+        mgr.stop();
+        assertFalse(mgr.isAlive());
+    }
+
+    @Test
+    public void testLock() throws Throwable {
+        // normal success case
+        testLock(true, () -> {
+            // arrange for first provider to throw exceptions
+            when(prov1.beforeLock(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(prov1.afterLock(mgr)).thenThrow(new RuntimeException(EXPECTED));
+        });
+
+        // already locked - shouldn't run anything besides beforeLock()
+        setUp();
+        mgr.configure(properties);
+        mgr.lock();
+        assertTrue(mgr.lock());
+        verify(prov1, times(2)).beforeLock(mgr);
+        verify(prov2, times(2)).beforeLock(mgr);
+        verify(controller).lock();
+        verify(controller2).lock();
+        verify(endpoint).lock();
+        verify(prov1).afterLock(mgr);
+        verify(prov2).afterLock(mgr);
+
+        // controller fails to lock - still does everything
+        testLock(false, () -> when(controller.lock()).thenReturn(false));
+
+        // controller throws an exception - still does everything
+        testLock(false, () -> when(controller.lock()).thenThrow(new RuntimeException(EXPECTED)));
+
+        // endpoint manager fails to lock - still does everything
+        testLock(false, () -> when(endpoint.lock()).thenReturn(false));
+
+        // other tests
+        checkBeforeAfter(
+            (prov, flag) -> when(prov.beforeLock(mgr)).thenReturn(flag),
+            (prov, flag) -> when(prov.afterLock(mgr)).thenReturn(flag),
+            () -> {
+                mgr.configure(properties);
+                mgr.start();
+                assertTrue(mgr.lock());
+            },
+            prov -> verify(prov).beforeLock(mgr),
+            () -> verify(endpoint).lock(),
+            prov -> verify(prov).afterLock(mgr));
+    }
+
+    /**
+     * Tests the lock() method, after setting some option.
+     *
+     * @param expectedResult what lock() is expected to return
+     * @param setOption function that sets an option
+     * @throws Throwable if an error occurs during setup
+     */
+    private void testLock(boolean expectedResult, RunnableWithEx setOption) throws Throwable {
+        setUp();
+        setOption.run();
+
+        mgr.configure(properties);
+        assertEquals(expectedResult, mgr.lock());
+
+        verify(prov1).beforeLock(mgr);
+        verify(prov2).beforeLock(mgr);
+
+        verify(controller).lock();
+        verify(controller2).lock();
+
+        verify(endpoint).lock();
+
+        verify(prov1).afterLock(mgr);
+        verify(prov2).afterLock(mgr);
+    }
+
+    @Test
+    public void testUnlock() throws Throwable {
+        // normal success case
+        testUnlock(true, () -> {
+            // arrange for first provider to throw exceptions
+            when(prov1.beforeUnlock(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(prov1.afterUnlock(mgr)).thenThrow(new RuntimeException(EXPECTED));
+        });
+
+        // not locked - shouldn't run anything besides beforeUnlock()
+        setUp();
+        mgr.configure(properties);
+        assertTrue(mgr.unlock());
+        verify(prov1).beforeUnlock(mgr);
+        verify(prov2).beforeUnlock(mgr);
+        verify(controller, never()).unlock();
+        verify(controller2, never()).unlock();
+        verify(endpoint, never()).unlock();
+        verify(prov1, never()).afterUnlock(mgr);
+        verify(prov2, never()).afterUnlock(mgr);
+
+        // controller fails to unlock - still does everything
+        testUnlock(false, () -> when(controller.unlock()).thenReturn(false));
+
+        // controller throws an exception - still does everything
+        testUnlock(false, () -> when(controller.unlock()).thenThrow(new RuntimeException(EXPECTED)));
+
+        // endpoint manager fails to unlock - still does everything
+        testUnlock(false, () -> when(endpoint.unlock()).thenReturn(false));
+
+        // other tests
+        checkBeforeAfter(
+            (prov, flag) -> when(prov.beforeUnlock(mgr)).thenReturn(flag),
+            (prov, flag) -> when(prov.afterUnlock(mgr)).thenReturn(flag),
+            () -> {
+                mgr.configure(properties);
+                mgr.lock();
+                assertTrue(mgr.unlock());
+            },
+            prov -> verify(prov).beforeUnlock(mgr),
+            () -> verify(endpoint).unlock(),
+            prov -> verify(prov).afterUnlock(mgr));
+    }
+
+    /**
+     * Tests the unlock() method, after setting some option.
+     *
+     * @param expectedResult what unlock() is expected to return
+     * @param setOption function that sets an option
+     * @throws Throwable if an error occurs during setup
+     */
+    private void testUnlock(boolean expectedResult, RunnableWithEx setOption) throws Throwable {
+        setUp();
+        setOption.run();
+
+        mgr.configure(properties);
+        mgr.lock();
+        assertEquals(expectedResult, mgr.unlock());
+
+        verify(prov1).beforeUnlock(mgr);
+        verify(prov2).beforeUnlock(mgr);
+
+        verify(controller).unlock();
+        verify(controller2).unlock();
+
+        verify(endpoint).unlock();
+
+        verify(prov1).afterUnlock(mgr);
+        verify(prov2).afterUnlock(mgr);
+    }
+
+    @Test
+    public void testIsLocked() {
+        mgr.configure(properties);
+        assertFalse(mgr.isLocked());
+
+        mgr.lock();
+        assertTrue(mgr.isLocked());
+
+        mgr.unlock();
+        assertFalse(mgr.isLocked());
+    }
+
+    @Test
+    public void testRemovePolicyControllerString() {
+        mgr.removePolicyController(MY_NAME);
+
+        verify(controllerFactory).destroy(MY_NAME);
+    }
+
+    @Test
+    public void testRemovePolicyControllerPolicyController() {
+        mgr.removePolicyController(controller);
+
+        verify(controllerFactory).destroy(controller);
+    }
+
+    @Test
+    public void testGetPolicyControllers() {
+        assertEquals(controllers, mgr.getPolicyControllers());
+    }
+
+    @Test
+    public void testGetPolicyControllerIds() {
+        assertEquals(Arrays.asList(CONTROLLER1, CONTROLLER2), mgr.getPolicyControllerIds());
+    }
+
+    @Test
+    public void testGetProperties() {
+        properties.setProperty("prop-x", "value-x");
+        properties.setProperty("prop-y", "value-y");
+
+        mgr.configure(properties);
+        assertEquals(properties, mgr.getProperties());
+    }
+
+    @Test
+    public void testGetSources() {
+        mgr.configure(properties);
+        assertEquals(sources, mgr.getSources());
+    }
+
+    @Test
+    public void testGetSinks() {
+        mgr.configure(properties);
+        assertEquals(sinks, mgr.getSinks());
+    }
+
+    @Test
+    public void testGetHttpServers() {
+        mgr.configure(properties);
+        assertEquals(servers, mgr.getHttpServers());
+    }
+
+    @Test
+    public void testGetFeatures() {
+        assertEquals(Arrays.asList(FEATURE1, FEATURE2), mgr.getFeatures());
+    }
+
+    @Test
+    public void testGetFeatureProviders() {
+        assertEquals(providers, mgr.getFeatureProviders());
+    }
+
+    @Test
+    public void testGetFeatureProvider() {
+        assertEquals(prov1, mgr.getFeatureProvider(FEATURE1));
+        assertEquals(prov2, mgr.getFeatureProvider(FEATURE2));
+
+        // null feature
+        assertThrows(IllegalArgumentException.class, () -> mgr.getFeatureProvider(null));
+
+        // empty feature
+        assertThrows(IllegalArgumentException.class, () -> mgr.getFeatureProvider(""));
+
+        // unknown feature
+        assertThrows(IllegalArgumentException.class, () -> mgr.getFeatureProvider("unknown-feature"));
+    }
+
+    @Test
+    public void testOnTopicEvent() {
+        mgr.onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, pdpConfigJson);
+
+        verify(controllerFactory).patch(controller3, drools3);
+        verify(controllerFactory).patch(controller4, drools4);
+
+        // null json - no additional patches
+        mgr.onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, null);
+
+        verify(controllerFactory).patch(controller3, drools3);
+        verify(controllerFactory).patch(controller4, drools4);
+    }
+
+    @Test
+    public void testDeliverStringObject() throws Exception {
+        mgr.configure(properties);
+        mgr.start();
+
+        assertTrue(mgr.deliver(MY_TOPIC, MY_EVENT));
+
+        verify(sink1).send(MESSAGE);
+
+        // invalid parameters
+        String nullStr = null;
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(nullStr, MY_EVENT));
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver("", MY_EVENT));
+
+        Object nullObj = null;
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(MY_TOPIC, nullObj));
+
+        // locked
+        mgr.lock();
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(MY_TOPIC, MY_EVENT));
+        mgr.unlock();
+
+        // not running
+        mgr.stop();
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(MY_TOPIC, MY_EVENT));
+
+        // issues with topic
+        setUp();
+        mgr.configure(properties);
+        mgr.start();
+
+        // null sinks
+        when(endpoint.getTopicSinks(MY_TOPIC)).thenReturn(null);
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(MY_TOPIC, MY_EVENT));
+
+        // empty sinks
+        when(endpoint.getTopicSinks(MY_TOPIC)).thenReturn(Collections.emptyList());
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(MY_TOPIC, MY_EVENT));
+
+        // too many sinks
+        when(endpoint.getTopicSinks(MY_TOPIC)).thenReturn(sinks);
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(MY_TOPIC, MY_EVENT));
+    }
+
+    @Test
+    public void testDeliverStringStringObject() {
+        mgr.configure(properties);
+        mgr.start();
+
+        assertTrue(mgr.deliver(NOOP_STR, MY_TOPIC, MY_EVENT));
+
+        verify(sink1).send(MESSAGE);
+
+        // invalid parameters
+        String nullStr = null;
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(nullStr, MY_TOPIC, MY_EVENT));
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver("", MY_TOPIC, MY_EVENT));
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver("unknown-bus-type", MY_TOPIC, MY_EVENT));
+
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(NOOP_STR, nullStr, MY_EVENT));
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(NOOP_STR, "", MY_EVENT));
+
+        Object nullObj = null;
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(NOOP_STR, MY_TOPIC, nullObj));
+
+        // locked
+        mgr.lock();
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(NOOP_STR, MY_TOPIC, MY_EVENT));
+        mgr.unlock();
+
+        // not running
+        mgr.stop();
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(NOOP_STR, MY_TOPIC, MY_EVENT));
+    }
+
+    @Test
+    public void testDeliverCommInfrastructureStringObject() throws Exception {
+        mgr.configure(properties);
+        mgr.start();
+
+        assertTrue(mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT));
+
+        verify(controller, never()).deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT);
+
+        verify(coder).encode(MY_TOPIC, MY_EVENT);
+        verify(sink1).send(MESSAGE);
+
+        // invalid parameters
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(CommInfrastructure.NOOP, null, MY_EVENT));
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(CommInfrastructure.NOOP, "", MY_EVENT));
+
+        Object nullObj = null;
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, nullObj));
+
+        // locked
+        mgr.lock();
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT));
+        mgr.unlock();
+
+        // not started
+        mgr.stop();
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT));
+
+        // send() throws an exception
+        setUp();
+        mgr.configure(properties);
+        mgr.start();
+        when(sink1.send(any())).thenThrow(new ArithmeticException(EXPECTED));
+        assertThrows(ArithmeticException.class, () -> mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT));
+
+        /*
+         * For remaining tests, have the controller handle delivery.
+         */
+        setUp();
+        mgr.configure(properties);
+        mgr.start();
+        DroolsController drools = mock(DroolsController.class);
+        when(coder.getDroolsController(MY_TOPIC, MY_EVENT)).thenReturn(drools);
+        when(controllerFactory.get(drools)).thenReturn(controller);
+        when(controller.deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT)).thenReturn(true);
+
+        assertTrue(mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT));
+
+        verify(controller).deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT);
+
+        verify(coder, never()).encode(MY_TOPIC, MY_EVENT);
+        verify(sink1, never()).send(MESSAGE);
+
+        // controller throws exception, so should drop into regular handling
+        when(controller.deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT)).thenThrow(new RuntimeException(EXPECTED));
+
+        assertTrue(mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT));
+
+        // should have attempted this again
+        verify(controller, times(2)).deliver(CommInfrastructure.NOOP, MY_TOPIC, MY_EVENT);
+
+        // now these should have been called
+        verify(coder).encode(MY_TOPIC, MY_EVENT);
+        verify(sink1).send(MESSAGE);
+    }
+
+    @Test
+    public void testDeliverCommInfrastructureStringString() {
+        mgr.configure(properties);
+
+        // not started yet
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, MESSAGE));
+
+        // start it
+        mgr.start();
+
+        assertTrue(mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, MESSAGE));
+        verify(sink1).send(MESSAGE);
+        verify(sink2, never()).send(any());
+
+        // invalid parameters
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(CommInfrastructure.NOOP, null, MESSAGE));
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(CommInfrastructure.NOOP, "", MESSAGE));
+
+        String nullStr = null;
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, nullStr));
+        assertThrows(IllegalArgumentException.class, () -> mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, ""));
+
+        // unknown topic
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(CommInfrastructure.NOOP, "unknown-topic", MESSAGE));
+
+        // locked
+        mgr.lock();
+        assertThrows(IllegalStateException.class, () -> mgr.deliver(CommInfrastructure.NOOP, MY_TOPIC, MESSAGE));
+        mgr.unlock();
+    }
+
+    @Test
+    public void testActivate() throws Throwable {
+        // normal success case
+        testActivate(() -> {
+            // arrange for first provider and controller to throw exceptions
+            when(prov1.beforeActivate(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(prov1.afterActivate(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(controller.start()).thenThrow(new RuntimeException(EXPECTED));
+        });
+
+        // controller generates linkage error
+        testActivate(() -> when(controller.start()).thenThrow(new LinkageError(EXPECTED)));
+
+        // other tests
+        checkBeforeAfter(
+            (prov, flag) -> when(prov.beforeActivate(mgr)).thenReturn(flag),
+            (prov, flag) -> when(prov.afterActivate(mgr)).thenReturn(flag),
+            () -> {
+                mgr.configure(properties);
+                mgr.lock();
+                mgr.activate();
+            },
+            prov -> verify(prov).beforeActivate(mgr),
+            () -> assertFalse(mgr.isLocked()),
+            prov -> verify(prov).afterActivate(mgr));
+    }
+
+    /**
+     * Tests the activate() method, after setting some option.
+     *
+     * @param setOption function that sets an option
+     * @throws Throwable if an error occurs during setup
+     */
+    private void testActivate(RunnableWithEx setOption) throws Throwable {
+        setUp();
+        setOption.run();
+
+        mgr.configure(properties);
+        mgr.lock();
+        mgr.activate();
+
+        verify(prov1).beforeActivate(mgr);
+        verify(prov2).beforeActivate(mgr);
+
+        // unlocked by activate() AND by unlock() (which is invoked by activate())
+        verify(controller, times(2)).unlock();
+        verify(controller2, times(2)).unlock();
+
+        verify(controller).start();
+        verify(controller2).start();
+
+        assertFalse(mgr.isLocked());
+
+        verify(prov1).afterActivate(mgr);
+        verify(prov2).afterActivate(mgr);
+    }
+
+    @Test
+    public void testDeactivate() throws Throwable {
+        // normal success case
+        testDeactivate(() -> {
+            // arrange for first provider and controller to throw exceptions
+            when(prov1.beforeDeactivate(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(prov1.afterDeactivate(mgr)).thenThrow(new RuntimeException(EXPECTED));
+            when(controller.stop()).thenThrow(new RuntimeException(EXPECTED));
+        });
+
+        // controller generates linkage error
+        testDeactivate(() -> when(controller.stop()).thenThrow(new LinkageError(EXPECTED)));
+
+        // other tests
+        checkBeforeAfter(
+            (prov, flag) -> when(prov.beforeDeactivate(mgr)).thenReturn(flag),
+            (prov, flag) -> when(prov.afterDeactivate(mgr)).thenReturn(flag),
+            () -> {
+                mgr.configure(properties);
+                mgr.deactivate();
+            },
+            prov -> verify(prov).beforeDeactivate(mgr),
+            () -> assertTrue(mgr.isLocked()),
+            prov -> verify(prov).afterDeactivate(mgr));
+    }
+
+    /**
+     * Tests the deactivate() method, after setting some option.
+     *
+     * @param setOption function that sets an option
+     * @throws Throwable if an error occurs during setup
+     */
+    private void testDeactivate(RunnableWithEx setOption) throws Throwable {
+        setUp();
+        setOption.run();
+
+        mgr.configure(properties);
+        mgr.deactivate();
+
+        verify(prov1).beforeDeactivate(mgr);
+        verify(prov2).beforeDeactivate(mgr);
+
+        verify(controller).lock();
+        verify(controller2).lock();
+
+        verify(controller).stop();
+        verify(controller2).stop();
+
+        assertTrue(mgr.isLocked());
+
+        verify(prov1).afterDeactivate(mgr);
+        verify(prov2).afterDeactivate(mgr);
+    }
+
+    @Test
+    public void testControllerConfig() throws Exception {
+        mgr.configure(properties);
+        assertTrue(mgr.configure(pdpConfig));
+
+        verify(controllerFactory).patch(controller3, drools3);
+        verify(controllerFactory).patch(controller4, drools4);
+
+        // empty controllers
+        pdpConfig.getControllers().clear();
+        assertFalse(mgr.configure(pdpConfig));
+
+        // null controllers
+        pdpConfig.setControllers(null);
+        assertFalse(mgr.configure(pdpConfig));
+
+        // arrange for controller3 to fail
+        setUp();
+        config3.setOperation("fail-3");
+        assertFalse(mgr.configure(pdpConfig));
+
+        verify(controllerFactory, never()).patch(controller3, drools3);
+        verify(controllerFactory).patch(controller4, drools4);
+
+        // arrange for both controllers to fail
+        setUp();
+        config3.setOperation("fail-3");
+        config4.setOperation("fail-4");
+        assertFalse(mgr.configure(pdpConfig));
+    }
+
+    @Test
+    public void testToString() {
+        assertTrue(mgr.toString().startsWith("PolicyEngineManager ["));
+    }
+
+    /**
+     * Performs an operation that has a beforeXxx method and an afterXxx method. Tries
+     * combinations where beforeXxx and afterXxx return {@code true} and {@code false}.
+     *
+     * @param setBefore function to set the return value of a provider's beforeXxx method
+     * @param setAfter function to set the return value of a provider's afterXxx method
+     * @param action invokes the operation
+     * @param verifyBefore verifies that a provider's beforeXxx method was invoked
+     * @param verifyMiddle verifies that the action occurring between the beforeXxx loop
+     *        and the afterXxx loop was invoked
+     * @param verifyAfter verifies that a provider's afterXxx method was invoked
+     * @throws Exception if an error occurs while calling {@link #setUp()}
+     */
+    private void checkBeforeAfter(BiConsumer<PolicyEngineFeatureAPI, Boolean> setBefore,
+                    BiConsumer<PolicyEngineFeatureAPI, Boolean> setAfter, Runnable action,
+                    Consumer<PolicyEngineFeatureAPI> verifyBefore, Runnable verifyMiddle,
+                    Consumer<PolicyEngineFeatureAPI> verifyAfter) throws Exception {
+
+        checkBeforeAfter_FalseFalse(setBefore, setAfter, action, verifyBefore, verifyMiddle, verifyAfter);
+        checkBeforeAfter_FalseTrue(setBefore, setAfter, action, verifyBefore, verifyMiddle, verifyAfter);
+        checkBeforeAfter_TrueFalse(setBefore, setAfter, action, verifyBefore, verifyMiddle, verifyAfter);
+
+        // don't need to test true-true, as it's behavior is a subset of true-false
+    }
+
+    /**
+     * Performs an operation that has a beforeXxx method and an afterXxx method. Tries the
+     * case where both the beforeXxx and afterXxx methods return {@code false}.
+     *
+     * @param setBefore function to set the return value of a provider's beforeXxx method
+     * @param setAfter function to set the return value of a provider's afterXxx method
+     * @param action invokes the operation
+     * @param verifyBefore verifies that a provider's beforeXxx method was invoked
+     * @param verifyMiddle verifies that the action occurring between the beforeXxx loop
+     *        and the afterXxx loop was invoked
+     * @param verifyAfter verifies that a provider's afterXxx method was invoked
+     * @throws Exception if an error occurs while calling {@link #setUp()}
+     */
+    private void checkBeforeAfter_FalseFalse(BiConsumer<PolicyEngineFeatureAPI, Boolean> setBefore,
+                    BiConsumer<PolicyEngineFeatureAPI, Boolean> setAfter, Runnable action,
+                    Consumer<PolicyEngineFeatureAPI> verifyBefore, Runnable verifyMiddle,
+                    Consumer<PolicyEngineFeatureAPI> verifyAfter) throws Exception {
+
+        setUp();
+
+        // configure for the test
+        setBefore.accept(prov1, false);
+        setBefore.accept(prov2, false);
+
+        setAfter.accept(prov1, false);
+        setAfter.accept(prov2, false);
+
+        // run the action
+        action.run();
+
+        // verify that various methods were invoked
+        verifyBefore.accept(prov1);
+        verifyBefore.accept(prov2);
+
+        verifyMiddle.run();
+
+        verifyAfter.accept(prov1);
+        verifyAfter.accept(prov2);
+    }
+
+    /**
+     * Performs an operation that has a beforeXxx method and an afterXxx method. Tries the
+     * case where the first provider's afterXxx returns {@code true}, while the others
+     * return {@code false}.
+     *
+     * @param setBefore function to set the return value of a provider's beforeXxx method
+     * @param setAfter function to set the return value of a provider's afterXxx method
+     * @param action invokes the operation
+     * @param verifyBefore verifies that a provider's beforeXxx method was invoked
+     * @param verifyMiddle verifies that the action occurring between the beforeXxx loop
+     *        and the afterXxx loop was invoked
+     * @param verifyAfter verifies that a provider's afterXxx method was invoked
+     * @throws Exception if an error occurs while calling {@link #setUp()}
+     */
+    private void checkBeforeAfter_FalseTrue(BiConsumer<PolicyEngineFeatureAPI, Boolean> setBefore,
+                    BiConsumer<PolicyEngineFeatureAPI, Boolean> setAfter, Runnable action,
+                    Consumer<PolicyEngineFeatureAPI> verifyBefore, Runnable verifyMiddle,
+                    Consumer<PolicyEngineFeatureAPI> verifyAfter) throws Exception {
+
+        setUp();
+
+        // configure for the test
+        setBefore.accept(prov1, false);
+        setBefore.accept(prov2, false);
+
+        setAfter.accept(prov1, true);
+        setAfter.accept(prov2, false);
+
+        // run the action
+        action.run();
+
+        // verify that various methods were invoked
+        verifyBefore.accept(prov1);
+        verifyBefore.accept(prov2);
+
+        verifyMiddle.run();
+
+        verifyAfter.accept(prov1);
+        assertThrows(AssertionError.class, () -> verifyAfter.accept(prov2));
+    }
+
+    /**
+     * Performs an operation that has a beforeXxx method and an afterXxx method. Tries the
+     * case where the first provider's beforeXxx returns {@code true}, while the others
+     * return {@code false}.
+     *
+     * @param setBefore function to set the return value of a provider's beforeXxx method
+     * @param setAfter function to set the return value of a provider's afterXxx method
+     * @param action invokes the operation
+     * @param verifyBefore verifies that a provider's beforeXxx method was invoked
+     * @param verifyMiddle verifies that the action occurring between the beforeXxx loop
+     *        and the afterXxx loop was invoked
+     * @param verifyAfter verifies that a provider's afterXxx method was invoked
+     * @throws Exception if an error occurs while calling {@link #setUp()}
+     */
+    private void checkBeforeAfter_TrueFalse(BiConsumer<PolicyEngineFeatureAPI, Boolean> setBefore,
+                    BiConsumer<PolicyEngineFeatureAPI, Boolean> setAfter, Runnable action,
+                    Consumer<PolicyEngineFeatureAPI> verifyBefore, Runnable verifyMiddle,
+                    Consumer<PolicyEngineFeatureAPI> verifyAfter) throws Exception {
+
+        setUp();
+
+        // configure for the test
+        setBefore.accept(prov1, true);
+        setBefore.accept(prov2, false);
+
+        setAfter.accept(prov1, false);
+        setAfter.accept(prov2, false);
+
+        // run the action
+        action.run();
+
+        // verify that various methods were invoked
+        verifyBefore.accept(prov1);
+
+        // remaining methods should not have been invoked
+        assertThrows(AssertionError.class, () -> verifyBefore.accept(prov2));
+
+        assertThrows(AssertionError.class, () -> verifyMiddle.run());
+
+        assertThrows(AssertionError.class, () -> verifyAfter.accept(prov1));
+        assertThrows(AssertionError.class, () -> verifyAfter.accept(prov2));
+    }
+
+    /**
+     * Manager with overrides.
+     */
+    private class PolicyEngineManagerImpl extends PolicyEngineManager {
+
+        @Override
+        protected List<PolicyEngineFeatureAPI> getEngineProviders() {
+            return providers;
+        }
+
+        @Override
+        protected List<PolicyControllerFeatureAPI> getControllerProviders() {
+            return contProviders;
+        }
+
+        @Override
+        protected void globalInitContainer(String[] cliArgs) {
+            globalInitArgs = cliArgs;
+        }
+
+        @Override
+        protected TopicEndpoint getTopicEndpointManager() {
+            return endpoint;
+        }
+
+        @Override
+        protected HttpServletServerFactory getServletFactory() {
+            return serverFactory;
+        }
+
+        @Override
+        protected PolicyControllerFactory getControllerFactory() {
+            return controllerFactory;
+        }
+
+        @Override
+        protected void startPdpJmxListener() {
+            jmxStarted = true;
+        }
+
+        @Override
+        protected void stopPdpJmxListener() {
+            jmxStopped = true;
+        }
+
+        @Override
+        protected Thread makeShutdownThread() {
+            shutdownThread = new MyShutdown();
+            return shutdownThread;
+        }
+
+        @Override
+        protected EventProtocolCoder getProtocolCoder() {
+            return coder;
+        }
+
+        @Override
+        protected SystemPersistence getPersistenceManager() {
+            return persist;
+        }
+
+        @Override
+        protected PolicyEngine getPolicyEngine() {
+            return engine;
+        }
+
+        /**
+         * Shutdown thread with overrides.
+         */
+        private class MyShutdown extends ShutdownThread {
+
+            @Override
+            protected void doSleep(long sleepMs) throws InterruptedException {
+                threadSleepMs = sleepMs;
+
+                if (shouldInterrupt) {
+                    throw new InterruptedException(EXPECTED);
+                }
+            }
+
+            @Override
+            protected void doExit(int code) {
+                threadExitCode = code;
+            }
+
+            @Override
+            public synchronized void start() {
+                threadStarted = true;
+            }
+
+            @Override
+            public void interrupt() {
+                threadInterrupted = true;
+            }
+        }
+    }
+}