Merge "Add data type and policy type reference checking"
authorJim Hahn <jrh3@att.com>
Mon, 10 Feb 2020 12:31:03 +0000 (12:31 +0000)
committerGerrit Code Review <gerrit@onap.org>
Mon, 10 Feb 2020 12:31:03 +0000 (12:31 +0000)
67 files changed:
models-examples/src/main/resources/policies/vCPE.policy.operational.input.tosca.json
models-examples/src/main/resources/policies/vCPE.policy.operational.input.tosca.yaml
models-interactions/model-actors/actor.appc/src/main/java/org/onap/policy/controlloop/actor/appc/AppcActorServiceProvider.java
models-interactions/model-actors/actor.appclcm/src/main/java/org/onap/policy/controlloop/actor/appclcm/AppcLcmActorServiceProvider.java
models-interactions/model-actors/actor.cds/src/main/java/org/onap/policy/controlloop/actor/cds/CdsActorServiceProvider.java
models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperator.java [new file with mode: 0644]
models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperator.java [new file with mode: 0644]
models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProvider.java
models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncOperator.java [new file with mode: 0644]
models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperatorTest.java [new file with mode: 0644]
models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BasicOperator.java [new file with mode: 0644]
models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperatorTest.java [new file with mode: 0644]
models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProviderTest.java
models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncOperatorTest.java [new file with mode: 0644]
models-interactions/model-actors/actor.sdnc/src/test/resources/bod.json [new file with mode: 0644]
models-interactions/model-actors/actor.sdnc/src/test/resources/reroute.json [new file with mode: 0644]
models-interactions/model-actors/actor.sdnr/src/main/java/org/onap/policy/controlloop/actor/sdnr/SdnrActorServiceProvider.java
models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java
models-interactions/model-actors/actor.vfc/src/main/java/org/onap/policy/controlloop/actor/vfc/VfcActorServiceProvider.java
models-interactions/model-actors/actorServiceProvider/pom.xml
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/ActorService.java
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandler.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/CallbackManager.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/DelayedIdentString.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/OperationOutcome.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operator.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Util.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContext.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImpl.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActor.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperator.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartial.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/StartConfigPartial.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParams.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParams.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParams.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ParameterValidationRuntimeException.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParams.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/FutureManager.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/ListenerManager.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFuture.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/spi/Actor.java
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/ActorServiceProviderTest.java
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/ActorServiceTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandlerTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/CallbackManagerTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/DelayedIdentStringTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/DummyActor.java
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/OperationOutcomeTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/UtilTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContextTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImplTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActorTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperatorTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartialTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/StartConfigPartialTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParamsTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParamsTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParamsTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ParameterValidationRuntimeExceptionTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParamsTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/FutureManagerTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/ListenerManagerTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFutureTest.java [new file with mode: 0644]
models-interactions/model-actors/actorServiceProvider/src/test/resources/logback-test.xml [new file with mode: 0644]
models-interactions/model-impl/events/src/main/java/org/onap/policy/controlloop/ControlLoopOperation.java
models-interactions/model-impl/sdnc/src/main/java/org/onap/policy/sdnc/SdncManager.java

index 6f3545b..6d9c030 100644 (file)
@@ -21,7 +21,7 @@
                 "description": "Restart the VM",
                 "operation": {
                   "actor": "APPC",
-                  "recipe": "Restart",
+                  "operation": "Restart",
                   "target": {
                     "type": "VNF"
                   }
index 0f669d9..9641eec 100644 (file)
@@ -17,7 +17,7 @@ topology_template:
                     description: Restart the VM
                     operation:
                         actor: APPC
-                        recipe: Restart
+                        operation: Restart
                         target:
                             type: VNF
                     timeout: 1200
index e2c997b..0da1e2a 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  * APPCActorServiceProvider
  * ================================================================================
- * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved.
+ * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,13 +33,15 @@ import org.onap.policy.common.utils.coder.CoderException;
 import org.onap.policy.common.utils.coder.StandardCoder;
 import org.onap.policy.controlloop.ControlLoopOperation;
 import org.onap.policy.controlloop.VirtualControlLoopEvent;
-import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.onap.policy.controlloop.actorserviceprovider.impl.ActorImpl;
 import org.onap.policy.controlloop.policy.Policy;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 
-public class AppcActorServiceProvider implements Actor {
+public class AppcActorServiceProvider extends ActorImpl {
+    private static final String NAME = "APPC";
+
     private static final Logger logger = LoggerFactory.getLogger(AppcActorServiceProvider.class);
 
     private static final StandardCoder coder = new StandardCoder();
@@ -62,9 +64,13 @@ public class AppcActorServiceProvider implements Actor {
     private static final ImmutableMap<String, List<String>> payloads = new ImmutableMap.Builder<String, List<String>>()
             .put(RECIPE_MODIFY, ImmutableList.of("generic-vnf.vnf-id")).build();
 
+    public AppcActorServiceProvider() {
+        super(NAME);
+    }
+
     @Override
     public String actor() {
-        return "APPC";
+        return NAME;
     }
 
     @Override
index fb30758..47898f7 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  * AppcLcmActorServiceProvider
  * ================================================================================
- * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved.
+ * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved.
  * Modifications copyright (c) 2018 Nokia
  * Modifications Copyright (C) 2019 Nordix Foundation.
  * ================================================================================
@@ -38,13 +38,15 @@ import org.onap.policy.appclcm.AppcLcmOutput;
 import org.onap.policy.appclcm.AppcLcmResponseCode;
 import org.onap.policy.controlloop.ControlLoopOperation;
 import org.onap.policy.controlloop.VirtualControlLoopEvent;
-import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.onap.policy.controlloop.actorserviceprovider.impl.ActorImpl;
 import org.onap.policy.controlloop.policy.Policy;
 import org.onap.policy.controlloop.policy.PolicyResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class AppcLcmActorServiceProvider implements Actor {
+public class AppcLcmActorServiceProvider extends ActorImpl {
+
+    private static final String NAME = "APPC";
 
     private static final Logger logger = LoggerFactory.getLogger(AppcLcmActorServiceProvider.class);
 
@@ -74,9 +76,13 @@ public class AppcLcmActorServiceProvider implements Actor {
             new ImmutableMap.Builder<String, List<String>>().put(RECIPE_RESTART, ImmutableList.of(APPC_VM_ID))
                     .put(RECIPE_MODIFY, ImmutableList.of(APPC_REQUEST_PARAMS, APPC_CONFIG_PARAMS)).build();
 
+    public AppcLcmActorServiceProvider() {
+        super(NAME);
+    }
+
     @Override
     public String actor() {
-        return "APPC";
+        return NAME;
     }
 
     @Override
index df13ba3..05ff02e 100644 (file)
@@ -1,6 +1,7 @@
 /*-
  * ============LICENSE_START=======================================================
  * Copyright (C) 2019 Bell Canada. All rights reserved.
+ * Modifications Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -47,7 +48,7 @@ import org.onap.policy.controlloop.ControlLoopOperation;
 import org.onap.policy.controlloop.VirtualControlLoopEvent;
 import org.onap.policy.controlloop.actor.cds.constants.CdsActorConstants;
 import org.onap.policy.controlloop.actor.cds.request.CdsActionRequest;
-import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.onap.policy.controlloop.actorserviceprovider.impl.ActorImpl;
 import org.onap.policy.controlloop.policy.Policy;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -55,10 +56,14 @@ import org.slf4j.LoggerFactory;
 /**
  * CDS Actor service-provider implementation. This is a deploy dark feature for El-Alto release.
  */
-public class CdsActorServiceProvider implements Actor {
+public class CdsActorServiceProvider extends ActorImpl {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(CdsActorServiceProvider.class);
 
+    public CdsActorServiceProvider() {
+        super(CdsActorConstants.CDS_ACTOR);
+    }
+
     /**
      * {@inheritDoc}.
      */
diff --git a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperator.java b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperator.java
new file mode 100644 (file)
index 0000000..2927bd8
--- /dev/null
@@ -0,0 +1,108 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.sdnc;
+
+import java.util.UUID;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.sdnc.SdncHealRequest;
+import org.onap.policy.sdnc.SdncHealRequestHeaderInfo;
+import org.onap.policy.sdnc.SdncHealRequestInfo;
+import org.onap.policy.sdnc.SdncHealServiceInfo;
+import org.onap.policy.sdnc.SdncHealVfModuleInfo;
+import org.onap.policy.sdnc.SdncHealVfModuleParameter;
+import org.onap.policy.sdnc.SdncHealVfModuleParametersInfo;
+import org.onap.policy.sdnc.SdncHealVfModuleRequestInput;
+import org.onap.policy.sdnc.SdncHealVnfInfo;
+import org.onap.policy.sdnc.SdncRequest;
+
+public class BandwidthOnDemandOperator extends SdncOperator {
+    public static final String NAME = "BandwidthOnDemand";
+
+    public static final String URI = "/GENERIC-RESOURCE-API:vf-module-topology-operation";
+
+    // fields in the enrichment data
+    public static final String SERVICE_ID_KEY = "service-instance.service-instance-id";
+    public static final String VNF_ID = "vnfId";
+
+    /**
+     * Constructs the object.
+     *
+     * @param actorName name of the actor with which this operator is associated
+     */
+    public BandwidthOnDemandOperator(String actorName) {
+        super(actorName, NAME);
+    }
+
+    @Override
+    protected SdncRequest constructRequest(ControlLoopEventContext context) {
+        String serviceInstance = context.getEnrichment().get(SERVICE_ID_KEY);
+        if (StringUtils.isBlank(serviceInstance)) {
+            throw new IllegalArgumentException("missing enrichment data, " + SERVICE_ID_KEY);
+        }
+
+        SdncHealVfModuleParameter bandwidth = new SdncHealVfModuleParameter();
+        bandwidth.setName("bandwidth");
+        bandwidth.setValue(context.getEnrichment().get("bandwidth"));
+
+        SdncHealVfModuleParameter timeStamp = new SdncHealVfModuleParameter();
+        timeStamp.setName("bandwidth-change-time");
+        timeStamp.setValue(context.getEnrichment().get("bandwidth-change-time"));
+
+        SdncHealVfModuleParametersInfo vfParametersInfo = new SdncHealVfModuleParametersInfo();
+        vfParametersInfo.addParameters(bandwidth);
+        vfParametersInfo.addParameters(timeStamp);
+
+        SdncHealVfModuleRequestInput vfRequestInfo = new SdncHealVfModuleRequestInput();
+        vfRequestInfo.setVfModuleParametersInfo(vfParametersInfo);
+
+        SdncHealServiceInfo serviceInfo = new SdncHealServiceInfo();
+        serviceInfo.setServiceInstanceId(serviceInstance);
+
+        SdncHealRequestInfo requestInfo = new SdncHealRequestInfo();
+        requestInfo.setRequestAction("SdwanBandwidthChange");
+
+        SdncHealRequestHeaderInfo headerInfo = new SdncHealRequestHeaderInfo();
+        headerInfo.setSvcAction("update");
+        headerInfo.setSvcRequestId(UUID.randomUUID().toString());
+
+        SdncRequest request = new SdncRequest();
+        request.setNsInstanceId(serviceInstance);
+        request.setRequestId(context.getRequestId());
+        request.setUrl(URI);
+
+        SdncHealVnfInfo vnfInfo = new SdncHealVnfInfo();
+        vnfInfo.setVnfId(context.getEnrichment().get(VNF_ID));
+
+        SdncHealVfModuleInfo vfModuleInfo = new SdncHealVfModuleInfo();
+        vfModuleInfo.setVfModuleId("");
+
+        SdncHealRequest healRequest = new SdncHealRequest();
+        healRequest.setVnfInfo(vnfInfo);
+        healRequest.setVfModuleInfo(vfModuleInfo);
+        healRequest.setRequestHeaderInfo(headerInfo);
+        healRequest.setVfModuleRequestInput(vfRequestInfo);
+        healRequest.setRequestInfo(requestInfo);
+        healRequest.setServiceInfo(serviceInfo);
+        request.setHealRequest(healRequest);
+        return request;
+    }
+}
diff --git a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperator.java b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperator.java
new file mode 100644 (file)
index 0000000..da400f8
--- /dev/null
@@ -0,0 +1,87 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.sdnc;
+
+import java.util.UUID;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.sdnc.SdncHealNetworkInfo;
+import org.onap.policy.sdnc.SdncHealRequest;
+import org.onap.policy.sdnc.SdncHealRequestHeaderInfo;
+import org.onap.policy.sdnc.SdncHealRequestInfo;
+import org.onap.policy.sdnc.SdncHealServiceInfo;
+import org.onap.policy.sdnc.SdncRequest;
+
+public class RerouteOperator extends SdncOperator {
+    public static final String NAME = "Reroute";
+
+    public static final String URI = "/GENERIC-RESOURCE-API:network-topology-operation";
+
+    // fields in the enrichment data
+    public static final String SERVICE_ID_KEY = "service-instance.service-instance-id";
+    public static final String NETWORK_ID_KEY = "network-information.network-id";
+
+    /**
+     * Constructs the object.
+     *
+     * @param actorName name of the actor with which this operator is associated
+     */
+    public RerouteOperator(String actorName) {
+        super(actorName, NAME);
+    }
+
+    @Override
+    protected SdncRequest constructRequest(ControlLoopEventContext context) {
+        String serviceInstance = context.getEnrichment().get(SERVICE_ID_KEY);
+        if (StringUtils.isBlank(serviceInstance)) {
+            throw new IllegalArgumentException("missing enrichment data, " + SERVICE_ID_KEY);
+        }
+        SdncHealServiceInfo serviceInfo = new SdncHealServiceInfo();
+        serviceInfo.setServiceInstanceId(serviceInstance);
+
+        String networkId = context.getEnrichment().get(NETWORK_ID_KEY);
+        if (StringUtils.isBlank(networkId)) {
+            throw new IllegalArgumentException("missing enrichment data, " + NETWORK_ID_KEY);
+        }
+        SdncHealNetworkInfo networkInfo = new SdncHealNetworkInfo();
+        networkInfo.setNetworkId(networkId);
+
+        SdncHealRequestInfo requestInfo = new SdncHealRequestInfo();
+        requestInfo.setRequestAction("ReoptimizeSOTNInstance");
+
+        SdncHealRequestHeaderInfo headerInfo = new SdncHealRequestHeaderInfo();
+        headerInfo.setSvcAction("reoptimize");
+        headerInfo.setSvcRequestId(UUID.randomUUID().toString());
+
+        SdncRequest request = new SdncRequest();
+        request.setNsInstanceId(serviceInstance);
+        request.setRequestId(context.getRequestId());
+        request.setUrl(URI);
+
+        SdncHealRequest healRequest = new SdncHealRequest();
+        healRequest.setRequestHeaderInfo(headerInfo);
+        healRequest.setNetworkInfo(networkInfo);
+        healRequest.setRequestInfo(requestInfo);
+        healRequest.setServiceInfo(serviceInfo);
+        request.setHealRequest(healRequest);
+        return request;
+    }
+}
index 24d019f..8dc8ba5 100644 (file)
@@ -4,7 +4,7 @@
  * ================================================================================
  * Copyright (C) 2018-2019 Huawei Intellectual Property. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
- * Modifications Copyright (C) 2019 AT&T Intellectual Property.
+ * Modifications Copyright (C) 2019-2020 AT&T Intellectual Property.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import java.util.List;
 import java.util.UUID;
 import org.onap.policy.controlloop.ControlLoopOperation;
 import org.onap.policy.controlloop.VirtualControlLoopEvent;
-import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.onap.policy.controlloop.actorserviceprovider.impl.HttpActor;
 import org.onap.policy.controlloop.policy.Policy;
 import org.onap.policy.sdnc.SdncHealNetworkInfo;
 import org.onap.policy.sdnc.SdncHealRequest;
@@ -45,10 +45,13 @@ import org.onap.policy.sdnc.SdncRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-
-public class SdncActorServiceProvider implements Actor {
+public class SdncActorServiceProvider extends HttpActor {
     private static final Logger logger = LoggerFactory.getLogger(SdncActorServiceProvider.class);
 
+    public static final String NAME = "SDNC";
+
+    // TODO old code: remove lines down to **HERE**
+
     // Strings for Sdnc Actor
     private static final String SDNC_ACTOR = "SDNC";
 
@@ -62,8 +65,23 @@ public class SdncActorServiceProvider implements Actor {
     private static final String RECIPE_BW_ON_DEMAND = "BandwidthOnDemand";
 
     private static final ImmutableList<String> recipes = ImmutableList.of(RECIPE_REROUTE);
-    private static final ImmutableMap<String, List<String>> targets =
-            new ImmutableMap.Builder<String, List<String>>().put(RECIPE_REROUTE, ImmutableList.of(TARGET_VM)).build();
+    private static final ImmutableMap<String, List<String>> targets = new ImmutableMap.Builder<String, List<String>>()
+                    .put(RECIPE_REROUTE, ImmutableList.of(TARGET_VM)).build();
+
+    // **HERE**
+
+    /**
+     * Constructs the object.
+     */
+    public SdncActorServiceProvider() {
+        super(NAME);
+
+        addOperator(new RerouteOperator(NAME));
+        addOperator(new BandwidthOnDemandOperator(NAME));
+    }
+
+
+    // TODO old code: remove lines down to **HERE**
 
     @Override
     public String actor() {
@@ -93,16 +111,15 @@ public class SdncActorServiceProvider implements Actor {
      * @param policy the policy
      * @return the constructed request
      */
-    public SdncRequest constructRequest(VirtualControlLoopEvent onset, ControlLoopOperation operation,
-            Policy policy) {
+    public SdncRequest constructRequest(VirtualControlLoopEvent onset, ControlLoopOperation operation, Policy policy) {
         switch (policy.getRecipe()) {
             case RECIPE_REROUTE:
                 return constructReOptimizeRequest(onset);
             case RECIPE_BW_ON_DEMAND:
-                logger.info("Construct request for receipe {}" , RECIPE_BW_ON_DEMAND);
+                logger.info("Construct request for receipe {}", RECIPE_BW_ON_DEMAND);
                 return constructBwOnDemandRequest(onset);
             default:
-                logger.info("Unsupported recipe {} " + policy.getRecipe());
+                logger.info("Unsupported recipe {}", policy.getRecipe());
                 return null;
         }
     }
@@ -199,4 +216,6 @@ public class SdncActorServiceProvider implements Actor {
         request.setHealRequest(healRequest);
         return request;
     }
+
+    // **HERE**
 }
diff --git a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncOperator.java b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncOperator.java
new file mode 100644 (file)
index 0000000..479ee90
--- /dev/null
@@ -0,0 +1,148 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.sdnc;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.onap.policy.common.endpoints.http.client.HttpClient;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.controlloop.actorserviceprovider.AsyncResponseHandler;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.sdnc.SdncRequest;
+import org.onap.policy.sdnc.SdncResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Superclass for SDNC Operators.
+ */
+public abstract class SdncOperator extends HttpOperator {
+    private static final Logger logger = LoggerFactory.getLogger(SdncOperator.class);
+
+    /**
+     * Constructs the object.
+     *
+     * @param actorName name of the actor with which this operator is associated
+     * @param name operation name
+     */
+    public SdncOperator(String actorName, String name) {
+        super(actorName, name);
+    }
+
+    @Override
+    protected CompletableFuture<OperationOutcome> startOperationAsync(ControlLoopOperationParams params, int attempt,
+                    OperationOutcome outcome) {
+
+        SdncRequest request = constructRequest(params.getContext());
+        return postRequest(params, outcome, request);
+    }
+
+    /**
+     * Constructs the request.
+     *
+     * @param context associated event context
+     * @return a new request
+     */
+    protected abstract SdncRequest constructRequest(ControlLoopEventContext context);
+
+    /**
+     * Posts the request and and arranges to retrieve the response.
+     *
+     * @param params operation parameters
+     * @param outcome updated with the response
+     * @param sdncRequest request to be posted
+     * @return the result of the request
+     */
+    private CompletableFuture<OperationOutcome> postRequest(ControlLoopOperationParams params, OperationOutcome outcome,
+                    SdncRequest sdncRequest) {
+        Map<String, Object> headers = new HashMap<>();
+
+        headers.put("Accept", "application/json");
+        String sdncUrl = getClient().getBaseUrl();
+
+        Util.logRestRequest(sdncUrl, sdncRequest);
+
+        Entity<SdncRequest> entity = Entity.entity(sdncRequest, MediaType.APPLICATION_JSON);
+
+        ResponseHandler handler = new ResponseHandler(params, outcome, sdncUrl);
+        return handler.handle(getClient().post(handler, getPath(), entity, headers));
+    }
+
+    private class ResponseHandler extends AsyncResponseHandler<Response> {
+        private final String sdncUrl;
+
+        public ResponseHandler(ControlLoopOperationParams params, OperationOutcome outcome, String sdncUrl) {
+            super(params, outcome);
+            this.sdncUrl = sdncUrl;
+        }
+
+        /**
+         * Handles the response.
+         */
+        @Override
+        protected OperationOutcome doComplete(Response rawResponse) {
+            String strResponse = HttpClient.getBody(rawResponse, String.class);
+
+            Util.logRestResponse(sdncUrl, strResponse);
+
+            SdncResponse response;
+            try {
+                response = makeDecoder().decode(strResponse, SdncResponse.class);
+            } catch (CoderException e) {
+                logger.warn("Sdnc Heal cannot decode response with http error code {}", rawResponse.getStatus(), e);
+                return SdncOperator.this.setOutcome(getParams(), getOutcome(), PolicyResult.FAILURE_EXCEPTION);
+            }
+
+            if (response.getResponseOutput() != null && "200".equals(response.getResponseOutput().getResponseCode())) {
+                return SdncOperator.this.setOutcome(getParams(), getOutcome(), PolicyResult.SUCCESS);
+
+            } else {
+                logger.info("Sdnc Heal Restcall failed with http error code {}", rawResponse.getStatus());
+                return SdncOperator.this.setOutcome(getParams(), getOutcome(), PolicyResult.FAILURE);
+            }
+        }
+
+        /**
+         * Handles exceptions.
+         */
+        @Override
+        protected OperationOutcome doFailed(Throwable thrown) {
+            logger.info("Sdnc Heal Restcall threw an exception", thrown);
+            return SdncOperator.this.setOutcome(getParams(), getOutcome(), PolicyResult.FAILURE_EXCEPTION);
+        }
+    }
+
+    // these may be overridden by junit tests
+
+    protected StandardCoder makeDecoder() {
+        return new StandardCoder();
+    }
+}
diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperatorTest.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperatorTest.java
new file mode 100644 (file)
index 0000000..02931a4
--- /dev/null
@@ -0,0 +1,70 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.sdnc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.sdnc.SdncRequest;
+
+public class BandwidthOnDemandOperatorTest extends BasicOperator {
+
+    private BandwidthOnDemandOperator oper;
+
+
+    /**
+     * Set up.
+     */
+    @Before
+    public void setUp() {
+        makeContext();
+        oper = new BandwidthOnDemandOperator(ACTOR);
+    }
+
+    @Test
+    public void testBandwidthOnDemandOperator() {
+        assertEquals(ACTOR, oper.getActorName());
+        assertEquals(BandwidthOnDemandOperator.NAME, oper.getName());
+    }
+
+    @Test
+    public void testConstructRequest() throws CoderException {
+        SdncRequest request = oper.constructRequest(context);
+        assertEquals("my-service", request.getNsInstanceId());
+        assertEquals(REQ_ID, request.getRequestId());
+        assertEquals(BandwidthOnDemandOperator.URI, request.getUrl());
+        assertNotNull(request.getHealRequest().getRequestHeaderInfo().getSvcRequestId());
+
+        verifyRequest("bod.json", request);
+
+        verifyMissing(oper, BandwidthOnDemandOperator.SERVICE_ID_KEY, "service");
+    }
+
+    @Override
+    protected Map<String, String> makeEnrichment() {
+        return Map.of(BandwidthOnDemandOperator.SERVICE_ID_KEY, "my-service", BandwidthOnDemandOperator.VNF_ID,
+                        "my-vnf");
+    }
+}
diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BasicOperator.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BasicOperator.java
new file mode 100644 (file)
index 0000000..b9028d4
--- /dev/null
@@ -0,0 +1,94 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.sdnc;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.UUID;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.common.utils.resources.ResourceUtils;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+
+/**
+ * Superclass for various operator tests.
+ */
+public abstract class BasicOperator {
+    protected static final UUID REQ_ID = UUID.randomUUID();
+    protected static final String ACTOR = "my-actor";
+
+    protected Map<String, String> enrichment;
+    protected VirtualControlLoopEvent event;
+    protected ControlLoopEventContext context;
+
+    /**
+     * Pretty-prints a request and verifies that the result matches the expected JSON.
+     *
+     * @param <T> request type
+     * @param expectedJsonFile name of the file containing the expected JSON
+     * @param request request to verify
+     * @throws CoderException if the request cannot be pretty-printed
+     */
+    protected <T> void verifyRequest(String expectedJsonFile, T request) throws CoderException {
+        String json = new StandardCoder().encode(request, true);
+        String expected = ResourceUtils.getResourceAsString(expectedJsonFile);
+
+        // strip request id, because it changes each time
+        final String stripper = "svc-request-id[^,]*";
+        json = json.replaceFirst(stripper, "").trim();
+        expected = expected.replaceFirst(stripper, "").trim();
+
+        assertEquals(expected, json);
+    }
+
+    /**
+     * Verifies that an exception is thrown if a field is missing from the enrichment
+     * data.
+     *
+     * @param oper operator to construct the request
+     * @param fieldName name of the field to be removed from the enrichment data
+     * @param expectedText text expected in the exception message
+     */
+    protected void verifyMissing(SdncOperator oper, String fieldName, String expectedText) {
+        makeContext();
+        enrichment.remove(fieldName);
+
+        assertThatIllegalArgumentException().isThrownBy(() -> oper.constructRequest(context))
+                        .withMessageContaining("missing").withMessageContaining(expectedText);
+    }
+
+    protected void makeContext() {
+        // need a mutable map, so make a copy
+        enrichment = new TreeMap<>(makeEnrichment());
+
+        event = new VirtualControlLoopEvent();
+        event.setRequestId(REQ_ID);
+        event.setAai(enrichment);
+
+        context = new ControlLoopEventContext(event);
+    }
+
+    protected abstract Map<String, String> makeEnrichment();
+}
diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperatorTest.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperatorTest.java
new file mode 100644 (file)
index 0000000..0a7bcad
--- /dev/null
@@ -0,0 +1,70 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.sdnc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.sdnc.SdncRequest;
+
+public class RerouteOperatorTest extends BasicOperator {
+
+    private RerouteOperator oper;
+
+
+    /**
+     * Set up.
+     */
+    @Before
+    public void setUp() {
+        makeContext();
+        oper = new RerouteOperator(ACTOR);
+    }
+
+    @Test
+    public void testRerouteOperator() {
+        assertEquals(ACTOR, oper.getActorName());
+        assertEquals(RerouteOperator.NAME, oper.getName());
+    }
+
+    @Test
+    public void testConstructRequest() throws CoderException {
+        SdncRequest request = oper.constructRequest(context);
+        assertEquals("my-service", request.getNsInstanceId());
+        assertEquals(REQ_ID, request.getRequestId());
+        assertEquals(RerouteOperator.URI, request.getUrl());
+        assertNotNull(request.getHealRequest().getRequestHeaderInfo().getSvcRequestId());
+
+        verifyRequest("reroute.json", request);
+
+        verifyMissing(oper, RerouteOperator.SERVICE_ID_KEY, "service");
+        verifyMissing(oper, RerouteOperator.NETWORK_ID_KEY, "network");
+    }
+
+    @Override
+    protected Map<String, String> makeEnrichment() {
+        return Map.of(RerouteOperator.SERVICE_ID_KEY, "my-service", RerouteOperator.NETWORK_ID_KEY, "my-network");
+    }
+}
index 9739c71..08655c3 100644 (file)
@@ -3,7 +3,7 @@
  * TestSdncActorServiceProvider
  * ================================================================================
  * Copyright (C) 2018-2019 Huawei. All rights reserved.
- * Modifications Copyright (C) 2018-2019 AT&T Corp. All rights reserved.
+ * Modifications Copyright (C) 2018-2020 AT&T Corp. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,8 +26,10 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
+import java.util.Arrays;
 import java.util.Objects;
 import java.util.UUID;
+import java.util.stream.Collectors;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -36,19 +38,19 @@ import org.onap.policy.controlloop.ControlLoopOperation;
 import org.onap.policy.controlloop.VirtualControlLoopEvent;
 import org.onap.policy.controlloop.policy.Policy;
 import org.onap.policy.sdnc.SdncRequest;
-import org.onap.policy.simulators.Util;
 
 public class SdncActorServiceProviderTest {
 
-    private static final String REROUTE = "Reroute";
+    private static final String REROUTE = RerouteOperator.NAME;
 
     /**
      * Set up before test class.
+     *
      * @throws Exception if the A&AI simulator cannot be started
      */
     @BeforeClass
     public static void setUpSimulator() throws Exception {
-        Util.buildAaiSim();
+        org.onap.policy.simulators.Util.buildAaiSim();
     }
 
     @AfterClass
@@ -56,6 +58,18 @@ public class SdncActorServiceProviderTest {
         HttpServletServerFactoryInstance.getServerFactory().destroy();
     }
 
+    @Test
+    public void testSdncActorServiceProvider() {
+        final SdncActorServiceProvider prov = new SdncActorServiceProvider();
+
+        // verify that it has the operators we expect
+        var expected = Arrays.asList(BandwidthOnDemandOperator.NAME, RerouteOperator.NAME).stream().sorted()
+                        .collect(Collectors.toList());
+        var actual = prov.getOperationNames().stream().sorted().collect(Collectors.toList());
+
+        assertEquals(expected.toString(), actual.toString());
+    }
+
     @Test
     public void testConstructRequest() {
         VirtualControlLoopEvent onset = new VirtualControlLoopEvent();
@@ -84,8 +98,7 @@ public class SdncActorServiceProviderTest {
         policy.setRecipe(REROUTE);
         assertNotNull(provider.constructRequest(onset, operation, policy));
 
-        SdncRequest request =
-                provider.constructRequest(onset, operation, policy);
+        SdncRequest request = provider.constructRequest(onset, operation, policy);
 
         assertEquals(requestId, Objects.requireNonNull(request).getRequestId());
         assertEquals("reoptimize", request.getHealRequest().getRequestHeaderInfo().getSvcAction());
diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncOperatorTest.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncOperatorTest.java
new file mode 100644 (file)
index 0000000..25d383e
--- /dev/null
@@ -0,0 +1,326 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.sdnc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import lombok.Setter;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onap.policy.common.endpoints.event.comm.bus.internal.BusTopicParams;
+import org.onap.policy.common.endpoints.event.comm.bus.internal.BusTopicParams.TopicParamsBuilder;
+import org.onap.policy.common.endpoints.http.client.HttpClient;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance;
+import org.onap.policy.common.endpoints.http.server.HttpServletServer;
+import org.onap.policy.common.endpoints.http.server.HttpServletServerFactoryInstance;
+import org.onap.policy.common.endpoints.properties.PolicyEndPointProperties;
+import org.onap.policy.common.gson.GsonMessageBodyHandler;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.common.utils.network.NetworkUtil;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.sdnc.SdncHealRequest;
+import org.onap.policy.sdnc.SdncRequest;
+import org.onap.policy.sdnc.SdncResponse;
+import org.onap.policy.sdnc.SdncResponseOutput;
+
+public class SdncOperatorTest {
+    public static final String MEDIA_TYPE_APPLICATION_JSON = "application/json";
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+    public static final String HTTP_CLIENT = "my-http-client";
+    public static final String HTTP_NO_SERVER = "my-http-no-server-client";
+    private static final String ACTOR = "my-actor";
+    private static final String OPERATION = "my-operation";
+
+    /**
+     * Outcome to be added to the response.
+     */
+    @Setter
+    private static SdncResponseOutput output;
+
+
+    private VirtualControlLoopEvent event;
+    private ControlLoopEventContext context;
+    private MyOper oper;
+
+    /**
+     * Starts the SDNC simulator.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+        // allocate a port
+        int port = NetworkUtil.allocPort();
+
+        /*
+         * Start the simulator. Must use "Properties" to configure it, otherwise the
+         * server will use the wrong serialization provider.
+         */
+        Properties svrprops = getServerProperties("my-server", port);
+        HttpServletServerFactoryInstance.getServerFactory().build(svrprops).forEach(HttpServletServer::start);
+
+        /*
+         * Start the clients, one to the server, and one to a non-existent server.
+         */
+        TopicParamsBuilder builder = BusTopicParams.builder().managed(true).hostname("localhost").basePath("sdnc")
+                        .serializationProvider(GsonMessageBodyHandler.class.getName());
+
+        HttpClientFactoryInstance.getClientFactory().build(builder.clientName(HTTP_CLIENT).port(port).build());
+
+        HttpClientFactoryInstance.getClientFactory()
+                        .build(builder.clientName(HTTP_NO_SERVER).port(NetworkUtil.allocPort()).build());
+    }
+
+    @AfterClass
+    public static void tearDownAfterClass() {
+        HttpClientFactoryInstance.getClientFactory().destroy();
+        HttpServletServerFactoryInstance.getServerFactory().destroy();
+    }
+
+    /**
+     * Initializes {@link #oper} and sets {@link #output} to a success code.
+     */
+    @Before
+    public void setUp() {
+        event = new VirtualControlLoopEvent();
+        context = new ControlLoopEventContext(event);
+
+        initOper(HTTP_CLIENT);
+
+        output = new SdncResponseOutput();
+        output.setResponseCode("200");
+    }
+
+    @After
+    public void tearDown() {
+        oper.shutdown();
+    }
+
+    @Test
+    public void testSdncOperator() {
+        assertEquals(ACTOR, oper.getActorName());
+        assertEquals(OPERATION, oper.getName());
+        assertEquals(ACTOR + "." + OPERATION, oper.getFullName());
+    }
+
+    @Test
+    public void testGetClient() {
+        assertNotNull(oper.getTheClient());
+    }
+
+    @Test
+    public void testStartOperationAsync_testPostRequest() throws Exception {
+        OperationOutcome outcome = runOperation();
+        assertNotNull(outcome);
+        assertEquals(PolicyResult.SUCCESS, outcome.getResult());
+    }
+
+    /**
+     * Tests postRequest() when decode() throws an exception.
+     */
+    @Test
+    public void testPostRequestDecodeException() throws Exception {
+
+        oper.setDecodeFailure(true);
+
+        OperationOutcome outcome = runOperation();
+        assertNotNull(outcome);
+        assertEquals(PolicyResult.FAILURE_EXCEPTION, outcome.getResult());
+    }
+
+    /**
+     * Tests postRequest() when there is no "output" field in the response.
+     */
+    @Test
+    public void testPostRequestNoOutput() throws Exception {
+
+        setOutput(null);
+
+        OperationOutcome outcome = runOperation();
+        assertNotNull(outcome);
+        assertEquals(PolicyResult.FAILURE, outcome.getResult());
+    }
+
+    /**
+     * Tests postRequest() when the output is not a success.
+     */
+    @Test
+    public void testPostRequestOutputFailure() throws Exception {
+
+        output.setResponseCode(null);
+
+        OperationOutcome outcome = runOperation();
+        assertNotNull(outcome);
+        assertEquals(PolicyResult.FAILURE, outcome.getResult());
+    }
+
+    /**
+     * Tests postRequest() when the post() request throws an exception retrieving the
+     * response.
+     */
+    @Test
+    public void testPostRequestException() throws Exception {
+
+        // reset "oper" to point to a non-existent server
+        oper.shutdown();
+        initOper(HTTP_NO_SERVER);
+
+        OperationOutcome outcome = runOperation();
+        assertNotNull(outcome);
+        assertEquals(PolicyResult.FAILURE_EXCEPTION, outcome.getResult());
+    }
+
+    private static Properties getServerProperties(String name, int port) {
+        final Properties props = new Properties();
+        props.setProperty(PolicyEndPointProperties.PROPERTY_HTTP_SERVER_SERVICES, name);
+
+        final String svcpfx = PolicyEndPointProperties.PROPERTY_HTTP_SERVER_SERVICES + "." + name;
+
+        props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_REST_CLASSES_SUFFIX, Server.class.getName());
+        props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_HOST_SUFFIX, "localhost");
+        props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_PORT_SUFFIX, String.valueOf(port));
+        props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_MANAGED_SUFFIX, "true");
+        props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SWAGGER_SUFFIX, "false");
+
+        props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SERIALIZATION_PROVIDER,
+                        GsonMessageBodyHandler.class.getName());
+        return props;
+    }
+
+    /**
+     * Initializes {@link #oper}.
+     *
+     * @param clientName name of the client which it should use
+     */
+    private void initOper(String clientName) {
+        oper = new MyOper();
+
+        HttpParams params = HttpParams.builder().clientName(clientName).path("request").build();
+        Map<String, Object> mapParams = Util.translateToMap(OPERATION, params);
+        oper.configure(mapParams);
+        oper.start();
+    }
+
+    /**
+     * Runs the operation.
+     *
+     * @return the outcome of the operation, or {@code null} if it does not complete in
+     *         time
+     */
+    private OperationOutcome runOperation() throws InterruptedException, ExecutionException, TimeoutException {
+        ControlLoopOperationParams params =
+                        ControlLoopOperationParams.builder().actor(ACTOR).operation(OPERATION).context(context).build();
+
+        CompletableFuture<OperationOutcome> future = oper.startOperationAsync(params, 1, params.makeOutcome());
+
+        return future.get(5, TimeUnit.SECONDS);
+    }
+
+
+    private class MyOper extends SdncOperator {
+
+        /**
+         * Set to {@code true} to cause the decoder to throw an exception.
+         */
+        @Setter
+        private boolean decodeFailure = false;
+
+        public MyOper() {
+            super(ACTOR, OPERATION);
+        }
+
+        protected HttpClient getTheClient() {
+            return getClient();
+        }
+
+        @Override
+        protected SdncRequest constructRequest(ControlLoopEventContext context) {
+            SdncRequest request = new SdncRequest();
+
+            SdncHealRequest heal = new SdncHealRequest();
+            request.setHealRequest(heal);
+
+            return request;
+        }
+
+        @Override
+        protected StandardCoder makeDecoder() {
+            if (decodeFailure) {
+                // return a coder that throws exceptions when decode() is invoked
+                return new StandardCoder() {
+                    @Override
+                    public <T> T decode(String json, Class<T> clazz) throws CoderException {
+                        throw new CoderException(EXPECTED_EXCEPTION);
+                    }
+                };
+
+            } else {
+                return super.makeDecoder();
+            }
+        }
+    }
+
+    /**
+     * SDNC Simulator.
+     */
+    @Path("/sdnc")
+    @Produces(MEDIA_TYPE_APPLICATION_JSON)
+    public static class Server {
+
+        /**
+         * Generates a response.
+         *
+         * @param request incoming request
+         * @return resulting response
+         */
+        @POST
+        @Path("/request")
+        @Consumes(value = {MEDIA_TYPE_APPLICATION_JSON})
+        public Response postRequest(SdncRequest request) {
+
+            SdncResponse response = new SdncResponse();
+            response.setResponseOutput(output);
+
+            return Response.status(Status.OK).entity(response).build();
+        }
+    }
+}
diff --git a/models-interactions/model-actors/actor.sdnc/src/test/resources/bod.json b/models-interactions/model-actors/actor.sdnc/src/test/resources/bod.json
new file mode 100644 (file)
index 0000000..8c60bbd
--- /dev/null
@@ -0,0 +1,32 @@
+{
+  "input": {
+    "sdnc-request-header": {
+      "svc-request-id": "076b243e-9236-4409-973d-dd318dcab3e9",
+      "svc-action": "update"
+    },
+    "request-information": {
+      "request-action": "SdwanBandwidthChange"
+    },
+    "service-information": {
+      "service-instance-id": "my-service"
+    },
+    "vnf-information": {
+      "vnf-id": "my-vnf"
+    },
+    "vf-module-information": {
+      "vf-module-id": ""
+    },
+    "vf-module-request-input": {
+      "vf-module-input-parameters": {
+        "param": [
+          {
+            "name": "bandwidth"
+          },
+          {
+            "name": "bandwidth-change-time"
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/models-interactions/model-actors/actor.sdnc/src/test/resources/reroute.json b/models-interactions/model-actors/actor.sdnc/src/test/resources/reroute.json
new file mode 100644 (file)
index 0000000..da70a55
--- /dev/null
@@ -0,0 +1,17 @@
+{
+  "input": {
+    "sdnc-request-header": {
+      "svc-request-id": "66087b39-8606-4367-9ac8-bdf64e162f29",
+      "svc-action": "reoptimize"
+    },
+    "request-information": {
+      "request-action": "ReoptimizeSOTNInstance"
+    },
+    "service-information": {
+      "service-instance-id": "my-service"
+    },
+    "network-information": {
+      "network-id": "my-network"
+    }
+  }
+}
index 4367f54..0919779 100644 (file)
@@ -3,7 +3,7 @@
  * SdnrActorServiceProvider
  * ================================================================================
  * Copyright (C) 2018 Wipro Limited Intellectual Property. All rights reserved.
- * Modifications Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,14 +23,12 @@ package org.onap.policy.controlloop.actor.sdnr;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-
 import java.util.Collections;
 import java.util.List;
-
 import org.onap.policy.controlloop.ControlLoopOperation;
 import org.onap.policy.controlloop.ControlLoopResponse;
 import org.onap.policy.controlloop.VirtualControlLoopEvent;
-import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.onap.policy.controlloop.actorserviceprovider.impl.ActorImpl;
 import org.onap.policy.controlloop.policy.Policy;
 import org.onap.policy.controlloop.policy.PolicyResult;
 import org.onap.policy.sdnr.PciCommonHeader;
@@ -39,11 +37,12 @@ import org.onap.policy.sdnr.PciRequestWrapper;
 import org.onap.policy.sdnr.PciResponse;
 import org.onap.policy.sdnr.PciResponseCode;
 import org.onap.policy.sdnr.PciResponseWrapper;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class SdnrActorServiceProvider implements Actor {
+public class SdnrActorServiceProvider extends ActorImpl {
+
+    private static final String NAME = "SDNR";
 
     public static class Pair<A, B> {
         public final A result;
@@ -81,9 +80,13 @@ public class SdnrActorServiceProvider implements Actor {
     private static final ImmutableMap<String, List<String>> payloads = new ImmutableMap.Builder<String, List<String>>()
             .put(RECIPE_MODIFY, ImmutableList.of(SDNR_REQUEST_PARAMS, SDNR_CONFIG_PARAMS)).build();
 
+    public SdnrActorServiceProvider() {
+        super(NAME);
+    }
+
     @Override
     public String actor() {
-        return "SDNR";
+        return NAME;
     }
 
     @Override
@@ -275,7 +278,7 @@ public class SdnrActorServiceProvider implements Actor {
         /* The ControlLoop response determined from the SDNR Response and input event. */
         ControlLoopResponse clRsp = new ControlLoopResponse();
         clRsp.setPayload(sdnrResponse.getPayload());
-        clRsp.setFrom("SDNR");
+        clRsp.setFrom(NAME);
         clRsp.setTarget("DCAE");
         clRsp.setClosedLoopControlName(event.getClosedLoopControlName());
         clRsp.setPolicyName(event.getPolicyName());
index b853075..a743f49 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  * SOActorServiceProvider
  * ================================================================================
- * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved.
+ * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -35,7 +35,7 @@ import org.onap.aai.domain.yang.Tenant;
 import org.onap.policy.aai.AaiCqResponse;
 import org.onap.policy.controlloop.ControlLoopOperation;
 import org.onap.policy.controlloop.VirtualControlLoopEvent;
-import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.onap.policy.controlloop.actorserviceprovider.impl.ActorImpl;
 import org.onap.policy.controlloop.policy.Policy;
 import org.onap.policy.so.SoCloudConfiguration;
 import org.onap.policy.so.SoManager;
@@ -51,7 +51,7 @@ import org.onap.policy.so.util.Serialization;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class SoActorServiceProvider implements Actor {
+public class SoActorServiceProvider extends ActorImpl {
     private static final Logger logger = LoggerFactory.getLogger(SoActorServiceProvider.class);
 
     private static final String TENANT_NOT_FOUND = "Tenant Item not found in AAI response {}";
@@ -89,6 +89,10 @@ public class SoActorServiceProvider implements Actor {
     private static String lastServiceItemServiceInstanceId;
     private static String lastVfModuleItemVfModuleInstanceId;
 
+    public SoActorServiceProvider() {
+        super(SO_ACTOR);
+    }
+
     @Override
     public String actor() {
         return SO_ACTOR;
index 1ca379f..8d560ef 100644 (file)
@@ -1,7 +1,7 @@
 /*-
  * ============LICENSE_START=======================================================
  * Copyright (C) 2017-2018 Intel Corp. All rights reserved.
- * Modifications Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,21 +22,19 @@ package org.onap.policy.controlloop.actor.vfc;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-
 import java.util.Collections;
 import java.util.List;
-
 import org.onap.policy.aai.AaiCqResponse;
 import org.onap.policy.controlloop.ControlLoopOperation;
 import org.onap.policy.controlloop.VirtualControlLoopEvent;
-import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.onap.policy.controlloop.actorserviceprovider.impl.ActorImpl;
 import org.onap.policy.controlloop.policy.Policy;
 import org.onap.policy.vfc.VfcHealActionVmInfo;
 import org.onap.policy.vfc.VfcHealAdditionalParams;
 import org.onap.policy.vfc.VfcHealRequest;
 import org.onap.policy.vfc.VfcRequest;
 
-public class VfcActorServiceProvider implements Actor {
+public class VfcActorServiceProvider extends ActorImpl {
     private static final String GENERIC_VNF_ID = "generic-vnf.vnf-id";
 
     // Strings for VFC Actor
@@ -52,6 +50,10 @@ public class VfcActorServiceProvider implements Actor {
     private static final ImmutableMap<String, List<String>> targets =
             new ImmutableMap.Builder<String, List<String>>().put(RECIPE_RESTART, ImmutableList.of(TARGET_VM)).build();
 
+    public VfcActorServiceProvider() {
+        super(VFC_ACTOR);
+    }
+
     @Override
     public String actor() {
         return VFC_ACTOR;
index ee3a924..54b13f0 100644 (file)
   ============LICENSE_END=========================================================
   -->
 
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-  <modelVersion>4.0.0</modelVersion>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
 
-  <parent>
-   <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
-    <artifactId>model-actors</artifactId>
-    <version>2.2.1-SNAPSHOT</version>
-  </parent>
+    <parent>
+        <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+        <artifactId>model-actors</artifactId>
+        <version>2.2.1-SNAPSHOT</version>
+    </parent>
 
-  <artifactId>actorServiceProvider</artifactId>
+    <artifactId>actorServiceProvider</artifactId>
 
-  <dependencies>
-    <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <scope>test</scope>
-    </dependency>
-  </dependencies>
+    <dependencies>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>aai</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>events</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.common</groupId>
+            <artifactId>policy-endpoints</artifactId>
+            <version>${policy.common.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.powermock</groupId>
+            <artifactId>powermock-api-mockito2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
 </project>
index 8099361..2886b1f 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  * ActorService
  * ================================================================================
- * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved.
+ * Copyright (C) 2017-2018, 2020 AT&T Intellectual Property. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
 
 package org.onap.policy.controlloop.actorserviceprovider;
 
-import com.google.common.collect.ImmutableList;
-
-import java.util.Iterator;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.ServiceLoader;
-
+import java.util.Set;
+import org.onap.policy.common.parameters.BeanValidationResult;
+import org.onap.policy.controlloop.actorserviceprovider.impl.StartConfigPartial;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException;
 import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class ActorService {
-
+/**
+ * Service that manages a set of actors. To use the service, first invoke
+ * {@link #configure(Map)} to configure all of the actors, and then invoke
+ * {@link #start()} to start all of the actors. When finished using the actor service,
+ * invoke {@link #stop()} or {@link #shutdown()}.
+ */
+public class ActorService extends StartConfigPartial<Map<String, Object>> {
     private static final Logger logger = LoggerFactory.getLogger(ActorService.class);
-    private static ActorService service;
 
-    // USed to load actors
-    private final ServiceLoader<Actor> loader;
+    private final Map<String, Actor> name2actor;
+
+    private static class LazyHolder {
+        static final ActorService INSTANCE = new ActorService();
+    }
+
+    /**
+     * Constructs the object and loads the list of actors.
+     */
+    protected ActorService() {
+        super("actors");
+
+        Map<String, Actor> map = new HashMap<>();
+
+        for (Actor newActor : loadActors()) {
+            map.compute(newActor.getName(), (name, existingActor) -> {
+                if (existingActor == null) {
+                    return newActor;
+                }
+
+                logger.warn("duplicate actor names for {}: {}, ignoring {}", name,
+                                existingActor.getClass().getSimpleName(), newActor.getClass().getSimpleName());
+                return existingActor;
+            });
+        }
 
-    private ActorService() {
-        loader = ServiceLoader.load(Actor.class);
+        name2actor = ImmutableMap.copyOf(map);
     }
 
     /**
@@ -47,27 +77,115 @@ public class ActorService {
      *
      * @return the instance
      */
-    public static synchronized ActorService getInstance() {
-        if (service == null) {
-            service = new ActorService();
+    public static ActorService getInstance() {
+        return LazyHolder.INSTANCE;
+    }
+
+    /**
+     * Gets a particular actor.
+     *
+     * @param name name of the actor of interest
+     * @return the desired actor
+     * @throws IllegalArgumentException if no actor by the given name exists
+     */
+    public Actor getActor(String name) {
+        Actor actor = name2actor.get(name);
+        if (actor == null) {
+            throw new IllegalArgumentException("unknown actor " + name);
         }
-        return service;
+
+        return actor;
     }
 
     /**
-     * Get the actors.
+     * Gets the actors.
      *
      * @return the actors
      */
-    public ImmutableList<Actor> actors() {
-        Iterator<Actor> iter = loader.iterator();
-        logger.debug("returning actors");
-        while (iter.hasNext()) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("Got {}", iter.next().actor());
+    public Collection<Actor> getActors() {
+        return name2actor.values();
+    }
+
+    /**
+     * Gets the names of the actors.
+     *
+     * @return the actor names
+     */
+    public Set<String> getActorNames() {
+        return name2actor.keySet();
+    }
+
+    @Override
+    protected void doConfigure(Map<String, Object> parameters) {
+        logger.info("configuring actors");
+
+        BeanValidationResult valres = new BeanValidationResult("ActorService", parameters);
+
+        for (Actor actor : name2actor.values()) {
+            String actorName = actor.getName();
+            Map<String, Object> subparams = Util.translateToMap(actorName, parameters.get(actorName));
+
+            if (subparams != null) {
+
+                try {
+                    actor.configure(subparams);
+
+                } catch (ParameterValidationRuntimeException e) {
+                    logger.warn("failed to configure actor {}", actorName, e);
+                    valres.addResult(e.getResult());
+
+                } catch (RuntimeException e) {
+                    logger.warn("failed to configure actor {}", actorName, e);
+                }
+
+            } else if (actor.isConfigured()) {
+                logger.warn("missing configuration parameters for actor {}; using previous parameters", actorName);
+
+            } else {
+                logger.warn("missing configuration parameters for actor {}; actor cannot be started", actorName);
             }
         }
 
-        return ImmutableList.copyOf(loader.iterator());
+        if (!valres.isValid() && logger.isWarnEnabled()) {
+            logger.warn("actor services validation errors:\n{}", valres.getResult());
+        }
+    }
+
+    @Override
+    protected void doStart() {
+        logger.info("starting actors");
+
+        for (Actor actor : name2actor.values()) {
+            if (actor.isConfigured()) {
+                Util.runFunction(actor::start, "failed to start actor {}", actor.getName());
+
+            } else {
+                logger.warn("not starting unconfigured actor {}", actor.getName());
+            }
+        }
+    }
+
+    @Override
+    protected void doStop() {
+        logger.info("stopping actors");
+        name2actor.values()
+                        .forEach(actor -> Util.runFunction(actor::stop, "failed to stop actor {}", actor.getName()));
+    }
+
+    @Override
+    protected void doShutdown() {
+        logger.info("shutting down actors");
+
+        // @formatter:off
+        name2actor.values().forEach(
+            actor -> Util.runFunction(actor::shutdown, "failed to shutdown actor {}", actor.getName()));
+
+        // @formatter:on
+    }
+
+    // the following methods may be overridden by junit tests
+
+    protected Iterable<Actor> loadActors() {
+        return ServiceLoader.load(Actor.class);
     }
 }
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandler.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandler.java
new file mode 100644 (file)
index 0000000..d784038
--- /dev/null
@@ -0,0 +1,119 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import javax.ws.rs.client.InvocationCallback;
+import lombok.AccessLevel;
+import lombok.Getter;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler for a <i>single</i> asynchronous response.
+ *
+ * @param <T> response type
+ */
+@Getter
+public abstract class AsyncResponseHandler<T> implements InvocationCallback<T> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AsyncResponseHandler.class);
+
+    @Getter(AccessLevel.NONE)
+    private final PipelineControllerFuture<OperationOutcome> result = new PipelineControllerFuture<>();
+    private final ControlLoopOperationParams params;
+    private final OperationOutcome outcome;
+
+    /**
+     * Constructs the object.
+     *
+     * @param params operation parameters
+     * @param outcome outcome to be populated based on the response
+     */
+    public AsyncResponseHandler(ControlLoopOperationParams params, OperationOutcome outcome) {
+        this.params = params;
+        this.outcome = outcome;
+    }
+
+    /**
+     * Handles the given future, arranging to cancel it when the response is received.
+     *
+     * @param future future to be handled
+     * @return a future to be used to cancel or wait for the response
+     */
+    public CompletableFuture<OperationOutcome> handle(Future<T> future) {
+        result.add(future);
+        return result;
+    }
+
+    /**
+     * Invokes {@link #doComplete()} and then completes "this" with the returned value.
+     */
+    @Override
+    public void completed(T rawResponse) {
+        try {
+            logger.trace("{}.{}: response completed for {}", params.getActor(), params.getOperation(),
+                            params.getRequestId());
+            result.complete(doComplete(rawResponse));
+
+        } catch (RuntimeException e) {
+            logger.trace("{}.{}: response handler threw an exception for {}", params.getActor(), params.getOperation(),
+                            params.getRequestId());
+            result.completeExceptionally(e);
+        }
+    }
+
+    /**
+     * Invokes {@link #doFailed()} and then completes "this" with the returned value.
+     */
+    @Override
+    public void failed(Throwable throwable) {
+        try {
+            logger.trace("{}.{}: response failure for {}", params.getActor(), params.getOperation(),
+                            params.getRequestId());
+            result.complete(doFailed(throwable));
+
+        } catch (RuntimeException e) {
+            logger.trace("{}.{}: response failure handler threw an exception for {}", params.getActor(),
+                            params.getOperation(), params.getRequestId());
+            result.completeExceptionally(e);
+        }
+    }
+
+    /**
+     * Completes the processing of a response.
+     *
+     * @param rawResponse raw response that was received
+     * @return the outcome
+     */
+    protected abstract OperationOutcome doComplete(T rawResponse);
+
+    /**
+     * Handles a response exception.
+     *
+     * @param thrown exception that was thrown
+     * @return the outcome
+     */
+    protected abstract OperationOutcome doFailed(Throwable thrown);
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/CallbackManager.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/CallbackManager.java
new file mode 100644 (file)
index 0000000..7d7c1d9
--- /dev/null
@@ -0,0 +1,84 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import java.time.Instant;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Manager for "start" and "end" callbacks.
+ */
+public class CallbackManager implements Runnable {
+    private final AtomicReference<Instant> startTime = new AtomicReference<>();
+    private final AtomicReference<Instant> endTime = new AtomicReference<>();
+
+    /**
+     * Determines if the "start" callback can be invoked. If so, it sets the
+     * {@link #startTime} to the current time.
+     *
+     * @return {@code true} if the "start" callback can be invoked, {@code false}
+     *         otherwise
+     */
+    public boolean canStart() {
+        return startTime.compareAndSet(null, Instant.now());
+    }
+
+    /**
+     * Determines if the "end" callback can be invoked. If so, it sets the
+     * {@link #endTime} to the current time.
+     *
+     * @return {@code true} if the "end" callback can be invoked, {@code false}
+     *         otherwise
+     */
+    public boolean canEnd() {
+        return endTime.compareAndSet(null, Instant.now());
+    }
+
+    /**
+     * Gets the start time.
+     *
+     * @return the start time, or {@code null} if {@link #canStart()} has not been
+     *         invoked yet.
+     */
+    public Instant getStartTime() {
+        return startTime.get();
+    }
+
+    /**
+     * Gets the end time.
+     *
+     * @return the end time, or {@code null} if {@link #canEnd()} has not been invoked
+     *         yet.
+     */
+    public Instant getEndTime() {
+        return endTime.get();
+    }
+
+    /**
+     * Prevents further callbacks from being executed by setting {@link #startTime}
+     * and {@link #endTime}.
+     */
+    @Override
+    public void run() {
+        canStart();
+        canEnd();
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/DelayedIdentString.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/DelayedIdentString.java
new file mode 100644 (file)
index 0000000..b7a9a53
--- /dev/null
@@ -0,0 +1,65 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import lombok.AllArgsConstructor;
+
+/**
+ * Object whose {@link #toString()} method invokes {@link Object#toString()} on another
+ * object, on-demand. This assumes that the other object's method returns an object
+ * identifier. This is typically used to include an object's identifier in a log message.
+ */
+@AllArgsConstructor
+public class DelayedIdentString {
+    /**
+     * String to return for null objects or null object identifiers.
+     */
+    public static final String NULL_STRING = "null";
+
+    private final Object object;
+
+    /**
+     * Gets the object's identifier, after stripping anything appearing before '@'.
+     */
+    @Override
+    public String toString() {
+        if (object == null) {
+            return NULL_STRING;
+        }
+
+        String ident = objectToString();
+        if (ident == null) {
+            return NULL_STRING;
+        }
+
+        int index = ident.indexOf('@');
+        return (index > 0 ? ident.substring(index) : ident);
+    }
+
+    /**
+     * Invokes the object's {@link Object#toString()} method.
+     *
+     * @return the output from the object's {@link Object#toString()} method
+     */
+    protected String objectToString() {
+        return object.toString();
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/OperationOutcome.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/OperationOutcome.java
new file mode 100644 (file)
index 0000000..6b09248
--- /dev/null
@@ -0,0 +1,116 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import java.time.Instant;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.policy.PolicyResult;
+
+/**
+ * Outcome from an operation. Objects of this type are passed from one stage to the next.
+ */
+@Data
+@NoArgsConstructor
+public class OperationOutcome {
+    private String actor;
+    private String operation;
+    private String target;
+    private Instant start;
+    private Instant end;
+    private String subRequestId;
+    private PolicyResult result = PolicyResult.SUCCESS;
+    private String message;
+
+    /**
+     * Copy constructor.
+     *
+     * @param source source object from which to copy
+     */
+    public OperationOutcome(OperationOutcome source) {
+        this.actor = source.actor;
+        this.operation = source.operation;
+        this.target = source.target;
+        this.start = source.start;
+        this.end = source.end;
+        this.subRequestId = source.subRequestId;
+        this.result = source.result;
+        this.message = source.message;
+    }
+
+    /**
+     * Creates a {@link ControlLoopOperation}, populating all fields with the values from
+     * this object. Sets the outcome field to the string representation of this object's
+     * outcome.
+     *
+     * @return
+     */
+    public ControlLoopOperation toControlLoopOperation() {
+        ControlLoopOperation clo = new ControlLoopOperation();
+
+        clo.setActor(actor);
+        clo.setOperation(operation);
+        clo.setTarget(target);
+        clo.setStart(start);
+        clo.setEnd(end);
+        clo.setSubRequestId(subRequestId);
+        clo.setOutcome(result.toString());
+        clo.setMessage(message);
+
+        return clo;
+    }
+
+    /**
+     * Determines if this outcome is for the given actor and operation.
+     *
+     * @param actor actor name
+     * @param operation operation name
+     * @return {@code true} if this outcome is for the given actor and operation
+     */
+    public boolean isFor(@NonNull String actor, @NonNull String operation) {
+        // do the operation check first, as it's most likely to be unique
+        return (operation.equals(this.operation) && actor.equals(this.actor));
+    }
+
+    /**
+     * Determines if an outcome is for the given actor and operation.
+     *
+     * @param outcome outcome to be examined, or {@code null}
+     * @param actor actor name
+     * @param operation operation name
+     * @return {@code true} if this outcome is for the given actor and operation,
+     *         {@code false} it is {@code null} or not for the actor/operation
+     */
+    public static boolean isFor(OperationOutcome outcome, String actor, String operation) {
+        return (outcome != null && outcome.isFor(actor, operation));
+    }
+
+    /**
+     * Sets the result.
+     *
+     * @param result new result
+     */
+    public void setResult(@NonNull PolicyResult result) {
+        this.result = result;
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operator.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operator.java
new file mode 100644 (file)
index 0000000..c09460e
--- /dev/null
@@ -0,0 +1,57 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import org.onap.policy.common.capabilities.Configurable;
+import org.onap.policy.common.capabilities.Startable;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+
+/**
+ * This is the service interface for defining an Actor operation used in Control Loop
+ * Operational Policies for performing actions on runtime entities.
+ */
+public interface Operator extends Startable, Configurable<Map<String, Object>> {
+
+    /**
+     * Gets the name of the associated actor.
+     *
+     * @return the name of the associated actor
+     */
+    String getActorName();
+
+    /**
+     * Gets the name of the operation.
+     *
+     * @return the operation name
+     */
+    String getName();
+
+    /**
+     * Called by enforcement PDP engine to start the operation. As part of the operation,
+     * it invokes the "start" and "complete" call-backs found within the parameters.
+     *
+     * @param params parameters needed to start the operation
+     * @return a future that can be used to cancel or await the result of the operation
+     */
+    CompletableFuture<OperationOutcome> startOperation(ControlLoopOperationParams params);
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Util.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Util.java
new file mode 100644 (file)
index 0000000..c3ddd17
--- /dev/null
@@ -0,0 +1,193 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.common.endpoints.utils.NetLoggerUtil;
+import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType;
+import org.onap.policy.common.utils.coder.Coder;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Actor utilities.
+ */
+public class Util {
+    private static final Logger logger = LoggerFactory.getLogger(Util.class);
+
+    private Util() {
+        // do nothing
+    }
+
+    /**
+     * Extracts an object's identity by invoking {@link Object#toString()} and returning
+     * the portion starting with "@". Extraction is done on-demand, when toString() is
+     * called on the result. This is typically used when logging.
+     *
+     * @param object object whose identity is to be extracted
+     * @return an object that will extract the source object's identity when this object's
+     *         toString() method is called
+     */
+    public static Object ident(Object object) {
+        return new DelayedIdentString(object);
+    }
+
+    /**
+     * Logs a REST request. If the request is not of type, String, then it attempts to
+     * pretty-print it into JSON before logging.
+     *
+     * @param url request URL
+     * @param request request to be logged
+     */
+    public static <T> void logRestRequest(String url, T request) {
+        logRestRequest(new StandardCoder(), url, request);
+    }
+
+    /**
+     * Logs a REST request. If the request is not of type, String, then it attempts to
+     * pretty-print it into JSON before logging.
+     *
+     * @param coder coder to be used to pretty-print the request
+     * @param url request URL
+     * @param request request to be logged
+     */
+    protected static <T> void logRestRequest(Coder coder, String url, T request) {
+        String json;
+        try {
+            if (request instanceof String) {
+                json = request.toString();
+            } else {
+                json = coder.encode(request, true);
+            }
+
+        } catch (CoderException e) {
+            logger.warn("cannot pretty-print request", e);
+            json = request.toString();
+        }
+
+        NetLoggerUtil.log(EventType.OUT, CommInfrastructure.REST, url, json);
+        logger.info("[OUT|{}|{}|]{}{}", CommInfrastructure.REST, url, NetLoggerUtil.SYSTEM_LS, json);
+    }
+
+    /**
+     * Logs a REST response. If the response is not of type, String, then it attempts to
+     * pretty-print it into JSON before logging.
+     *
+     * @param url request URL
+     * @param response response to be logged
+     */
+    public static <T> void logRestResponse(String url, T response) {
+        logRestResponse(new StandardCoder(), url, response);
+    }
+
+    /**
+     * Logs a REST response. If the request is not of type, String, then it attempts to
+     * pretty-print it into JSON before logging.
+     *
+     * @param coder coder to be used to pretty-print the response
+     * @param url request URL
+     * @param response response to be logged
+     */
+    protected static <T> void logRestResponse(Coder coder, String url, T response) {
+        String json;
+        try {
+            if (response == null) {
+                json = null;
+            } else if (response instanceof String) {
+                json = response.toString();
+            } else {
+                json = coder.encode(response, true);
+            }
+
+        } catch (CoderException e) {
+            logger.warn("cannot pretty-print response", e);
+            json = response.toString();
+        }
+
+        NetLoggerUtil.log(EventType.IN, CommInfrastructure.REST, url, json);
+        logger.info("[IN|{}|{}|]{}{}", CommInfrastructure.REST, url, NetLoggerUtil.SYSTEM_LS, json);
+    }
+
+    /**
+     * Runs a function and logs a message if it throws an exception. Does <i>not</i>
+     * re-throw the exception.
+     *
+     * @param function function to be run
+     * @param exceptionMessage message to log if an exception is thrown
+     * @param exceptionArgs arguments to be passed to the logger
+     */
+    public static void runFunction(Runnable function, String exceptionMessage, Object... exceptionArgs) {
+        try {
+            function.run();
+
+        } catch (RuntimeException ex) {
+            // create a new array containing the original arguments plus the exception
+            Object[] allArgs = Arrays.copyOf(exceptionArgs, exceptionArgs.length + 1);
+            allArgs[exceptionArgs.length] = ex;
+
+            logger.warn(exceptionMessage, allArgs);
+        }
+    }
+
+    /**
+     * Translates parameters from one class to another, typically from a Map to a POJO or
+     * vice versa.
+     *
+     * @param identifier identifier of the actor/operation being translated; used to build
+     *        an exception message
+     * @param source source object to be translated
+     * @param clazz target class
+     * @return the translated object
+     */
+    public static <T> T translate(String identifier, Object source, Class<T> clazz) {
+        Coder coder = new StandardCoder();
+
+        try {
+            String json = coder.encode(source);
+            return coder.decode(json, clazz);
+
+        } catch (CoderException | RuntimeException e) {
+            throw new IllegalArgumentException("cannot translate parameters for " + identifier, e);
+        }
+    }
+
+    /**
+     * Translates parameters to a Map.
+     *
+     * @param identifier identifier of the actor/operation being translated; used to build
+     *        an exception message
+     * @param source source parameters
+     * @return the parameters, as a Map
+     */
+    @SuppressWarnings("unchecked")
+    public static Map<String, Object> translateToMap(String identifier, Object source) {
+        if (source == null) {
+            return null;
+        }
+
+        return translate(identifier, source, LinkedHashMap.class);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContext.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContext.java
new file mode 100644 (file)
index 0000000..cd4d257
--- /dev/null
@@ -0,0 +1,103 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.controlloop;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+
+/**
+ * Context associated with a control loop event.
+ */
+@Getter
+@Setter
+public class ControlLoopEventContext implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+
+    private final VirtualControlLoopEvent event;
+
+    /**
+     * Enrichment data extracted from the event. Never {@code null}, though it may be
+     * immutable.
+     */
+    private final Map<String, String> enrichment;
+
+    @Getter(AccessLevel.NONE)
+    @Setter(AccessLevel.NONE)
+    private Map<String, Serializable> properties = new ConcurrentHashMap<>();
+
+    /**
+     * Request ID extracted from the event, or a generated value if the event has no
+     * request id; never {@code null}.
+     */
+    private final UUID requestId;
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param event event with which this is associated
+     */
+    public ControlLoopEventContext(@NonNull VirtualControlLoopEvent event) {
+        this.event = event;
+        this.requestId = (event.getRequestId() != null ? event.getRequestId() : UUID.randomUUID());
+        this.enrichment = (event.getAai() != null ? event.getAai() : Map.of());
+    }
+
+    /**
+     * Determines if the context contains a property.
+     *
+     * @param name name of the property of interest
+     * @return {@code true} if the context contains the property, {@code false} otherwise
+     */
+    public boolean contains(String name) {
+        return properties.containsKey(name);
+    }
+
+    /**
+     * Gets a property, casting it to the desired type.
+     *
+     * @param <T> desired type
+     * @param name name of the property whose value is to be retrieved
+     * @return the property's value, or {@code null} if it does not yet have a value
+     */
+    @SuppressWarnings("unchecked")
+    public <T> T getProperty(String name) {
+        return (T) properties.get(name);
+    }
+
+    /**
+     * Sets a property's value.
+     *
+     * @param name property name
+     * @param value new property value
+     */
+    public void setProperty(String name, Serializable value) {
+        properties.put(name, value);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImpl.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImpl.java
new file mode 100644 (file)
index 0000000..d7f322e
--- /dev/null
@@ -0,0 +1,235 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import org.onap.policy.common.parameters.BeanValidationResult;
+import org.onap.policy.controlloop.actorserviceprovider.Operator;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException;
+import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of an actor.
+ */
+public class ActorImpl extends StartConfigPartial<Map<String, Object>> implements Actor {
+    private static final Logger logger = LoggerFactory.getLogger(ActorImpl.class);
+
+    /**
+     * Maps a name to an operator.
+     */
+    private final Map<String, Operator> name2operator = new ConcurrentHashMap<>();
+
+    /**
+     * Constructs the object.
+     *
+     * @param name actor name
+     */
+    public ActorImpl(String name) {
+        super(name);
+    }
+
+    /**
+     * Adds an operator supported by this actor.
+     *
+     * @param operator operation to be added
+     */
+    protected synchronized void addOperator(Operator operator) {
+        /*
+         * This method is "synchronized" to prevent the state from changing while the
+         * operator is added. The map, itself, does not need synchronization as it's a
+         * concurrent map.
+         */
+
+        if (isConfigured()) {
+            throw new IllegalStateException("attempt to set operators on a configured actor: " + getName());
+        }
+
+        name2operator.compute(operator.getName(), (opName, existingOp) -> {
+            if (existingOp == null) {
+                return operator;
+            }
+
+            logger.warn("duplicate names for actor operation {}.{}: {}, ignoring {}", getName(), opName,
+                            existingOp.getClass().getSimpleName(), operator.getClass().getSimpleName());
+            return existingOp;
+        });
+    }
+
+    @Override
+    public String getName() {
+        return getFullName();
+    }
+
+    @Override
+    public Operator getOperator(String name) {
+        Operator operator = name2operator.get(name);
+        if (operator == null) {
+            throw new IllegalArgumentException("unknown operation " + getName() + "." + name);
+        }
+
+        return operator;
+    }
+
+    @Override
+    public Collection<Operator> getOperators() {
+        return name2operator.values();
+    }
+
+    @Override
+    public Set<String> getOperationNames() {
+        return name2operator.keySet();
+    }
+
+    /**
+     * For each operation, it looks for a set of parameters by the same name and, if
+     * found, configures the operation with the parameters.
+     */
+    @Override
+    protected void doConfigure(Map<String, Object> parameters) {
+        final String actorName = getName();
+        logger.info("configuring operations for actor {}", actorName);
+
+        BeanValidationResult valres = new BeanValidationResult(actorName, parameters);
+
+        // function that creates operator-specific parameters, given the operation name
+        Function<String, Map<String, Object>> opParamsMaker = makeOperatorParameters(parameters);
+
+        for (Operator operator : name2operator.values()) {
+            String operName = operator.getName();
+            Map<String, Object> subparams = opParamsMaker.apply(operName);
+
+            if (subparams != null) {
+
+                try {
+                    operator.configure(subparams);
+
+                } catch (ParameterValidationRuntimeException e) {
+                    logger.warn("failed to configure operation {}.{}", actorName, operName, e);
+                    valres.addResult(e.getResult());
+
+                } catch (RuntimeException e) {
+                    logger.warn("failed to configure operation {}.{}", actorName, operName, e);
+                }
+
+            } else if (operator.isConfigured()) {
+                logger.warn("missing configuration parameters for operation {}.{}; using previous parameters",
+                                actorName, operName);
+
+            } else {
+                logger.warn("missing configuration parameters for operation {}.{}; operation cannot be started",
+                                actorName, operName);
+            }
+        }
+    }
+
+    /**
+     * Extracts the operator parameters from the actor parameters, for a given operator.
+     * This method assumes each operation has its own set of parameters.
+     *
+     * @param actorParameters actor parameters
+     * @return a function to extract the operator parameters from the actor parameters.
+     *         Note: this function may return {@code null} if there are no parameters for
+     *         the given operation name
+     */
+    protected Function<String, Map<String, Object>> makeOperatorParameters(Map<String, Object> actorParameters) {
+
+        return operName -> Util.translateToMap(getName() + "." + operName, actorParameters.get(operName));
+    }
+
+    /**
+     * Starts each operation.
+     */
+    @Override
+    protected void doStart() {
+        final String actorName = getName();
+        logger.info("starting operations for actor {}", actorName);
+
+        for (Operator oper : name2operator.values()) {
+            if (oper.isConfigured()) {
+                Util.runFunction(oper::start, "failed to start operation {}.{}", actorName, oper.getName());
+
+            } else {
+                logger.warn("not starting unconfigured operation {}.{}", actorName, oper.getName());
+            }
+        }
+    }
+
+    /**
+     * Stops each operation.
+     */
+    @Override
+    protected void doStop() {
+        final String actorName = getName();
+        logger.info("stopping operations for actor {}", actorName);
+
+        // @formatter:off
+        name2operator.values().forEach(
+            oper -> Util.runFunction(oper::stop, "failed to stop operation {}.{}", actorName, oper.getName()));
+        // @formatter:on
+    }
+
+    /**
+     * Shuts down each operation.
+     */
+    @Override
+    protected void doShutdown() {
+        final String actorName = getName();
+        logger.info("shutting down operations for actor {}", actorName);
+
+        // @formatter:off
+        name2operator.values().forEach(oper -> Util.runFunction(oper::shutdown,
+                        "failed to shutdown operation {}.{}", actorName, oper.getName()));
+        // @formatter:on
+    }
+
+    // TODO old code: remove lines down to **HERE**
+
+    @Override
+    public String actor() {
+        return null;
+    }
+
+    @Override
+    public List<String> recipes() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public List<String> recipeTargets(String recipe) {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public List<String> recipePayloads(String recipe) {
+        return Collections.emptyList();
+    }
+
+    // **HERE**
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActor.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActor.java
new file mode 100644 (file)
index 0000000..28b7b39
--- /dev/null
@@ -0,0 +1,58 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import java.util.Map;
+import java.util.function.Function;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpActorParams;
+
+/**
+ * Actor that uses HTTP, where the only additional property that an operator needs is a
+ * URL. The actor's parameters must be an {@link HttpActorParams} and its operator
+ * parameters are expected to be an {@link HttpParams}.
+ */
+public class HttpActor extends ActorImpl {
+
+    /**
+     * Constructs the object.
+     *
+     * @param name actor's name
+     */
+    public HttpActor(String name) {
+        super(name);
+    }
+
+    /**
+     * Translates the parameters to an {@link HttpActorParams} and then creates a function
+     * that will extract operator-specific parameters.
+     */
+    @Override
+    protected Function<String, Map<String, Object>> makeOperatorParameters(Map<String, Object> actorParameters) {
+        String actorName = getName();
+
+        // @formatter:off
+        return Util.translate(actorName, actorParameters, HttpActorParams.class)
+                        .doValidation(actorName)
+                        .makeOperationParameters(actorName);
+        // @formatter:on
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperator.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperator.java
new file mode 100644 (file)
index 0000000..5664929
--- /dev/null
@@ -0,0 +1,84 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.Getter;
+import org.onap.policy.common.endpoints.http.client.HttpClient;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactory;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException;
+
+/**
+ * Operator that uses HTTP. The operator's parameters must be a {@link HttpParams}.
+ */
+public class HttpOperator extends OperatorPartial {
+
+    @Getter(AccessLevel.PROTECTED)
+    private HttpClient client;
+
+    @Getter
+    private long timeoutSec;
+
+    /**
+     * URI path for this particular operation.
+     */
+    @Getter
+    private String path;
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param actorName name of the actor with which this operator is associated
+     * @param name operation name
+     */
+    public HttpOperator(String actorName, String name) {
+        super(actorName, name);
+    }
+
+    /**
+     * Translates the parameters to an {@link HttpParams} and then extracts the relevant
+     * values.
+     */
+    @Override
+    protected void doConfigure(Map<String, Object> parameters) {
+        HttpParams params = Util.translate(getFullName(), parameters, HttpParams.class);
+        ValidationResult result = params.validate(getFullName());
+        if (!result.isValid()) {
+            throw new ParameterValidationRuntimeException("invalid parameters", result);
+        }
+
+        client = getClientFactory().get(params.getClientName());
+        path = params.getPath();
+        timeoutSec = params.getTimeoutSec();
+    }
+
+    // these may be overridden by junits
+
+    protected HttpClientFactory getClientFactory() {
+        return HttpClientFactoryInstance.getClientFactory();
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartial.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartial.java
new file mode 100644 (file)
index 0000000..df5258d
--- /dev/null
@@ -0,0 +1,845 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.actorserviceprovider.CallbackManager;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.Operator;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Partial implementation of an operator. In general, it's preferable that subclasses
+ * would override
+ * {@link #startOperationAsync(ControlLoopOperationParams, int, OperationOutcome)
+ * startOperationAsync()}. However, if that proves to be too difficult, then they can
+ * simply override {@link #doOperation(ControlLoopOperationParams, int, OperationOutcome)
+ * doOperation()}. In addition, if the operation requires any preprocessor steps, the
+ * subclass may choose to override
+ * {@link #startPreprocessorAsync(ControlLoopOperationParams) startPreprocessorAsync()}.
+ * <p/>
+ * The futures returned by the methods within this class can be canceled, and will
+ * propagate the cancellation to any subtasks. Thus it is also expected that any futures
+ * returned by overridden methods will do the same. Of course, if a class overrides
+ * {@link #doOperation(ControlLoopOperationParams, int, OperationOutcome) doOperation()},
+ * then there's little that can be done to cancel that particular operation.
+ */
+public abstract class OperatorPartial extends StartConfigPartial<Map<String, Object>> implements Operator {
+
+    private static final Logger logger = LoggerFactory.getLogger(OperatorPartial.class);
+
+    /**
+     * Executor to be used for tasks that may perform blocking I/O. The default executor
+     * simply launches a new thread for each command that is submitted to it.
+     * <p/>
+     * May be overridden by junit tests.
+     */
+    @Getter(AccessLevel.PROTECTED)
+    @Setter(AccessLevel.PROTECTED)
+    private Executor blockingExecutor = command -> {
+        Thread thread = new Thread(command);
+        thread.setDaemon(true);
+        thread.start();
+    };
+
+    @Getter
+    private final String actorName;
+
+    @Getter
+    private final String name;
+
+    /**
+     * Constructs the object.
+     *
+     * @param actorName name of the actor with which this operator is associated
+     * @param name operation name
+     */
+    public OperatorPartial(String actorName, String name) {
+        super(actorName + "." + name);
+        this.actorName = actorName;
+        this.name = name;
+    }
+
+    /**
+     * This method does nothing.
+     */
+    @Override
+    protected void doConfigure(Map<String, Object> parameters) {
+        // do nothing
+    }
+
+    /**
+     * This method does nothing.
+     */
+    @Override
+    protected void doStart() {
+        // do nothing
+    }
+
+    /**
+     * This method does nothing.
+     */
+    @Override
+    protected void doStop() {
+        // do nothing
+    }
+
+    /**
+     * This method does nothing.
+     */
+    @Override
+    protected void doShutdown() {
+        // do nothing
+    }
+
+    @Override
+    public final CompletableFuture<OperationOutcome> startOperation(ControlLoopOperationParams params) {
+        if (!isAlive()) {
+            throw new IllegalStateException("operation is not running: " + getFullName());
+        }
+
+        // allocate a controller for the entire operation
+        final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        CompletableFuture<OperationOutcome> preproc = startPreprocessorAsync(params);
+        if (preproc == null) {
+            // no preprocessor required - just start the operation
+            return startOperationAttempt(params, controller, 1);
+        }
+
+        /*
+         * Do preprocessor first and then, if successful, start the operation. Note:
+         * operations create their own outcome, ignoring the outcome from any previous
+         * steps.
+         *
+         * Wrap the preprocessor to ensure "stop" is propagated to it.
+         */
+        // @formatter:off
+        controller.wrap(preproc)
+                        .exceptionally(fromException(params, "preprocessor of operation"))
+                        .thenCompose(handlePreprocessorFailure(params, controller))
+                        .thenCompose(unusedOutcome -> startOperationAttempt(params, controller, 1));
+        // @formatter:on
+
+        return controller;
+    }
+
+    /**
+     * Handles a failure in the preprocessor pipeline. If a failure occurred, then it
+     * invokes the call-backs, marks the controller complete, and returns an incomplete
+     * future, effectively halting the pipeline. Otherwise, it returns the outcome that it
+     * received.
+     * <p/>
+     * Assumes that no callbacks have been invoked yet.
+     *
+     * @param params operation parameters
+     * @param controller pipeline controller
+     * @return a function that checks the outcome status and continues, if successful, or
+     *         indicates a failure otherwise
+     */
+    private Function<OperationOutcome, CompletableFuture<OperationOutcome>> handlePreprocessorFailure(
+                    ControlLoopOperationParams params, PipelineControllerFuture<OperationOutcome> controller) {
+
+        return outcome -> {
+
+            if (outcome != null && isSuccess(outcome)) {
+                logger.trace("{}: preprocessor succeeded for {}", getFullName(), params.getRequestId());
+                return CompletableFuture.completedFuture(outcome);
+            }
+
+            logger.warn("preprocessor failed, discontinuing operation {} for {}", getFullName(), params.getRequestId());
+
+            final Executor executor = params.getExecutor();
+            final CallbackManager callbacks = new CallbackManager();
+
+            // propagate "stop" to the callbacks
+            controller.add(callbacks);
+
+            final OperationOutcome outcome2 = params.makeOutcome();
+
+            // TODO need a FAILURE_MISSING_DATA (e.g., A&AI)
+
+            outcome2.setResult(PolicyResult.FAILURE_GUARD);
+            outcome2.setMessage(outcome != null ? outcome.getMessage() : null);
+
+            // @formatter:off
+            CompletableFuture.completedFuture(outcome2)
+                            .whenCompleteAsync(callbackStarted(params, callbacks), executor)
+                            .whenCompleteAsync(callbackCompleted(params, callbacks), executor)
+                            .whenCompleteAsync(controller.delayedComplete(), executor);
+            // @formatter:on
+
+            return new CompletableFuture<>();
+        };
+    }
+
+    /**
+     * Invokes the operation's preprocessor step(s) as a "future". This method simply
+     * returns {@code null}.
+     * <p/>
+     * This method assumes the following:
+     * <ul>
+     * <li>the operator is alive</li>
+     * <li>exceptions generated within the pipeline will be handled by the invoker</li>
+     * </ul>
+     *
+     * @param params operation parameters
+     * @return a function that will start the preprocessor and returns its outcome, or
+     *         {@code null} if this operation needs no preprocessor
+     */
+    protected CompletableFuture<OperationOutcome> startPreprocessorAsync(ControlLoopOperationParams params) {
+        return null;
+    }
+
+    /**
+     * Starts the operation attempt, with no preprocessor. When all retries complete, it
+     * will complete the controller.
+     *
+     * @param params operation parameters
+     * @param controller controller for all operation attempts
+     * @param attempt attempt number, typically starting with 1
+     * @return a future that will return the final result of all attempts
+     */
+    private CompletableFuture<OperationOutcome> startOperationAttempt(ControlLoopOperationParams params,
+                    PipelineControllerFuture<OperationOutcome> controller, int attempt) {
+
+        // propagate "stop" to the operation attempt
+        controller.wrap(startAttemptWithoutRetries(params, attempt))
+                        .thenCompose(retryOnFailure(params, controller, attempt));
+
+        return controller;
+    }
+
+    /**
+     * Starts the operation attempt, without doing any retries.
+     *
+     * @param params operation parameters
+     * @param attempt attempt number, typically starting with 1
+     * @return a future that will return the result of a single operation attempt
+     */
+    private CompletableFuture<OperationOutcome> startAttemptWithoutRetries(ControlLoopOperationParams params,
+                    int attempt) {
+
+        logger.info("{}: start operation attempt {} for {}", getFullName(), attempt, params.getRequestId());
+
+        final Executor executor = params.getExecutor();
+        final OperationOutcome outcome = params.makeOutcome();
+        final CallbackManager callbacks = new CallbackManager();
+
+        // this operation attempt gets its own controller
+        final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        // propagate "stop" to the callbacks
+        controller.add(callbacks);
+
+        // @formatter:off
+        CompletableFuture<OperationOutcome> future = CompletableFuture.completedFuture(outcome)
+                        .whenCompleteAsync(callbackStarted(params, callbacks), executor)
+                        .thenCompose(controller.wrap(outcome2 -> startOperationAsync(params, attempt, outcome2)));
+        // @formatter:on
+
+        // handle timeouts, if specified
+        long timeoutMillis = getTimeOutMillis(params.getTimeoutSec());
+        if (timeoutMillis > 0) {
+            logger.info("{}: set timeout to {}ms for {}", getFullName(), timeoutMillis, params.getRequestId());
+            future = future.orTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
+        }
+
+        /*
+         * Note: we re-invoke callbackStarted() just to be sure the callback is invoked
+         * before callbackCompleted() is invoked.
+         *
+         * Note: no need to remove "callbacks" from the pipeline, as we're going to stop
+         * the pipeline as the last step anyway.
+         */
+
+        // @formatter:off
+        future.exceptionally(fromException(params, "operation"))
+                    .thenApply(setRetryFlag(params, attempt))
+                    .whenCompleteAsync(callbackStarted(params, callbacks), executor)
+                    .whenCompleteAsync(callbackCompleted(params, callbacks), executor)
+                    .whenCompleteAsync(controller.delayedComplete(), executor);
+        // @formatter:on
+
+        return controller;
+    }
+
+    /**
+     * Determines if the outcome was successful.
+     *
+     * @param outcome outcome to examine
+     * @return {@code true} if the outcome was successful
+     */
+    protected boolean isSuccess(OperationOutcome outcome) {
+        return (outcome.getResult() == PolicyResult.SUCCESS);
+    }
+
+    /**
+     * Determines if the outcome was a failure for this operator.
+     *
+     * @param outcome outcome to examine, or {@code null}
+     * @return {@code true} if the outcome is not {@code null} and was a failure
+     *         <i>and</i> was associated with this operator, {@code false} otherwise
+     */
+    protected boolean isActorFailed(OperationOutcome outcome) {
+        return (isSameOperation(outcome) && outcome.getResult() == PolicyResult.FAILURE);
+    }
+
+    /**
+     * Determines if the given outcome is for this operation.
+     *
+     * @param outcome outcome to examine
+     * @return {@code true} if the outcome is for this operation, {@code false} otherwise
+     */
+    protected boolean isSameOperation(OperationOutcome outcome) {
+        return OperationOutcome.isFor(outcome, getActorName(), getName());
+    }
+
+    /**
+     * Invokes the operation as a "future". This method simply invokes
+     * {@link #doOperation(ControlLoopOperationParams)} using the {@link #blockingExecutor
+     * "blocking executor"}, returning the result via a "future".
+     * <p/>
+     * Note: if the operation uses blocking I/O, then it should <i>not</i> be run using
+     * the executor in the "params", as that may bring the background thread pool to a
+     * grinding halt. The {@link #blockingExecutor "blocking executor"} should be used
+     * instead.
+     * <p/>
+     * This method assumes the following:
+     * <ul>
+     * <li>the operator is alive</li>
+     * <li>verifyRunning() has been invoked</li>
+     * <li>callbackStarted() has been invoked</li>
+     * <li>the invoker will perform appropriate timeout checks</li>
+     * <li>exceptions generated within the pipeline will be handled by the invoker</li>
+     * </ul>
+     *
+     * @param params operation parameters
+     * @param attempt attempt number, typically starting with 1
+     * @return a function that will start the operation and return its result when
+     *         complete
+     */
+    protected CompletableFuture<OperationOutcome> startOperationAsync(ControlLoopOperationParams params, int attempt,
+                    OperationOutcome outcome) {
+
+        return CompletableFuture.supplyAsync(() -> doOperation(params, attempt, outcome), getBlockingExecutor());
+    }
+
+    /**
+     * Low-level method that performs the operation. This can make the same assumptions
+     * that are made by {@link #doOperationAsFuture(ControlLoopOperationParams)}. This
+     * particular method simply throws an {@link UnsupportedOperationException}.
+     *
+     * @param params operation parameters
+     * @param attempt attempt number, typically starting with 1
+     * @param operation the operation being performed
+     * @return the outcome of the operation
+     */
+    protected OperationOutcome doOperation(ControlLoopOperationParams params, int attempt, OperationOutcome operation) {
+
+        throw new UnsupportedOperationException("start operation " + getFullName());
+    }
+
+    /**
+     * Sets the outcome status to FAILURE_RETRIES, if the current operation outcome is
+     * FAILURE, assuming the policy specifies retries and the retry count has been
+     * exhausted.
+     *
+     * @param params operation parameters
+     * @param attempt latest attempt number, starting with 1
+     * @return a function to get the next future to execute
+     */
+    private Function<OperationOutcome, OperationOutcome> setRetryFlag(ControlLoopOperationParams params, int attempt) {
+
+        return operation -> {
+            if (operation != null && !isActorFailed(operation)) {
+                /*
+                 * wrong type or wrong operation - just leave it as is. No need to log
+                 * anything here, as retryOnFailure() will log a message
+                 */
+                return operation;
+            }
+
+            // get a non-null operation
+            OperationOutcome oper2;
+            if (operation != null) {
+                oper2 = operation;
+            } else {
+                oper2 = params.makeOutcome();
+                oper2.setResult(PolicyResult.FAILURE);
+            }
+
+            Integer retry = params.getRetry();
+            if (retry != null && retry > 0 && attempt > retry) {
+                /*
+                 * retries were specified and we've already tried them all - change to
+                 * FAILURE_RETRIES
+                 */
+                logger.info("operation {} retries exhausted for {}", getFullName(), params.getRequestId());
+                oper2.setResult(PolicyResult.FAILURE_RETRIES);
+            }
+
+            return oper2;
+        };
+    }
+
+    /**
+     * Restarts the operation if it was a FAILURE. Assumes that
+     * {@link #setRetryFlag(ControlLoopOperationParams, int)} was previously invoked, and
+     * thus that the "operation" is not {@code null}.
+     *
+     * @param params operation parameters
+     * @param controller controller for all of the retries
+     * @param attempt latest attempt number, starting with 1
+     * @return a function to get the next future to execute
+     */
+    private Function<OperationOutcome, CompletableFuture<OperationOutcome>> retryOnFailure(
+                    ControlLoopOperationParams params, PipelineControllerFuture<OperationOutcome> controller,
+                    int attempt) {
+
+        return operation -> {
+            if (!isActorFailed(operation)) {
+                // wrong type or wrong operation - just leave it as is
+                logger.trace("not retrying operation {} for {}", getFullName(), params.getRequestId());
+                controller.complete(operation);
+                return new CompletableFuture<>();
+            }
+
+            Integer retry = params.getRetry();
+            if (retry == null || retry <= 0) {
+                // no retries - already marked as FAILURE, so just return it
+                logger.info("operation {} no retries for {}", getFullName(), params.getRequestId());
+                controller.complete(operation);
+                return new CompletableFuture<>();
+            }
+
+
+            /*
+             * Retry the operation.
+             */
+            logger.info("retry operation {} for {}", getFullName(), params.getRequestId());
+
+            return startOperationAttempt(params, controller, attempt + 1);
+        };
+    }
+
+    /**
+     * Converts an exception into an operation outcome, returning a copy of the outcome to
+     * prevent background jobs from changing it.
+     *
+     * @param params operation parameters
+     * @param type type of item throwing the exception
+     * @return a function that will convert an exception into an operation outcome
+     */
+    private Function<Throwable, OperationOutcome> fromException(ControlLoopOperationParams params, String type) {
+
+        return thrown -> {
+            OperationOutcome outcome = params.makeOutcome();
+
+            logger.warn("exception throw by {} {}.{} for {}", type, outcome.getActor(), outcome.getOperation(),
+                            params.getRequestId(), thrown);
+
+            return setOutcome(params, outcome, thrown);
+        };
+    }
+
+    /**
+     * Similar to {@link CompletableFuture#anyOf(CompletableFuture...)}, but it cancels
+     * any outstanding futures when one completes.
+     *
+     * @param params operation parameters
+     * @param futures futures for which to wait
+     * @return a future to cancel or await an outcome. If this future is canceled, then
+     *         all of the futures will be canceled
+     */
+    protected CompletableFuture<OperationOutcome> anyOf(ControlLoopOperationParams params,
+                    List<CompletableFuture<OperationOutcome>> futures) {
+
+        // convert list to an array
+        @SuppressWarnings("rawtypes")
+        CompletableFuture[] arrFutures = futures.toArray(new CompletableFuture[futures.size()]);
+
+        @SuppressWarnings("unchecked")
+        CompletableFuture<OperationOutcome> result = anyOf(params, arrFutures);
+        return result;
+    }
+
+    /**
+     * Same as {@link CompletableFuture#anyOf(CompletableFuture...)}, but it cancels any
+     * outstanding futures when one completes.
+     *
+     * @param params operation parameters
+     * @param futures futures for which to wait
+     * @return a future to cancel or await an outcome. If this future is canceled, then
+     *         all of the futures will be canceled
+     */
+    protected CompletableFuture<OperationOutcome> anyOf(ControlLoopOperationParams params,
+                    @SuppressWarnings("unchecked") CompletableFuture<OperationOutcome>... futures) {
+
+        final Executor executor = params.getExecutor();
+        final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        attachFutures(controller, futures);
+
+        // @formatter:off
+        CompletableFuture.anyOf(futures)
+                            .thenApply(object -> (OperationOutcome) object)
+                            .whenCompleteAsync(controller.delayedComplete(), executor);
+        // @formatter:on
+
+        return controller;
+    }
+
+    /**
+     * Similar to {@link CompletableFuture#allOf(CompletableFuture...)}, but it cancels
+     * the futures if returned future is canceled. The future returns the "worst" outcome,
+     * based on priority (see {@link #detmPriority(OperationOutcome)}).
+     *
+     * @param params operation parameters
+     * @param futures futures for which to wait
+     * @return a future to cancel or await an outcome. If this future is canceled, then
+     *         all of the futures will be canceled
+     */
+    protected CompletableFuture<OperationOutcome> allOf(ControlLoopOperationParams params,
+                    List<CompletableFuture<OperationOutcome>> futures) {
+
+        // convert list to an array
+        @SuppressWarnings("rawtypes")
+        CompletableFuture[] arrFutures = futures.toArray(new CompletableFuture[futures.size()]);
+
+        @SuppressWarnings("unchecked")
+        CompletableFuture<OperationOutcome> result = allOf(params, arrFutures);
+        return result;
+    }
+
+    /**
+     * Same as {@link CompletableFuture#allOf(CompletableFuture...)}, but it cancels the
+     * futures if returned future is canceled. The future returns the "worst" outcome,
+     * based on priority (see {@link #detmPriority(OperationOutcome)}).
+     *
+     * @param params operation parameters
+     * @param futures futures for which to wait
+     * @return a future to cancel or await an outcome. If this future is canceled, then
+     *         all of the futures will be canceled
+     */
+    protected CompletableFuture<OperationOutcome> allOf(ControlLoopOperationParams params,
+                    @SuppressWarnings("unchecked") CompletableFuture<OperationOutcome>... futures) {
+
+        final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        attachFutures(controller, futures);
+
+        OperationOutcome[] outcomes = new OperationOutcome[futures.length];
+
+        @SuppressWarnings("rawtypes")
+        CompletableFuture[] futures2 = new CompletableFuture[futures.length];
+
+        // record the outcomes of each future when it completes
+        for (int count = 0; count < futures2.length; ++count) {
+            final int count2 = count;
+            futures2[count] = futures[count].whenComplete((outcome2, thrown) -> outcomes[count2] = outcome2);
+        }
+
+        CompletableFuture.allOf(futures2).whenComplete(combineOutcomes(params, controller, outcomes));
+
+        return controller;
+    }
+
+    /**
+     * Attaches the given futures to the controller.
+     *
+     * @param controller master controller for all of the futures
+     * @param futures futures to be attached to the controller
+     */
+    private void attachFutures(PipelineControllerFuture<OperationOutcome> controller,
+                    @SuppressWarnings("unchecked") CompletableFuture<OperationOutcome>... futures) {
+
+        // attach each task
+        for (CompletableFuture<OperationOutcome> future : futures) {
+            controller.add(future);
+        }
+    }
+
+    /**
+     * Combines the outcomes from a set of tasks.
+     *
+     * @param params operation parameters
+     * @param future future to be completed with the combined result
+     * @param outcomes outcomes to be examined
+     */
+    private BiConsumer<Void, Throwable> combineOutcomes(ControlLoopOperationParams params,
+                    CompletableFuture<OperationOutcome> future, OperationOutcome[] outcomes) {
+
+        return (unused, thrown) -> {
+            if (thrown != null) {
+                future.completeExceptionally(thrown);
+                return;
+            }
+
+            // identify the outcome with the highest priority
+            OperationOutcome outcome = outcomes[0];
+            int priority = detmPriority(outcome);
+
+            // start with "1", as we've already dealt with "0"
+            for (int count = 1; count < outcomes.length; ++count) {
+                OperationOutcome outcome2 = outcomes[count];
+                int priority2 = detmPriority(outcome2);
+
+                if (priority2 > priority) {
+                    outcome = outcome2;
+                    priority = priority2;
+                }
+            }
+
+            logger.trace("{}: combined outcome of tasks is {} for {}", getFullName(),
+                            (outcome == null ? null : outcome.getResult()), params.getRequestId());
+
+            future.complete(outcome);
+        };
+    }
+
+    /**
+     * Determines the priority of an outcome based on its result.
+     *
+     * @param outcome outcome to examine, or {@code null}
+     * @return the outcome's priority
+     */
+    protected int detmPriority(OperationOutcome outcome) {
+        if (outcome == null) {
+            return 1;
+        }
+
+        switch (outcome.getResult()) {
+            case SUCCESS:
+                return 0;
+
+            case FAILURE_GUARD:
+                return 2;
+
+            case FAILURE_RETRIES:
+                return 3;
+
+            case FAILURE:
+                return 4;
+
+            case FAILURE_TIMEOUT:
+                return 5;
+
+            case FAILURE_EXCEPTION:
+            default:
+                return 6;
+        }
+    }
+
+    /**
+     * Performs a task, after verifying that the controller is still running. Also checks
+     * that the previous outcome was successful, if specified.
+     *
+     * @param params operation parameters
+     * @param controller overall pipeline controller
+     * @param checkSuccess {@code true} to check the previous outcome, {@code false}
+     *        otherwise
+     * @param outcome outcome of the previous task
+     * @param tasks tasks to be performed
+     * @return a function to perform the task. If everything checks out, then it returns
+     *         the task's future. Otherwise, it returns an incomplete future and completes
+     *         the controller instead.
+     */
+    // @formatter:off
+    protected CompletableFuture<OperationOutcome> doTask(ControlLoopOperationParams params,
+                    PipelineControllerFuture<OperationOutcome> controller,
+                    boolean checkSuccess, OperationOutcome outcome,
+                    CompletableFuture<OperationOutcome> task) {
+        // @formatter:on
+
+        if (checkSuccess && !isSuccess(outcome)) {
+            /*
+             * must complete before canceling so that cancel() doesn't cause controller to
+             * complete
+             */
+            controller.complete(outcome);
+            task.cancel(false);
+            return new CompletableFuture<>();
+        }
+
+        return controller.wrap(task);
+    }
+
+    /**
+     * Performs a task, after verifying that the controller is still running. Also checks
+     * that the previous outcome was successful, if specified.
+     *
+     * @param params operation parameters
+     * @param controller overall pipeline controller
+     * @param checkSuccess {@code true} to check the previous outcome, {@code false}
+     *        otherwise
+     * @param tasks tasks to be performed
+     * @return a function to perform the task. If everything checks out, then it returns
+     *         the task's future. Otherwise, it returns an incomplete future and completes
+     *         the controller instead.
+     */
+    // @formatter:off
+    protected Function<OperationOutcome, CompletableFuture<OperationOutcome>> doTask(ControlLoopOperationParams params,
+                    PipelineControllerFuture<OperationOutcome> controller,
+                    boolean checkSuccess,
+                    Function<OperationOutcome, CompletableFuture<OperationOutcome>> task) {
+        // @formatter:on
+
+        return outcome -> {
+
+            if (!controller.isRunning()) {
+                return new CompletableFuture<>();
+            }
+
+            if (checkSuccess && !isSuccess(outcome)) {
+                controller.complete(outcome);
+                return new CompletableFuture<>();
+            }
+
+            return controller.wrap(task.apply(outcome));
+        };
+    }
+
+    /**
+     * Sets the start time of the operation and invokes the callback to indicate that the
+     * operation has started. Does nothing if the pipeline has been stopped.
+     * <p/>
+     * This assumes that the "outcome" is not {@code null}.
+     *
+     * @param params operation parameters
+     * @param callbacks used to determine if the start callback can be invoked
+     * @return a function that sets the start time and invokes the callback
+     */
+    private BiConsumer<OperationOutcome, Throwable> callbackStarted(ControlLoopOperationParams params,
+                    CallbackManager callbacks) {
+
+        return (outcome, thrown) -> {
+
+            if (callbacks.canStart()) {
+                // haven't invoked "start" callback yet
+                outcome.setStart(callbacks.getStartTime());
+                outcome.setEnd(null);
+                params.callbackStarted(outcome);
+            }
+        };
+    }
+
+    /**
+     * Sets the end time of the operation and invokes the callback to indicate that the
+     * operation has completed. Does nothing if the pipeline has been stopped.
+     * <p/>
+     * This assumes that the "outcome" is not {@code null}.
+     * <p/>
+     * Note: the start time must be a reference rather than a plain value, because it's
+     * value must be gotten on-demand, when the returned function is executed at a later
+     * time.
+     *
+     * @param params operation parameters
+     * @param callbacks used to determine if the end callback can be invoked
+     * @return a function that sets the end time and invokes the callback
+     */
+    private BiConsumer<OperationOutcome, Throwable> callbackCompleted(ControlLoopOperationParams params,
+                    CallbackManager callbacks) {
+
+        return (outcome, thrown) -> {
+
+            if (callbacks.canEnd()) {
+                outcome.setStart(callbacks.getStartTime());
+                outcome.setEnd(callbacks.getEndTime());
+                params.callbackCompleted(outcome);
+            }
+        };
+    }
+
+    /**
+     * Sets an operation's outcome and message, based on a throwable.
+     *
+     * @param params operation parameters
+     * @param operation operation to be updated
+     * @return the updated operation
+     */
+    protected OperationOutcome setOutcome(ControlLoopOperationParams params, OperationOutcome operation,
+                    Throwable thrown) {
+        PolicyResult result = (isTimeout(thrown) ? PolicyResult.FAILURE_TIMEOUT : PolicyResult.FAILURE_EXCEPTION);
+        return setOutcome(params, operation, result);
+    }
+
+    /**
+     * Sets an operation's outcome and default message based on the result.
+     *
+     * @param params operation parameters
+     * @param operation operation to be updated
+     * @param result result of the operation
+     * @return the updated operation
+     */
+    protected OperationOutcome setOutcome(ControlLoopOperationParams params, OperationOutcome operation,
+                    PolicyResult result) {
+        logger.trace("{}: set outcome {} for {}", getFullName(), result, params.getRequestId());
+        operation.setResult(result);
+        operation.setMessage(result == PolicyResult.SUCCESS ? ControlLoopOperation.SUCCESS_MSG
+                        : ControlLoopOperation.FAILED_MSG);
+
+        return operation;
+    }
+
+    /**
+     * Determines if a throwable is due to a timeout.
+     *
+     * @param thrown throwable of interest
+     * @return {@code true} if the throwable is due to a timeout, {@code false} otherwise
+     */
+    protected boolean isTimeout(Throwable thrown) {
+        if (thrown instanceof CompletionException) {
+            thrown = thrown.getCause();
+        }
+
+        return (thrown instanceof TimeoutException);
+    }
+
+    // these may be overridden by junit tests
+
+    /**
+     * Gets the operation timeout. Subclasses may override this method to obtain the
+     * timeout in some other way (e.g., through configuration properties).
+     *
+     * @param timeoutSec timeout, in seconds, or {@code null}
+     * @return the operation timeout, in milliseconds
+     */
+    protected long getTimeOutMillis(Integer timeoutSec) {
+        return (timeoutSec == null ? 0 : TimeUnit.MILLISECONDS.convert(timeoutSec, TimeUnit.SECONDS));
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/StartConfigPartial.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/StartConfigPartial.java
new file mode 100644 (file)
index 0000000..6c883f1
--- /dev/null
@@ -0,0 +1,153 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import lombok.Getter;
+import org.onap.policy.common.capabilities.Configurable;
+import org.onap.policy.common.capabilities.Startable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Partial implementation of an object that is both startable and configurable. It
+ * provides the high level methods defined in the interface, while deferring the details
+ * to abstract methods that must be provided by the subclasses. It also manages the
+ * current {@link #state}.
+ *
+ * @param <T> type of parameters expected by {@link #configure(Object)}
+ */
+public abstract class StartConfigPartial<T> implements Startable, Configurable<T> {
+    private static final Logger logger = LoggerFactory.getLogger(StartConfigPartial.class);
+
+    @Getter
+    private final String fullName;
+
+    public enum State {
+        IDLE, CONFIGURED, ALIVE
+    }
+
+    private State state = State.IDLE;
+
+    /**
+     * Constructs the object.
+     *
+     * @param fullName full name of this object, used for logging and exception purposes
+     */
+    public StartConfigPartial(String fullName) {
+        this.fullName = fullName;
+    }
+
+    @Override
+    public synchronized boolean isAlive() {
+        return (state == State.ALIVE);
+    }
+
+    /**
+     * Determines if this object has been configured.
+     *
+     * @return {@code true} if this object has been configured, {@code false} otherwise
+     */
+    public synchronized boolean isConfigured() {
+        return (state != State.IDLE);
+    }
+
+    @Override
+    public synchronized void configure(T parameters) {
+        if (isAlive()) {
+            throw new IllegalStateException("attempt to reconfigure, but already running " + getFullName());
+        }
+
+        logger.info("initializing {}", getFullName());
+
+        doConfigure(parameters);
+
+        state = State.CONFIGURED;
+    }
+
+    @Override
+    public synchronized boolean start() {
+        switch (state) {
+            case ALIVE:
+                logger.info("{} is already running", getFullName());
+                break;
+
+            case CONFIGURED:
+                logger.info("starting {}", getFullName());
+                doStart();
+                state = State.ALIVE;
+                break;
+
+            case IDLE:
+            default:
+                throw new IllegalStateException("attempt to start unconfigured " + getFullName());
+        }
+
+        return true;
+    }
+
+    @Override
+    public synchronized boolean stop() {
+        if (isAlive()) {
+            logger.info("stopping {}", getFullName());
+            state = State.CONFIGURED;
+            doStop();
+
+        } else {
+            logger.info("{} is not running", getFullName());
+        }
+
+        return true;
+    }
+
+    @Override
+    public synchronized void shutdown() {
+        if (!isAlive()) {
+            logger.info("{} is not running", getFullName());
+            return;
+        }
+
+        logger.info("shutting down actor {}", getFullName());
+        state = State.CONFIGURED;
+        doShutdown();
+    }
+
+    /**
+     * Configures this object.
+     *
+     * @param parameters configuration parameters
+     */
+    protected abstract void doConfigure(T parameters);
+
+    /**
+     * Starts this object.
+     */
+    protected abstract void doStart();
+
+    /**
+     * Stops this object.
+     */
+    protected abstract void doStop();
+
+    /**
+     * Shuts down this object.
+     */
+    protected abstract void doShutdown();
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParams.java
new file mode 100644 (file)
index 0000000..57fce40
--- /dev/null
@@ -0,0 +1,218 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ForkJoinPool;
+import java.util.function.Consumer;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import org.onap.policy.common.parameters.BeanValidationResult;
+import org.onap.policy.common.parameters.BeanValidator;
+import org.onap.policy.common.parameters.annotations.NotNull;
+import org.onap.policy.controlloop.actorserviceprovider.ActorService;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.policy.Target;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Parameters for control loop operations. The executor defaults to
+ * {@link ForkJoinPool#commonPool()}, but may be overridden.
+ */
+@Getter
+@Builder(toBuilder = true)
+@AllArgsConstructor
+@EqualsAndHashCode
+public class ControlLoopOperationParams {
+    private static final Logger logger = LoggerFactory.getLogger(ControlLoopOperationParams.class);
+
+    /**
+     * Actor name.
+     */
+    @NotNull
+    private String actor;
+
+    /**
+     * Actor service in which to find the actor/operation.
+     */
+    @NotNull
+    private ActorService actorService;
+
+    /**
+     * Event for which the operation applies.
+     */
+    @NotNull
+    private ControlLoopEventContext context;
+
+    /**
+     * Executor to use to run the operation.
+     */
+    @NotNull
+    @Builder.Default
+    private Executor executor = ForkJoinPool.commonPool();
+
+    /**
+     * Operation name.
+     */
+    @NotNull
+    private String operation;
+
+    /**
+     * Payload data for the request.
+     */
+    private Map<String, String> payload;
+
+    /**
+     * Number of retries allowed, or {@code null} if no retries.
+     */
+    private Integer retry;
+
+    /**
+     * The entity's target information. May be {@code null}, depending on the requirement
+     * of the operation to be invoked.
+     */
+    private Target target;
+
+    /**
+     * Target entity.
+     */
+    @NotNull
+    private String targetEntity;
+
+    /**
+     * Timeout, in seconds, or {@code null} if no timeout. Zero and negative values also
+     * imply no timeout.
+     */
+    @Builder.Default
+    private Integer timeoutSec = 300;
+
+    /**
+     * The function to invoke when the operation starts. This is optional.
+     * <p/>
+     * Note: this may be invoked multiple times, but with different actor/operations. That
+     * may happen if the current operation requires other operations to be performed first
+     * (e.g., A&AI queries, guard checks).
+     */
+    private Consumer<OperationOutcome> startCallback;
+
+    /**
+     * The function to invoke when the operation completes. This is optional.
+     * <p/>
+     * Note: this may be invoked multiple times, but with different actor/operations. That
+     * may happen if the current operation requires other operations to be performed first
+     * (e.g., A&AI queries, guard checks).
+     */
+    private Consumer<OperationOutcome> completeCallback;
+
+    /**
+     * Starts the specified operation.
+     *
+     * @return a future that will return the result of the operation
+     * @throws IllegalArgumentException if the parameters are invalid
+     */
+    public CompletableFuture<OperationOutcome> start() {
+        BeanValidationResult result = validate();
+        if (!result.isValid()) {
+            logger.warn("parameter error in operation {}.{} for {}:\n{}", getActor(), getOperation(), getRequestId(),
+                            result.getResult());
+            throw new IllegalArgumentException("invalid parameters");
+        }
+
+        // @formatter:off
+        return actorService
+                    .getActor(getActor())
+                    .getOperator(getOperation())
+                    .startOperation(this);
+        // @formatter:on
+    }
+
+    /**
+     * Gets the requested ID of the associated event.
+     *
+     * @return the event's request ID, or {@code null} if no request ID is available
+     */
+    public UUID getRequestId() {
+        return (context == null || context.getEvent() == null ? null : context.getEvent().getRequestId());
+    }
+
+    /**
+     * Makes an operation outcome, populating it from the parameters.
+     *
+     * @return a new operation outcome
+     */
+    public OperationOutcome makeOutcome() {
+        OperationOutcome outcome = new OperationOutcome();
+        outcome.setActor(getActor());
+        outcome.setOperation(getOperation());
+        outcome.setTarget(targetEntity);
+
+        return outcome;
+    }
+
+    /**
+     * Invokes the callback to indicate that the operation has started. Any exceptions
+     * generated by the callback are logged, but not re-thrown.
+     *
+     * @param operation the operation that is being started
+     */
+    public void callbackStarted(OperationOutcome operation) {
+        logger.info("started operation {}.{} for {}", operation.getActor(), operation.getOperation(), getRequestId());
+
+        if (startCallback != null) {
+            Util.runFunction(() -> startCallback.accept(operation), "{}.{}: start-callback threw an exception for {}",
+                            operation.getActor(), operation.getOperation(), getRequestId());
+        }
+    }
+
+    /**
+     * Invokes the callback to indicate that the operation has completed. Any exceptions
+     * generated by the callback are logged, but not re-thrown.
+     *
+     * @param operation the operation that is being started
+     */
+    public void callbackCompleted(OperationOutcome operation) {
+        logger.info("completed operation {}.{} outcome={} for {}", operation.getActor(), operation.getOperation(),
+                        operation.getResult(), getRequestId());
+
+        if (completeCallback != null) {
+            Util.runFunction(() -> completeCallback.accept(operation),
+                            "{}.{}: complete-callback threw an exception for {}", operation.getActor(),
+                            operation.getOperation(), getRequestId());
+        }
+    }
+
+    /**
+     * Validates the parameters.
+     *
+     * @return the validation result
+     */
+    public BeanValidationResult validate() {
+        return new BeanValidator().validateTop(ControlLoopOperationParams.class.getSimpleName(), this);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParams.java
new file mode 100644 (file)
index 0000000..da4fb4f
--- /dev/null
@@ -0,0 +1,112 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import java.util.Map;
+import java.util.function.Function;
+import lombok.Data;
+import org.onap.policy.common.parameters.BeanValidationResult;
+import org.onap.policy.common.parameters.BeanValidator;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.common.parameters.annotations.Min;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+
+/**
+ * Parameters used by Actors that connect to a server via HTTP. This contains the
+ * parameters that are common to all of the operations. Only the path changes for each
+ * operation, thus it includes a mapping from operation name to path.
+ */
+@Data
+@NotNull
+@NotBlank
+public class HttpActorParams {
+
+    /**
+     * Name of the HttpClient, as found in the HttpClientFactory.
+     */
+    private String clientName;
+
+    /**
+     * Amount of time, in seconds to wait for the HTTP request to complete, where zero
+     * indicates that it should wait forever. The default is zero.
+     */
+    @Min(0)
+    private long timeoutSec = 0;
+
+    /**
+     * Maps the operation name to its URI path.
+     */
+    private Map<String, String> path;
+
+    /**
+     * Extracts a specific operation's parameters from "this".
+     *
+     * @param name name of the item containing "this"
+     * @return a function to extract an operation's parameters from "this". Note: the
+     *         returned function is not thread-safe
+     */
+    public Function<String, Map<String, Object>> makeOperationParameters(String name) {
+        HttpParams subparams = HttpParams.builder().clientName(getClientName()).timeoutSec(getTimeoutSec()).build();
+
+        return operation -> {
+            String subpath = path.get(operation);
+            if (subpath == null) {
+                return null;
+            }
+
+            subparams.setPath(subpath);
+            return Util.translateToMap(name + "." + operation, subparams);
+        };
+    }
+
+    /**
+     * Validates the parameters.
+     *
+     * @param name name of the object containing these parameters
+     * @return "this"
+     * @throws IllegalArgumentException if the parameters are invalid
+     */
+    public HttpActorParams doValidation(String name) {
+        ValidationResult result = validate(name);
+        if (!result.isValid()) {
+            throw new ParameterValidationRuntimeException("invalid parameters", result);
+        }
+
+        return this;
+    }
+
+    /**
+     * Validates the parameters.
+     *
+     * @param resultName name of the result
+     *
+     * @return the validation result
+     */
+    public ValidationResult validate(String resultName) {
+        BeanValidationResult result = new BeanValidator().validateTop(resultName, this);
+
+        result.validateMap("path", path, (result2, entry) -> result2.validateNotNull(entry.getKey(), entry.getValue()));
+
+        return result;
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParams.java
new file mode 100644 (file)
index 0000000..695ffe4
--- /dev/null
@@ -0,0 +1,69 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import lombok.Builder;
+import lombok.Data;
+import org.onap.policy.common.parameters.BeanValidator;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.common.parameters.annotations.Min;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Parameters used by Operators that connect to a server via HTTP.
+ */
+@NotNull
+@NotBlank
+@Data
+@Builder(toBuilder = true)
+public class HttpParams {
+
+    /**
+     * Name of the HttpClient, as found in the HttpClientFactory.
+     */
+    private String clientName;
+
+    /**
+     * URI path.
+     */
+    private String path;
+
+    /**
+     * Amount of time, in seconds to wait for the HTTP request to complete, where zero
+     * indicates that it should wait forever. The default is zero.
+     */
+    @Min(0)
+    @Builder.Default
+    private long timeoutSec = 0;
+
+
+    /**
+     * Validates the parameters.
+     *
+     * @param resultName name of the result
+     *
+     * @return the validation result
+     */
+    public ValidationResult validate(String resultName) {
+        return new BeanValidator().validateTop(resultName, this);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ParameterValidationRuntimeException.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ParameterValidationRuntimeException.java
new file mode 100644 (file)
index 0000000..3004e19
--- /dev/null
@@ -0,0 +1,57 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import lombok.Getter;
+import org.onap.policy.common.parameters.ValidationResult;
+
+/**
+ * Parameter runtime exception, with an associated validation result. This is used to
+ * throw an exception while passing a validation result up the chain.
+ * <p/>
+ * Note: the validation result is <i>not</i> included in the exception message.
+ */
+public class ParameterValidationRuntimeException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    @Getter
+    private final transient ValidationResult result;
+
+
+    public ParameterValidationRuntimeException(ValidationResult result) {
+        this.result = result;
+    }
+
+    public ParameterValidationRuntimeException(String message, ValidationResult result) {
+        super(message);
+        this.result = result;
+    }
+
+    public ParameterValidationRuntimeException(Throwable cause, ValidationResult result) {
+        super(cause);
+        this.result = result;
+    }
+
+    public ParameterValidationRuntimeException(String message, Throwable cause, ValidationResult result) {
+        super(message, cause);
+        this.result = result;
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParams.java
new file mode 100644 (file)
index 0000000..9e6d8a1
--- /dev/null
@@ -0,0 +1,68 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import lombok.Builder;
+import lombok.Data;
+import org.onap.policy.common.parameters.BeanValidator;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.common.parameters.annotations.Min;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Parameters used by Operators that connect to a server via DMaaP.
+ */
+@NotNull
+@NotBlank
+@Data
+@Builder(toBuilder = true)
+public class TopicParams {
+
+    /**
+     * Name of the target topic end point to which requests should be published.
+     */
+    private String target;
+
+    /**
+     * Source topic end point, from which to read responses.
+     */
+    private String source;
+
+    /**
+     * Amount of time, in seconds to wait for the response, where zero indicates that it
+     * should wait forever. The default is zero.
+     */
+    @Min(0)
+    @Builder.Default
+    private long timeoutSec = 0;
+
+    /**
+     * Validates both the publisher and the subscriber parameters.
+     *
+     * @param resultName name of the result
+     *
+     * @return the validation result
+     */
+    public ValidationResult validate(String resultName) {
+        return new BeanValidator().validateTop(resultName, this);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/FutureManager.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/FutureManager.java
new file mode 100644 (file)
index 0000000..aac2f77
--- /dev/null
@@ -0,0 +1,89 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.pipeline;
+
+import java.util.IdentityHashMap;
+import java.util.concurrent.Future;
+import lombok.NoArgsConstructor;
+
+/**
+ * Manager that manages both futures and listeners. When {@link #stop()} is called, the
+ * listeners are executed and the futures are canceled. The various methods synchronize on
+ * "this" while they manipulate internal data structures.
+ */
+@NoArgsConstructor
+public class FutureManager extends ListenerManager {
+
+    /**
+     * Maps a future to its listener. Records the {@link Runnable} that is passed to
+     * {@link ListenerManager#add(Runnable)} when {@link #add(Future)} is invoked. This is
+     * needed if {@link #remove(Future)} is invoked, so that the same {@link Runnable} is
+     * used each time.
+     */
+    @SuppressWarnings("rawtypes")
+    private final IdentityHashMap<Future, Runnable> future2listener = new IdentityHashMap<>(5);
+
+    /**
+     * Adds a future that is to be canceled when this controller is stopped. Note: if the
+     * controller is already stopped, then the future will be canceled immediately, within
+     * the invoking thread.
+     *
+     * @param future future to be added
+     */
+    public <T> void add(Future<T> future) {
+        Runnable listener = () -> future.cancel(false);
+
+        synchronized (this) {
+            if (future2listener.putIfAbsent(future, listener) != null) {
+                // this future is already in the map, nothing more to do
+                return;
+            }
+
+            if (addOnly(listener)) {
+                // successfully added
+                return;
+            }
+        }
+
+        runListener(listener);
+    }
+
+    /**
+     * Removes a future so that it is not canceled when this controller is stopped.
+     *
+     * @param future future to be removed
+     */
+    public synchronized <T> void remove(Future<T> future) {
+        Runnable listener = future2listener.remove(future);
+        if (listener != null) {
+            remove(listener);
+        }
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+
+        synchronized (this) {
+            future2listener.clear();
+        }
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/ListenerManager.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/ListenerManager.java
new file mode 100644 (file)
index 0000000..1d64a87
--- /dev/null
@@ -0,0 +1,115 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.pipeline;
+
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+
+/**
+ * Listener manager, used by operations within the pipeline to determine if they should
+ * continue to run. When {@link #stop()} is called, the listeners are executed. The
+ * various methods synchronize on "this" while they manipulate internal data structures.
+ */
+@NoArgsConstructor
+public class ListenerManager {
+
+    @Getter
+    private volatile boolean running = true;
+
+    /**
+     * Listeners to be executed when {@link #stop()} is invoked.
+     */
+    private final IdentityHashMap<Runnable, Void> listeners = new IdentityHashMap<>(5);
+
+    /**
+     * Indicates that operations within the pipeline should stop executing.
+     */
+    public void stop() {
+        ArrayList<Runnable> items;
+
+        synchronized (this) {
+            if (!running) {
+                return;
+            }
+
+            running = false;
+            items = new ArrayList<>(listeners.keySet());
+            listeners.clear();
+        }
+
+        items.forEach(this::runListener);
+    }
+
+    /**
+     * Adds a listener that is to be invoked when this controller is stopped. Note: if the
+     * controller is already stopped, then the listener will be invoked immediately,
+     * within the invoking thread.
+     *
+     * @param listener listener to be added
+     */
+    public void add(Runnable listener) {
+        if (!addOnly(listener)) {
+            runListener(listener);
+        }
+    }
+
+    /**
+     * Adds a listener that is to be invoked when this controller is stopped. Note: if the
+     * controller is already stopped, then the listener will be invoked immediately,
+     * within the invoking thread.
+     *
+     * @param listener listener to be added
+     * @return {@code true} if the the listener was added, {@code false} if it could not
+     *         be added because this manager has already been stopped
+     */
+    protected boolean addOnly(Runnable listener) {
+        synchronized (this) {
+            if (running) {
+                listeners.put(listener, null);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Runs a listener, catching any exceptions that it may throw.
+     *
+     * @param listener listener to be executed
+     */
+    protected void runListener(Runnable listener) {
+        // TODO do this asynchronously?
+        Util.runFunction(listener, "pipeline listener {} threw an exception", listener);
+    }
+
+    /**
+     * Removes a listener so that it is not invoked when this controller is stopped.
+     *
+     * @param listener listener to be removed
+     */
+    public synchronized void remove(Runnable listener) {
+        listeners.remove(listener);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFuture.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFuture.java
new file mode 100644 (file)
index 0000000..92843e2
--- /dev/null
@@ -0,0 +1,220 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.pipeline;
+
+import static org.onap.policy.controlloop.actorserviceprovider.Util.ident;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import lombok.NoArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Pipeline controller, used by operations within the pipeline to determine if they should
+ * continue to run. Whenever this is canceled or completed, it automatically cancels all
+ * futures and runs all listeners that have been added.
+ */
+@NoArgsConstructor
+public class PipelineControllerFuture<T> extends CompletableFuture<T> {
+
+    private static final Logger logger = LoggerFactory.getLogger(PipelineControllerFuture.class);
+
+    private static final String COMPLETE_EXCEPT_MSG = "{}: complete future with exception";
+    private static final String CANCEL_MSG = "{}: cancel future";
+    private static final String COMPLETE_MSG = "{}: complete future";
+
+    /**
+     * Tracks items added to this controller via one of the <i>add</i> methods.
+     */
+    private final FutureManager futures = new FutureManager();
+
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        return doAndStop(() -> super.cancel(mayInterruptIfRunning), CANCEL_MSG, ident(this));
+    }
+
+    @Override
+    public boolean complete(T value) {
+        return doAndStop(() -> super.complete(value), COMPLETE_MSG, ident(this));
+    }
+
+    @Override
+    public boolean completeExceptionally(Throwable ex) {
+        return doAndStop(() -> super.completeExceptionally(ex), COMPLETE_EXCEPT_MSG, ident(this));
+    }
+
+    @Override
+    public CompletableFuture<T> completeAsync(Supplier<? extends T> supplier, Executor executor) {
+        return super.completeAsync(() -> doAndStop(supplier, COMPLETE_MSG, ident(this)), executor);
+    }
+
+    @Override
+    public CompletableFuture<T> completeAsync(Supplier<? extends T> supplier) {
+        return super.completeAsync(() -> doAndStop(supplier, COMPLETE_MSG, ident(this)));
+    }
+
+    @Override
+    public CompletableFuture<T> completeOnTimeout(T value, long timeout, TimeUnit unit) {
+        logger.info("{}: set future timeout to {} {}", ident(this), timeout, unit);
+        return super.completeOnTimeout(value, timeout, unit);
+    }
+
+    @Override
+    public <U> PipelineControllerFuture<U> newIncompleteFuture() {
+        return new PipelineControllerFuture<>();
+    }
+
+    /**
+     * Generates a function that, when invoked, will remove the given future. This is
+     * typically added onto the end of a pipeline via one of the
+     * {@link CompletableFuture#whenComplete(BiConsumer)} methods.
+     *
+     * @return a function that removes the given future
+     */
+    public <F> BiConsumer<F, Throwable> delayedRemove(Future<F> future) {
+        return (value, thrown) -> remove(future);
+    }
+
+    /**
+     * Generates a function that, when invoked, will remove the given listener. This is
+     * typically added onto the end of a pipeline via one of the
+     * {@link CompletableFuture#whenComplete(BiConsumer)} methods.
+     *
+     * @return a function that removes the given listener
+     */
+    public <F> BiConsumer<F, Throwable> delayedRemove(Runnable listener) {
+        return (value, thrown) -> remove(listener);
+    }
+
+    /**
+     * Generates a function that, when invoked, will stop all pipeline listeners and
+     * complete this future. This is typically added onto the end of a pipeline via one of
+     * the {@link CompletableFuture#whenComplete(BiConsumer)} methods.
+     *
+     * @return a function that stops all pipeline listeners
+     */
+    public BiConsumer<T, Throwable> delayedComplete() {
+        return (value, thrown) -> {
+            if (thrown == null) {
+                complete(value);
+            } else {
+                completeExceptionally(thrown);
+            }
+        };
+    }
+
+    /**
+     * Adds a future to the controller and arranges for it to be removed from the
+     * controller when it completes, whether or not it throws an exception. If the
+     * controller has already been stopped, then the future is canceled and a new,
+     * incomplete future is returned.
+     *
+     * @param future future to be wrapped
+     * @return a new future
+     */
+    public CompletableFuture<T> wrap(CompletableFuture<T> future) {
+        if (!isRunning()) {
+            logger.trace("{}: not running, skipping next task {}", ident(this), ident(future));
+            future.cancel(false);
+            return new CompletableFuture<>();
+        }
+
+        add(future);
+        return future.whenComplete(this.delayedRemove(future));
+    }
+
+    /**
+     * Adds a function whose return value is to be canceled when this controller is
+     * stopped. Note: if the controller is already stopped, then the function will
+     * <i>not</i> be executed.
+     *
+     * @param futureMaker function to be invoked to create the future
+     * @return a function to create the future and arrange for it to be managed by this
+     *         controller
+     */
+    public <F> Function<F, CompletableFuture<F>> wrap(Function<F, CompletableFuture<F>> futureMaker) {
+
+        return input -> {
+            if (!isRunning()) {
+                logger.trace("{}: discarded new future", ident(this));
+                return new CompletableFuture<>();
+            }
+
+            CompletableFuture<F> future = futureMaker.apply(input);
+            add(future);
+
+            return future.whenComplete(delayedRemove(future));
+        };
+    }
+
+    public <F> void add(Future<F> future) {
+        logger.trace("{}: add future {}", ident(this), ident(future));
+        futures.add(future);
+    }
+
+    public void add(Runnable listener) {
+        logger.trace("{}: add listener {}", ident(this), ident(listener));
+        futures.add(listener);
+    }
+
+    public boolean isRunning() {
+        return futures.isRunning();
+    }
+
+    public <F> void remove(Future<F> future) {
+        logger.trace("{}: remove future {}", ident(this), ident(future));
+        futures.remove(future);
+    }
+
+    public void remove(Runnable listener) {
+        logger.trace("{}: remove listener {}", ident(this), ident(listener));
+        futures.remove(listener);
+    }
+
+    /**
+     * Performs an operation, stops the futures, and returns the value from the operation.
+     * Logs a message using the given arguments.
+     *
+     *
+     * @param <R> type of value to be returned
+     * @param supplier operation to perform
+     * @param message message to be logged
+     * @param args message arguments to fill "{}" place-holders
+     * @return the operation's result
+     */
+    private <R> R doAndStop(Supplier<R> supplier, String message, Object... args) {
+        try {
+            logger.trace(message, args);
+            return supplier.get();
+
+        } finally {
+            logger.trace("{}: stopping this future", ident(this));
+            futures.stop();
+        }
+    }
+}
index 88f3c16..620950a 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  * Actor
  * ================================================================================
- * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved.
+ * Copyright (C) 2017-2018, 2020 AT&T Intellectual Property. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
 
 package org.onap.policy.controlloop.actorserviceprovider.spi;
 
+import java.util.Collection;
+
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.onap.policy.common.capabilities.Configurable;
+import org.onap.policy.common.capabilities.Startable;
+import org.onap.policy.controlloop.actorserviceprovider.Operator;
+
+/**
+ * This is the service interface for defining an Actor used in Control Loop Operational
+ * Policies for performing actions on runtime entities.
+ *
+ * @author pameladragosh
+ *
+ */
+public interface Actor extends Startable, Configurable<Map<String,Object>> {
+
+    /**
+     * Gets the name of the actor.
+     *
+     * @return the actor name
+     */
+    String getName();
+
+    /**
+     * Gets a particular operator.
+     *
+     * @param name name of the operation of interest
+     * @return the desired operation
+     * @throws IllegalArgumentException if no operation by the given name exists
+     */
+    Operator getOperator(String name);
+
+    /**
+     * Gets the supported operations.
+     *
+     * @return the supported operations
+     */
+    public Collection<Operator> getOperators();
+
+    /**
+     * Gets the names of the supported operations.
+     *
+     * @return the names of the supported operations
+     */
+    public Set<String> getOperationNames();
+
 
-public interface Actor {
+    // TODO old code: remove lines down to **HERE**
 
     String actor();
 
@@ -33,4 +80,5 @@ public interface Actor {
 
     List<String> recipePayloads(String recipe);
 
+    // **HERE**
 }
index 7ab21de..139c517 100644 (file)
@@ -4,7 +4,7 @@
  * ================================================================================
  * Copyright (C) 2018 Ericsson. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
- * Modifications Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -28,6 +28,8 @@ import static org.junit.Assert.assertNotNull;
 import org.junit.Test;
 import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
 
+// TODO combine this with ActorServiceTest
+
 public class ActorServiceProviderTest {
 
     private static final String DOROTHY = "Dorothy";
@@ -37,12 +39,12 @@ public class ActorServiceProviderTest {
         ActorService actorService = ActorService.getInstance();
         assertNotNull(actorService);
 
-        assertEquals(1, actorService.actors().size());
+        assertEquals(1, actorService.getActors().size());
 
         actorService = ActorService.getInstance();
         assertNotNull(actorService);
 
-        Actor dummyActor = ActorService.getInstance().actors().get(0);
+        Actor dummyActor = ActorService.getInstance().getActors().iterator().next();
         assertNotNull(dummyActor);
 
         assertEquals("DummyActor", dummyActor.actor());
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/ActorServiceTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/ActorServiceTest.java
new file mode 100644 (file)
index 0000000..851a791
--- /dev/null
@@ -0,0 +1,382 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.parameters.ObjectValidationResult;
+import org.onap.policy.common.parameters.ValidationStatus;
+import org.onap.policy.controlloop.actorserviceprovider.impl.ActorImpl;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException;
+import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+
+public class ActorServiceTest {
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+    private static final String ACTOR1 = "actor A";
+    private static final String ACTOR2 = "actor B";
+    private static final String ACTOR3 = "actor C";
+    private static final String ACTOR4 = "actor D";
+
+    private Actor actor1;
+    private Actor actor2;
+    private Actor actor3;
+    private Actor actor4;
+
+    private Map<String, Object> sub1;
+    private Map<String, Object> sub2;
+    private Map<String, Object> sub3;
+    private Map<String, Object> sub4;
+    private Map<String, Object> params;
+
+    private ActorService service;
+
+
+    /**
+     * Initializes the fields, including a fully populated {@link #service}.
+     */
+    @Before
+    public void setUp() {
+        actor1 = spy(new ActorImpl(ACTOR1));
+        actor2 = spy(new ActorImpl(ACTOR2));
+        actor3 = spy(new ActorImpl(ACTOR3));
+        actor4 = spy(new ActorImpl(ACTOR4));
+
+        sub1 = Map.of("sub A", "value A");
+        sub2 = Map.of("sub B", "value B");
+        sub3 = Map.of("sub C", "value C");
+        sub4 = Map.of("sub D", "value D");
+
+        params = Map.of(ACTOR1, sub1, ACTOR2, sub2, ACTOR3, sub3, ACTOR4, sub4);
+
+        service = makeService(actor1, actor2, actor3, actor4);
+    }
+
+    @Test
+    public void testActorService() {
+        /*
+         * make a service where actors two and four have names that are duplicates of the
+         * others
+         */
+        actor2 = spy(new ActorImpl(ACTOR1));
+        actor4 = spy(new ActorImpl(ACTOR3));
+
+        service = makeService(actor1, actor2, actor3, actor4);
+
+        assertEquals(2, service.getActorNames().size());
+
+        assertSame(actor1, service.getActor(ACTOR1));
+        assertSame(actor3, service.getActor(ACTOR3));
+    }
+
+    @Test
+    public void testDoStart() {
+        service.configure(params);
+
+        setUpOp("testDoStart", actor -> when(actor.isConfigured()).thenReturn(false), Actor::start);
+
+        /*
+         * Start the service.
+         */
+        service.start();
+        assertTrue(service.isAlive());
+
+        Iterator<Actor> iter = service.getActors().iterator();
+        verify(iter.next()).start();
+        verify(iter.next(), never()).start();
+        verify(iter.next()).start();
+        verify(iter.next()).start();
+
+        // no additional types of operations
+        verify(actor1).configure(any());
+
+        // no other types of operations
+        verify(actor1, never()).stop();
+        verify(actor1, never()).shutdown();
+    }
+
+    @Test
+    public void testDoStop() {
+        service.configure(params);
+        service.start();
+
+        setUpOp("testDoStop", Actor::stop, Actor::stop);
+
+        /*
+         * Stop the service.
+         */
+        service.stop();
+        assertFalse(service.isAlive());
+
+        Iterator<Actor> iter = service.getActors().iterator();
+        verify(iter.next()).stop();
+        verify(iter.next(), times(2)).stop();
+        verify(iter.next()).stop();
+        verify(iter.next()).stop();
+
+        // no additional types of operations
+        verify(actor1).configure(any());
+        verify(actor1).start();
+
+        // no other types of operation
+        verify(actor1, never()).shutdown();
+    }
+
+    @Test
+    public void testDoShutdown() {
+        service.configure(params);
+        service.start();
+
+        setUpOp("testDoShutdown", Actor::shutdown, Actor::shutdown);
+
+        /*
+         * Shut down the service.
+         */
+        service.shutdown();
+        assertFalse(service.isAlive());
+
+        Iterator<Actor> iter = service.getActors().iterator();
+        verify(iter.next()).shutdown();
+        verify(iter.next(), times(2)).shutdown();
+        verify(iter.next()).shutdown();
+        verify(iter.next()).shutdown();
+
+        // no additional types of operations
+        verify(actor1).configure(any());
+        verify(actor1).start();
+
+        // no other types of operation
+        verify(actor1, never()).stop();
+    }
+
+    /**
+     * Applies an operation to the second actor, and then arranges for the third actor to
+     * throw an exception when its operation is performed.
+     *
+     * @param testName test name
+     * @param oper2 operation to apply to the second actor
+     * @param oper3 operation to apply to the third actor
+     */
+    private void setUpOp(String testName, Consumer<Actor> oper2, Consumer<Actor> oper3) {
+        Collection<Actor> actors = service.getActors();
+        assertEquals(testName, 4, actors.size());
+
+        Iterator<Actor> iter = actors.iterator();
+
+        // leave the first alone
+        iter.next();
+
+        // apply oper2 to the second actor
+        oper2.accept(iter.next());
+
+        // throw an exception in the third
+        oper3.accept(doThrow(new IllegalStateException(EXPECTED_EXCEPTION)).when(iter.next()));
+
+        // leave the fourth alone
+        iter.next();
+    }
+
+    @Test
+    public void testGetInstance() {
+        service = ActorService.getInstance();
+        assertNotNull(service);
+
+        assertSame(service, ActorService.getInstance());
+    }
+
+    @Test
+    public void testGetActor() {
+        assertSame(actor1, service.getActor(ACTOR1));
+        assertSame(actor3, service.getActor(ACTOR3));
+
+        assertThatIllegalArgumentException().isThrownBy(() -> service.getActor("unknown actor"));
+    }
+
+    @Test
+    public void testGetActors() {
+        // @formatter:off
+        assertEquals("[actor A, actor B, actor C, actor D]",
+                        service.getActors().stream()
+                            .map(Actor::getName)
+                            .sorted()
+                            .collect(Collectors.toList())
+                            .toString());
+        // @formatter:on
+    }
+
+    @Test
+    public void testGetActorNames() {
+        // @formatter:off
+        assertEquals("[actor A, actor B, actor C, actor D]",
+                        service.getActorNames().stream()
+                            .sorted()
+                            .collect(Collectors.toList())
+                            .toString());
+        // @formatter:on
+    }
+
+    @Test
+    public void testDoConfigure() {
+        service.configure(params);
+        assertTrue(service.isConfigured());
+
+        verify(actor1).configure(sub1);
+        verify(actor2).configure(sub2);
+        verify(actor3).configure(sub3);
+        verify(actor4).configure(sub4);
+
+        // no other types of operations
+        verify(actor1, never()).start();
+        verify(actor1, never()).stop();
+        verify(actor1, never()).shutdown();
+    }
+
+    /**
+     * Tests doConfigure() where actors throw parameter validation and runtime exceptions.
+     */
+    @Test
+    public void testDoConfigureExceptions() {
+        makeValidException(actor1);
+        makeRuntimeException(actor2);
+        makeValidException(actor3);
+
+        service.configure(params);
+        assertTrue(service.isConfigured());
+    }
+
+    /**
+     * Tests doConfigure(). Arranges for the following:
+     * <ul>
+     * <li>one actor is configured, but has parameters</li>
+     * <li>another actor is configured, but has no parameters</li>
+     * <li>another actor has no parameters and is not configured</li>
+     * </ul>
+     */
+    @Test
+    public void testDoConfigureConfigure() {
+        // need mutable parameters
+        params = new TreeMap<>(params);
+
+        // configure one actor
+        actor1.configure(sub1);
+
+        // configure another and remove its parameters
+        actor2.configure(sub2);
+        params.remove(ACTOR2);
+
+        // create a new, unconfigured actor
+        ActorImpl actor5 = spy(new ActorImpl("UNCONFIGURED"));
+        service = makeService(actor1, actor2, actor3, actor4, actor5);
+
+        /*
+         * Configure it.
+         */
+        service.configure(params);
+        assertTrue(service.isConfigured());
+
+        // this should have been configured again
+        verify(actor1, times(2)).configure(sub1);
+
+        // no parameters, so this should not have been configured again
+        verify(actor2).configure(sub2);
+
+        // these were only configured once
+        verify(actor3).configure(sub3);
+        verify(actor4).configure(sub4);
+
+        // never configured
+        verify(actor5, never()).configure(any());
+        assertFalse(actor5.isConfigured());
+
+        // start and verify that all are started except for the last
+        service.start();
+        verify(actor1).start();
+        verify(actor2).start();
+        verify(actor3).start();
+        verify(actor4).start();
+        verify(actor5, never()).start();
+    }
+
+    /**
+     * Arranges for an actor to throw a validation exception when
+     * {@link Actor#configure(Map)} is invoked.
+     *
+     * @param actor actor of interest
+     */
+    private void makeValidException(Actor actor) {
+        ParameterValidationRuntimeException ex = new ParameterValidationRuntimeException(
+                        new ObjectValidationResult(actor.getName(), null, ValidationStatus.INVALID, "null"));
+        doThrow(ex).when(actor).configure(any());
+    }
+
+    /**
+     * Arranges for an actor to throw a runtime exception when
+     * {@link Actor#configure(Map)} is invoked.
+     *
+     * @param actor actor of interest
+     */
+    private void makeRuntimeException(Actor actor) {
+        IllegalStateException ex = new IllegalStateException(EXPECTED_EXCEPTION);
+        doThrow(ex).when(actor).configure(any());
+    }
+
+    @Test
+    public void testLoadActors() {
+        assertFalse(ActorService.getInstance().getActors().isEmpty());
+        assertNotNull(ActorService.getInstance().getActor("DummyActor"));
+    }
+
+    /**
+     * Makes an actor service whose {@link ActorService#loadActors()} method returns the
+     * given actors.
+     *
+     * @param actors actors to be returned
+     * @return a new actor service
+     */
+    private ActorService makeService(Actor... actors) {
+        return new ActorService() {
+            @Override
+            protected Iterable<Actor> loadActors() {
+                return Arrays.asList(actors);
+            }
+        };
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandlerTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandlerTest.java
new file mode 100644 (file)
index 0000000..31c6d20
--- /dev/null
@@ -0,0 +1,172 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.policy.PolicyResult;
+
+public class AsyncResponseHandlerTest {
+
+    private static final String ACTOR = "my-actor";
+    private static final String OPERATION = "my-operation";
+    private static final UUID REQ_ID = UUID.randomUUID();
+    private static final String TEXT = "some text";
+
+    private VirtualControlLoopEvent event;
+    private ControlLoopEventContext context;
+    private ControlLoopOperationParams params;
+    private OperationOutcome outcome;
+    private MyHandler handler;
+
+    /**
+     * Initializes all fields, including {@link #handler}.
+     */
+    @Before
+    public void setUp() {
+        event = new VirtualControlLoopEvent();
+        event.setRequestId(REQ_ID);
+
+        context = new ControlLoopEventContext(event);
+        params = ControlLoopOperationParams.builder().actor(ACTOR).operation(OPERATION).context(context).build();
+        outcome = params.makeOutcome();
+
+        handler = new MyHandler(params, outcome);
+    }
+
+    @Test
+    public void testAsyncResponseHandler_testGetParams_testGetOutcome() {
+        assertSame(params, handler.getParams());
+        assertSame(outcome, handler.getOutcome());
+    }
+
+    @Test
+    public void testHandle() {
+        CompletableFuture<String> future = new CompletableFuture<>();
+        handler.handle(future).complete(outcome);
+
+        assertTrue(future.isCancelled());
+    }
+
+    @Test
+    public void testCompleted() throws Exception {
+        CompletableFuture<OperationOutcome> result = handler.handle(new CompletableFuture<>());
+        handler.completed(TEXT);
+        assertTrue(result.isDone());
+        assertSame(outcome, result.get());
+        assertEquals(PolicyResult.FAILURE_RETRIES, outcome.getResult());
+        assertEquals(TEXT, outcome.getMessage());
+    }
+
+    /**
+     * Tests completed() when doCompleted() throws an exception.
+     */
+    @Test
+    public void testCompletedException() throws Exception {
+        IllegalStateException except = new IllegalStateException();
+
+        outcome = params.makeOutcome();
+        handler = new MyHandler(params, outcome) {
+            @Override
+            protected OperationOutcome doComplete(String rawResponse) {
+                throw except;
+            }
+        };
+
+        CompletableFuture<OperationOutcome> result = handler.handle(new CompletableFuture<>());
+        handler.completed(TEXT);
+        assertTrue(result.isCompletedExceptionally());
+
+        AtomicReference<Throwable> thrown = new AtomicReference<>();
+        result.whenComplete((unused, thrown2) -> thrown.set(thrown2));
+
+        assertSame(except, thrown.get());
+    }
+
+    @Test
+    public void testFailed() throws Exception {
+        IllegalStateException except = new IllegalStateException();
+
+        CompletableFuture<OperationOutcome> result = handler.handle(new CompletableFuture<>());
+        handler.failed(except);
+
+        assertTrue(result.isDone());
+        assertSame(outcome, result.get());
+        assertEquals(PolicyResult.FAILURE_GUARD, outcome.getResult());
+    }
+
+    /**
+     * Tests failed() when doFailed() throws an exception.
+     */
+    @Test
+    public void testFailedException() throws Exception {
+        IllegalStateException except = new IllegalStateException();
+
+        outcome = params.makeOutcome();
+        handler = new MyHandler(params, outcome) {
+            @Override
+            protected OperationOutcome doFailed(Throwable thrown) {
+                throw except;
+            }
+        };
+
+        CompletableFuture<OperationOutcome> result = handler.handle(new CompletableFuture<>());
+        handler.failed(except);
+        assertTrue(result.isCompletedExceptionally());
+
+        AtomicReference<Throwable> thrown = new AtomicReference<>();
+        result.whenComplete((unused, thrown2) -> thrown.set(thrown2));
+
+        assertSame(except, thrown.get());
+    }
+
+    private class MyHandler extends AsyncResponseHandler<String> {
+
+        public MyHandler(ControlLoopOperationParams params, OperationOutcome outcome) {
+            super(params, outcome);
+        }
+
+        @Override
+        protected OperationOutcome doComplete(String rawResponse) {
+            OperationOutcome outcome = getOutcome();
+            outcome.setResult(PolicyResult.FAILURE_RETRIES);
+            outcome.setMessage(rawResponse);
+            return outcome;
+        }
+
+        @Override
+        protected OperationOutcome doFailed(Throwable thrown) {
+            OperationOutcome outcome = getOutcome();
+            outcome.setResult(PolicyResult.FAILURE_GUARD);
+            return outcome;
+        }
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/CallbackManagerTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/CallbackManagerTest.java
new file mode 100644 (file)
index 0000000..44606cb
--- /dev/null
@@ -0,0 +1,89 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+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 java.time.Instant;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CallbackManagerTest {
+
+    private CallbackManager mgr;
+
+    @Before
+    public void setUp() {
+        mgr = new CallbackManager();
+    }
+
+    @Test
+    public void testCanStart_testGetStartTime() {
+        // null until canXxx() is called
+        assertNull(mgr.getStartTime());
+
+        assertTrue(mgr.canStart());
+
+        Instant time = mgr.getStartTime();
+        assertNotNull(time);
+        assertNull(mgr.getEndTime());
+
+        // false for now on
+        assertFalse(mgr.canStart());
+        assertFalse(mgr.canStart());
+
+        assertEquals(time, mgr.getStartTime());
+    }
+
+    @Test
+    public void testCanEnd_testGetEndTime() {
+        // null until canXxx() is called
+        assertNull(mgr.getEndTime());
+        assertNull(mgr.getEndTime());
+
+        assertTrue(mgr.canEnd());
+
+        Instant time = mgr.getEndTime();
+        assertNotNull(time);
+        assertNull(mgr.getStartTime());
+
+        // false for now on
+        assertFalse(mgr.canEnd());
+        assertFalse(mgr.canEnd());
+
+        assertEquals(time, mgr.getEndTime());
+    }
+
+    @Test
+    public void testRun() {
+        mgr.run();
+
+        assertNotNull(mgr.getStartTime());
+        assertNotNull(mgr.getEndTime());
+
+        assertFalse(mgr.canStart());
+        assertFalse(mgr.canEnd());
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/DelayedIdentStringTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/DelayedIdentStringTest.java
new file mode 100644 (file)
index 0000000..5b9856f
--- /dev/null
@@ -0,0 +1,93 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class DelayedIdentStringTest {
+
+    private int countToStringCalls;
+    private Object object;
+    private DelayedIdentString delay;
+
+    /**
+     * Initializes fields, including {@link #delay}.
+     */
+    @Before
+    public void setUp() {
+        countToStringCalls = 0;
+
+        object = new Object() {
+            @Override
+            public String toString() {
+                ++countToStringCalls;
+                return super.toString();
+            }
+        };
+
+        delay = new DelayedIdentString(object);
+    }
+
+    @Test
+    public void testToString() {
+        String delayed = delay.toString();
+        assertEquals(1, countToStringCalls);
+
+        String real = object.toString();
+        assertNotEquals(real, delayed);
+
+        assertThat(delayed).startsWith("@");
+        assertTrue(delayed.length() > 1);
+
+        // test case where the object is null
+        assertEquals(DelayedIdentString.NULL_STRING, new DelayedIdentString(null).toString());
+
+        // test case where the object returns null from toString()
+        object = new Object() {
+            @Override
+            public String toString() {
+                return null;
+            }
+        };
+        assertEquals(DelayedIdentString.NULL_STRING, new DelayedIdentString(object).toString());
+
+        // test case where the object's toString() does not include "@"
+        object = new Object() {
+            @Override
+            public String toString() {
+                return "some text";
+            }
+        };
+        assertEquals(object.toString(), new DelayedIdentString(object).toString());
+    }
+
+    @Test
+    public void testDelayedIdentString() {
+        // should not have called the object's toString() method yet
+        assertEquals(0, countToStringCalls);
+    }
+}
index e9cf238..76cadff 100644 (file)
@@ -4,6 +4,7 @@
  * ================================================================================
  * Copyright (C) 2018 Ericsson. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
+ * Modifications Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,10 +24,14 @@ package org.onap.policy.controlloop.actorserviceprovider;
 
 import java.util.ArrayList;
 import java.util.List;
+import org.onap.policy.controlloop.actorserviceprovider.impl.ActorImpl;
 
-import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+public class DummyActor extends ActorImpl {
+
+    public DummyActor() {
+        super(DummyActor.class.getSimpleName());
+    }
 
-public class DummyActor implements Actor {
     @Override
     public String actor() {
         return this.getClass().getSimpleName();
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/OperationOutcomeTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/OperationOutcomeTest.java
new file mode 100644 (file)
index 0000000..4e97283
--- /dev/null
@@ -0,0 +1,137 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.time.Instant;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.policy.PolicyResult;
+
+public class OperationOutcomeTest {
+    private static final String ACTOR = "my-actor";
+    private static final String OPERATION = "my-operation";
+    private static final String TARGET = "my-target";
+    private static final Instant START = Instant.ofEpochMilli(10);
+    private static final Instant END = Instant.ofEpochMilli(20);
+    private static final String SUB_REQ_ID = "my-sub-request-id";
+    private static final PolicyResult RESULT = PolicyResult.FAILURE_GUARD;
+    private static final String MESSAGE = "my-message";
+
+    private OperationOutcome outcome;
+
+    @Before
+    public void setUp() {
+        outcome = new OperationOutcome();
+    }
+
+    @Test
+    public void testOperationOutcomeOperationOutcome() {
+        setAll();
+
+        OperationOutcome outcome2 = new OperationOutcome(outcome);
+
+        assertEquals(ACTOR, outcome2.getActor());
+        assertEquals(OPERATION, outcome2.getOperation());
+        assertEquals(TARGET, outcome2.getTarget());
+        assertEquals(START, outcome2.getStart());
+        assertEquals(END, outcome2.getEnd());
+        assertEquals(SUB_REQ_ID, outcome2.getSubRequestId());
+        assertEquals(RESULT, outcome2.getResult());
+        assertEquals(MESSAGE, outcome2.getMessage());
+    }
+
+    @Test
+    public void testToControlLoopOperation() {
+        setAll();
+
+        ControlLoopOperation outcome2 = outcome.toControlLoopOperation();
+
+        assertEquals(ACTOR, outcome2.getActor());
+        assertEquals(OPERATION, outcome2.getOperation());
+        assertEquals(TARGET, outcome2.getTarget());
+        assertEquals(START, outcome2.getStart());
+        assertEquals(END, outcome2.getEnd());
+        assertEquals(SUB_REQ_ID, outcome2.getSubRequestId());
+        assertEquals(RESULT.toString(), outcome2.getOutcome());
+        assertEquals(MESSAGE, outcome2.getMessage());
+    }
+
+    /**
+     * Tests both isFor() methods, as one invokes the other.
+     */
+    @Test
+    public void testIsFor() {
+        setAll();
+
+        // null case
+        assertFalse(OperationOutcome.isFor(null, ACTOR, OPERATION));
+
+        // actor mismatch
+        assertFalse(OperationOutcome.isFor(outcome, TARGET, OPERATION));
+
+        // operation mismatch
+        assertFalse(OperationOutcome.isFor(outcome, ACTOR, TARGET));
+
+        // null actor in outcome
+        outcome.setActor(null);
+        assertFalse(OperationOutcome.isFor(outcome, ACTOR, OPERATION));
+        outcome.setActor(ACTOR);
+
+        // null operation in outcome
+        outcome.setOperation(null);
+        assertFalse(OperationOutcome.isFor(outcome, ACTOR, OPERATION));
+        outcome.setOperation(OPERATION);
+
+        // null actor argument
+        assertThatThrownBy(() -> outcome.isFor(null, OPERATION));
+
+        // null operation argument
+        assertThatThrownBy(() -> outcome.isFor(ACTOR, null));
+
+        // true case
+        assertTrue(OperationOutcome.isFor(outcome, ACTOR, OPERATION));
+    }
+
+    @Test
+    public void testSetResult() {
+        outcome.setResult(PolicyResult.FAILURE_EXCEPTION);
+        assertEquals(PolicyResult.FAILURE_EXCEPTION, outcome.getResult());
+
+        assertThatThrownBy(() -> outcome.setResult(null));
+    }
+
+    private void setAll() {
+        outcome.setActor(ACTOR);
+        outcome.setEnd(END);
+        outcome.setMessage(MESSAGE);
+        outcome.setOperation(OPERATION);
+        outcome.setResult(RESULT);
+        outcome.setStart(START);
+        outcome.setSubRequestId(SUB_REQ_ID);
+        outcome.setTarget(TARGET);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/UtilTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/UtilTest.java
new file mode 100644 (file)
index 0000000..4a3f321
--- /dev/null
@@ -0,0 +1,263 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import ch.qos.logback.classic.Logger;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import lombok.Builder;
+import lombok.Data;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.common.utils.test.log.logback.ExtractAppender;
+import org.slf4j.LoggerFactory;
+
+public class UtilTest {
+    private static final String MY_REQUEST = "my-request";
+    private static final String URL = "my-url";
+    private static final String OUT_URL = "OUT|REST|my-url";
+    private static final String IN_URL = "IN|REST|my-url";
+    protected static final String EXPECTED_EXCEPTION = "expected exception";
+
+    /**
+     * Used to attach an appender to the class' logger.
+     */
+    private static final Logger logger = (Logger) LoggerFactory.getLogger(Util.class);
+    private static final ExtractAppender appender = new ExtractAppender();
+
+    /**
+     * Initializes statics.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        appender.setContext(logger.getLoggerContext());
+        appender.start();
+
+        logger.addAppender(appender);
+    }
+
+    @AfterClass
+    public static void tearDownAfterClass() {
+        appender.stop();
+    }
+
+    @Before
+    public void setUp() {
+        appender.clearExtractions();
+    }
+
+    @Test
+    public void testIdent() {
+        Object object = new Object();
+        String result = Util.ident(object).toString();
+
+        assertNotEquals(object.toString(), result);
+        assertThat(result).startsWith("@");
+        assertTrue(result.length() > 1);
+    }
+
+    @Test
+    public void testLogRestRequest() throws CoderException {
+        // log structured data
+        appender.clearExtractions();
+        Util.logRestRequest(URL, new Abc(10, null, null));
+        List<String> output = appender.getExtracted();
+        assertEquals(1, output.size());
+
+        assertThat(output.get(0)).contains(OUT_URL).contains("{\n  \"intValue\": 10\n}");
+
+        // log a plain string
+        appender.clearExtractions();
+        Util.logRestRequest(URL, MY_REQUEST);
+        output = appender.getExtracted();
+        assertEquals(1, output.size());
+
+        assertThat(output.get(0)).contains(OUT_URL).contains(MY_REQUEST);
+
+        // exception from coder
+        StandardCoder coder = new StandardCoder() {
+            @Override
+            public String encode(Object object, boolean pretty) throws CoderException {
+                throw new CoderException(EXPECTED_EXCEPTION);
+            }
+        };
+
+        appender.clearExtractions();
+        Util.logRestRequest(coder, URL, new Abc(11, null, null));
+        output = appender.getExtracted();
+        assertEquals(2, output.size());
+        assertThat(output.get(0)).contains("cannot pretty-print request");
+        assertThat(output.get(1)).contains(OUT_URL);
+    }
+
+    @Test
+    public void testLogRestResponse() throws CoderException {
+        // log structured data
+        appender.clearExtractions();
+        Util.logRestResponse(URL, new Abc(10, null, null));
+        List<String> output = appender.getExtracted();
+        assertEquals(1, output.size());
+
+        assertThat(output.get(0)).contains(IN_URL).contains("{\n  \"intValue\": 10\n}");
+
+        // log null response
+        appender.clearExtractions();
+        Util.logRestResponse(URL, null);
+        output = appender.getExtracted();
+        assertEquals(1, output.size());
+
+        assertThat(output.get(0)).contains(IN_URL).contains("null");
+
+        // log a plain string
+        appender.clearExtractions();
+        Util.logRestResponse(URL, MY_REQUEST);
+        output = appender.getExtracted();
+        assertEquals(1, output.size());
+
+        assertThat(output.get(0)).contains(IN_URL).contains(MY_REQUEST);
+
+        // exception from coder
+        StandardCoder coder = new StandardCoder() {
+            @Override
+            public String encode(Object object, boolean pretty) throws CoderException {
+                throw new CoderException(EXPECTED_EXCEPTION);
+            }
+        };
+
+        appender.clearExtractions();
+        Util.logRestResponse(coder, URL, new Abc(11, null, null));
+        output = appender.getExtracted();
+        assertEquals(2, output.size());
+        assertThat(output.get(0)).contains("cannot pretty-print response");
+        assertThat(output.get(1)).contains(IN_URL);
+    }
+
+    @Test
+    public void testRunFunction() {
+        // no exception, no log
+        AtomicInteger count = new AtomicInteger();
+        Util.runFunction(() -> count.incrementAndGet(), "no error");
+        assertEquals(1, count.get());
+        assertEquals(0, appender.getExtracted().size());
+
+        // with an exception
+        Runnable runnable = () -> {
+            count.incrementAndGet();
+            throw new IllegalStateException("expected exception");
+        };
+
+        appender.clearExtractions();
+        Util.runFunction(runnable, "error with no args");
+        List<String> output = appender.getExtracted();
+        assertEquals(1, output.size());
+        assertThat(output.get(0)).contains("error with no args");
+
+        appender.clearExtractions();
+        Util.runFunction(runnable, "error {} {} arg(s)", "with", 2);
+        output = appender.getExtracted();
+        assertEquals(1, output.size());
+        assertThat(output.get(0)).contains("error with 2 arg(s)");
+    }
+
+    @Test
+    public void testTranslate() {
+        // Abc => Abc
+        final Abc abc = Abc.builder().intValue(1).strValue("hello").anotherString("another").build();
+        Abc abc2 = Util.translate("abc to abc", abc, Abc.class);
+        assertEquals(abc, abc2);
+
+        // Abc => Similar
+        Similar sim = Util.translate("abc to similar", abc, Similar.class);
+        assertEquals(abc.getIntValue(), sim.getIntValue());
+        assertEquals(abc.getStrValue(), sim.getStrValue());
+
+        // Abc => Map
+        @SuppressWarnings("unchecked")
+        Map<String, Object> map = Util.translate("abc to map", abc, TreeMap.class);
+        assertEquals("{anotherString=another, intValue=1, strValue=hello}", map.toString());
+
+        // Map => Map
+        @SuppressWarnings("unchecked")
+        Map<String, Object> map2 = Util.translate("map to map", map, LinkedHashMap.class);
+        assertEquals(map.toString(), map2.toString());
+
+        // Map => Abc
+        abc2 = Util.translate("map to abc", map, Abc.class);
+        assertEquals(abc, abc2);
+    }
+
+    @Test
+    public void testTranslateToMap() {
+        assertNull(Util.translateToMap("map: null", null));
+
+        // Abc => Map
+        final Abc abc = Abc.builder().intValue(2).strValue("world").anotherString("some").build();
+        Map<String, Object> map = new TreeMap<>(Util.translateToMap("map: abc to map", abc));
+        assertEquals("{anotherString=some, intValue=2, strValue=world}", map.toString());
+
+        // Map => Map
+        Map<String, Object> map2 = Util.translateToMap("map: map to map", map);
+        assertEquals(map.toString(), map2.toString());
+
+        assertThatIllegalArgumentException().isThrownBy(() -> Util.translateToMap("map: string", "some string"))
+                        .withMessageContaining("map: string");
+    }
+
+    @Data
+    @Builder
+    public static class Abc {
+        private int intValue;
+        private String strValue;
+        private String anotherString;
+    }
+
+    // this shares some fields with Abc so the data should transfer
+    @Data
+    @Builder
+    public static class Similar {
+        private int intValue;
+        private String strValue;
+    }
+
+    // throws an exception when getXxx() is used
+    public static class DataWithException {
+        @SuppressWarnings("unused")
+        private int intValue;
+
+        public int getIntValue() {
+            throw new IllegalStateException();
+        }
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContextTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContextTest.java
new file mode 100644 (file)
index 0000000..0d917ad
--- /dev/null
@@ -0,0 +1,87 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.controlloop;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+import java.util.Map;
+import java.util.UUID;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+
+public class ControlLoopEventContextTest {
+    private static final UUID REQ_ID = UUID.randomUUID();
+
+    private Map<String, String> enrichment;
+    private VirtualControlLoopEvent event;
+    private ControlLoopEventContext context;
+
+    /**
+     * Initializes data, including {@link #context}.
+     */
+    @Before
+    public void setUp() {
+        enrichment = Map.of("abc", "one", "def", "two");
+
+        event = new VirtualControlLoopEvent();
+        event.setRequestId(REQ_ID);
+        event.setAai(enrichment);
+
+        context = new ControlLoopEventContext(event);
+    }
+
+    @Test
+    public void testControlLoopEventContext() {
+        assertSame(event, context.getEvent());
+        assertSame(REQ_ID, context.getRequestId());
+        assertEquals(enrichment, context.getEnrichment());
+
+        // null event
+        assertThatThrownBy(() -> new ControlLoopEventContext(null));
+
+        // no request id, no enrichment data
+        event.setRequestId(null);
+        event.setAai(null);
+        context = new ControlLoopEventContext(event);
+        assertSame(event, context.getEvent());
+        assertNotNull(context.getRequestId());
+        assertEquals(Map.of(), context.getEnrichment());
+    }
+
+    @Test
+    public void testContains_testGetProperty_testSetProperty() {
+        context.setProperty("abc", "a string");
+        context.setProperty("def", 100);
+
+        assertFalse(context.contains("ghi"));
+
+        String strValue = context.getProperty("abc");
+        assertEquals("a string", strValue);
+
+        int intValue = context.getProperty("def");
+        assertEquals(100, intValue);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImplTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImplTest.java
new file mode 100644 (file)
index 0000000..a209fb0
--- /dev/null
@@ -0,0 +1,384 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.parameters.ObjectValidationResult;
+import org.onap.policy.common.parameters.ValidationStatus;
+import org.onap.policy.controlloop.actorserviceprovider.Operator;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException;
+
+public class ActorImplTest {
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+    private static final String ACTOR_NAME = "my-actor";
+    private static final String OPER1 = "add";
+    private static final String OPER2 = "subtract";
+    private static final String OPER3 = "multiply";
+    private static final String OPER4 = "divide";
+
+    private MyOper oper1;
+    private MyOper oper2;
+    private MyOper oper3;
+    private MyOper oper4;
+
+    private Map<String, Object> sub1;
+    private Map<String, Object> sub2;
+    private Map<String, Object> sub3;
+    private Map<String, Object> sub4;
+    private Map<String, Object> params;
+
+    private ActorImpl actor;
+
+
+    /**
+     * Initializes the fields, including a fully populated {@link #actor}.
+     */
+    @Before
+    public void setUp() {
+        oper1 = spy(new MyOper(OPER1));
+        oper2 = spy(new MyOper(OPER2));
+        oper3 = spy(new MyOper(OPER3));
+        oper4 = spy(new MyOper(OPER4));
+
+        sub1 = Map.of("sub A", "value A");
+        sub2 = Map.of("sub B", "value B");
+        sub3 = Map.of("sub C", "value C");
+        sub4 = Map.of("sub D", "value D");
+
+        params = Map.of(OPER1, sub1, OPER2, sub2, OPER3, sub3, OPER4, sub4);
+
+        actor = makeActor(oper1, oper2, oper3, oper4);
+    }
+
+    @Test
+    public void testActorImpl_testGetName() {
+        assertEquals(ACTOR_NAME, actor.getName());
+        assertEquals(4, actor.getOperationNames().size());
+    }
+
+    @Test
+    public void testDoStart() {
+        actor.configure(params);
+        assertEquals(4, actor.getOperationNames().size());
+
+        /*
+         * arrange for second operator to be unconfigured and the third operator to throw
+         * an exception
+         */
+        Iterator<Operator> iter = actor.getOperators().iterator();
+        iter.next();
+        when(iter.next().isConfigured()).thenReturn(false);
+        when(iter.next().start()).thenThrow(new IllegalStateException(EXPECTED_EXCEPTION));
+
+        /*
+         * Start the actor.
+         */
+        actor.start();
+        assertTrue(actor.isAlive());
+
+        iter = actor.getOperators().iterator();
+        verify(iter.next()).start();
+        // this one isn't configured, so shouldn't attempt to start it
+        verify(iter.next(), never()).start();
+        // this one threw an exception
+        iter.next();
+        verify(iter.next()).start();
+
+        // no other types of operations
+        verify(oper1, never()).stop();
+        verify(oper1, never()).shutdown();
+    }
+
+    @Test
+    public void testDoStop() {
+        actor.configure(params);
+        actor.start();
+        assertEquals(4, actor.getOperationNames().size());
+
+        // arrange for second operator to throw an exception
+        Iterator<Operator> iter = actor.getOperators().iterator();
+        iter.next();
+        when(iter.next().stop()).thenThrow(new IllegalStateException(EXPECTED_EXCEPTION));
+
+        /*
+         * Stop the actor.
+         */
+        actor.stop();
+        assertFalse(actor.isAlive());
+
+        iter = actor.getOperators().iterator();
+        verify(iter.next()).stop();
+        // this one threw an exception
+        iter.next();
+        verify(iter.next()).stop();
+        verify(iter.next()).stop();
+
+        // no additional types of operations
+        verify(oper1).configure(any());
+        verify(oper1).start();
+
+        // no other types of operation
+        verify(oper1, never()).shutdown();
+    }
+
+    @Test
+    public void testDoShutdown() {
+        actor.configure(params);
+        actor.start();
+        assertEquals(4, actor.getOperationNames().size());
+
+        // arrange for second operator to throw an exception
+        Iterator<Operator> iter = actor.getOperators().iterator();
+        iter.next();
+        doThrow(new IllegalStateException(EXPECTED_EXCEPTION)).when(iter.next()).shutdown();
+
+        /*
+         * Stop the actor.
+         */
+        actor.shutdown();
+        assertFalse(actor.isAlive());
+
+        iter = actor.getOperators().iterator();
+        verify(iter.next()).shutdown();
+        // this one threw an exception
+        iter.next();
+        verify(iter.next()).shutdown();
+        verify(iter.next()).shutdown();
+
+        // no additional types of operations
+        verify(oper1).configure(any());
+        verify(oper1).start();
+
+        // no other types of operation
+        verify(oper1, never()).stop();
+    }
+
+    @Test
+    public void testAddOperator() {
+        // cannot add operators if already configured
+        actor.configure(params);
+        assertThatIllegalStateException().isThrownBy(() -> actor.addOperator(oper1));
+
+        /*
+         * make an actor where operators two and four have names that are duplicates of
+         * the others
+         */
+        oper2 = spy(new MyOper(OPER1));
+        oper4 = spy(new MyOper(OPER3));
+
+        actor = makeActor(oper1, oper2, oper3, oper4);
+
+        assertEquals(2, actor.getOperationNames().size());
+
+        assertSame(oper1, actor.getOperator(OPER1));
+        assertSame(oper3, actor.getOperator(OPER3));
+    }
+
+    @Test
+    public void testGetOperator() {
+        assertSame(oper1, actor.getOperator(OPER1));
+        assertSame(oper3, actor.getOperator(OPER3));
+
+        assertThatIllegalArgumentException().isThrownBy(() -> actor.getOperator("unknown name"));
+    }
+
+    @Test
+    public void testGetOperators() {
+        // @formatter:off
+        assertEquals("[add, divide, multiply, subtract]",
+                        actor.getOperators().stream()
+                            .map(Operator::getName)
+                            .sorted()
+                            .collect(Collectors.toList())
+                            .toString());
+        // @formatter:on
+    }
+
+    @Test
+    public void testGetOperationNames() {
+        // @formatter:off
+        assertEquals("[add, divide, multiply, subtract]",
+                        actor.getOperationNames().stream()
+                            .sorted()
+                            .collect(Collectors.toList())
+                            .toString());
+        // @formatter:on
+    }
+
+    @Test
+    public void testDoConfigure() {
+        actor.configure(params);
+        assertTrue(actor.isConfigured());
+
+        verify(oper1).configure(sub1);
+        verify(oper2).configure(sub2);
+        verify(oper3).configure(sub3);
+        verify(oper4).configure(sub4);
+
+        // no other types of operations
+        verify(oper1, never()).start();
+        verify(oper1, never()).stop();
+        verify(oper1, never()).shutdown();
+    }
+
+    /**
+     * Tests doConfigure() where operators throw parameter validation and runtime
+     * exceptions.
+     */
+    @Test
+    public void testDoConfigureExceptions() {
+        makeValidException(oper1);
+        makeRuntimeException(oper2);
+        makeValidException(oper3);
+
+        actor.configure(params);
+        assertTrue(actor.isConfigured());
+    }
+
+    /**
+     * Tests doConfigure(). Arranges for the following:
+     * <ul>
+     * <li>one operator is configured, but has parameters</li>
+     * <li>another operator is configured, but has no parameters</li>
+     * <li>another operator has no parameters and is not configured</li>
+     * </ul>
+     */
+    @Test
+    public void testDoConfigureConfigure() {
+        // need mutable parameters
+        params = new TreeMap<>(params);
+
+        // configure one operator
+        oper1.configure(sub1);
+
+        // configure another and remove its parameters
+        oper2.configure(sub2);
+        params.remove(OPER2);
+
+        // create a new, unconfigured actor
+        Operator oper5 = spy(new MyOper("UNCONFIGURED"));
+        actor = makeActor(oper1, oper2, oper3, oper4, oper5);
+
+        /*
+         * Configure it.
+         */
+        actor.configure(params);
+        assertTrue(actor.isConfigured());
+
+        // this should have been configured again
+        verify(oper1, times(2)).configure(sub1);
+
+        // no parameters, so this should not have been configured again
+        verify(oper2).configure(sub2);
+
+        // these were only configured once
+        verify(oper3).configure(sub3);
+        verify(oper4).configure(sub4);
+
+        // never configured
+        verify(oper5, never()).configure(any());
+        assertFalse(oper5.isConfigured());
+
+        // start and verify that all are started except for the last
+        actor.start();
+        verify(oper1).start();
+        verify(oper2).start();
+        verify(oper3).start();
+        verify(oper4).start();
+        verify(oper5, never()).start();
+    }
+
+    /**
+     * Arranges for an operator to throw a validation exception when
+     * {@link Operator#configure(Map)} is invoked.
+     *
+     * @param oper operator of interest
+     */
+    private void makeValidException(Operator oper) {
+        ParameterValidationRuntimeException ex = new ParameterValidationRuntimeException(
+                        new ObjectValidationResult(actor.getName(), null, ValidationStatus.INVALID, "null"));
+        doThrow(ex).when(oper).configure(any());
+    }
+
+    /**
+     * Arranges for an operator to throw a runtime exception when
+     * {@link Operator#configure(Map)} is invoked.
+     *
+     * @param oper operator of interest
+     */
+    private void makeRuntimeException(Operator oper) {
+        IllegalStateException ex = new IllegalStateException(EXPECTED_EXCEPTION);
+        doThrow(ex).when(oper).configure(any());
+    }
+
+    @Test
+    public void testMakeOperatorParameters() {
+        actor.configure(params);
+
+        // each operator should have received its own parameters
+        verify(oper1).configure(sub1);
+        verify(oper2).configure(sub2);
+        verify(oper3).configure(sub3);
+        verify(oper4).configure(sub4);
+    }
+
+    /**
+     * Makes an actor with the given operators.
+     *
+     * @param operators associated operators
+     * @return a new actor
+     */
+    private ActorImpl makeActor(Operator... operators) {
+        ActorImpl actor = new ActorImpl(ACTOR_NAME);
+
+        for (Operator oper : operators) {
+            actor.addOperator(oper);
+        }
+
+        return actor;
+    }
+
+    private static class MyOper extends OperatorPartial implements Operator {
+
+        public MyOper(String name) {
+            super(ACTOR_NAME, name);
+        }
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActorTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActorTest.java
new file mode 100644 (file)
index 0000000..2da7899
--- /dev/null
@@ -0,0 +1,81 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Function;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpActorParams;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException;
+
+public class HttpActorTest {
+
+    private static final String ACTOR = "my-actor";
+    private static final String UNKNOWN = "unknown";
+    private static final String CLIENT = "my-client";
+    private static final long TIMEOUT = 10L;
+
+    private HttpActor actor;
+
+    @Before
+    public void setUp() {
+        actor = new HttpActor(ACTOR);
+    }
+
+    @Test
+    public void testMakeOperatorParameters() {
+        HttpActorParams params = new HttpActorParams();
+        params.setClientName(CLIENT);
+        params.setTimeoutSec(TIMEOUT);
+        params.setPath(Map.of("operA", "urlA", "operB", "urlB"));
+
+        final HttpActor prov = new HttpActor(ACTOR);
+        Function<String, Map<String, Object>> maker =
+                        prov.makeOperatorParameters(Util.translateToMap(prov.getName(), params));
+
+        assertNull(maker.apply(UNKNOWN));
+
+        // use a TreeMap to ensure the properties are sorted
+        assertEquals("{clientName=my-client, path=urlA, timeoutSec=10}",
+                        new TreeMap<>(maker.apply("operA")).toString());
+
+        assertEquals("{clientName=my-client, path=urlB, timeoutSec=10}",
+                        new TreeMap<>(maker.apply("operB")).toString());
+
+        // with invalid actor parameters
+        params.setClientName(null);
+        assertThatThrownBy(() -> prov.makeOperatorParameters(Util.translateToMap(prov.getName(), params)))
+                        .isInstanceOf(ParameterValidationRuntimeException.class);
+    }
+
+    @Test
+    public void testHttpActor() {
+        assertEquals(ACTOR, actor.getName());
+        assertEquals(ACTOR, actor.getFullName());
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperatorTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperatorTest.java
new file mode 100644 (file)
index 0000000..c006cf3
--- /dev/null
@@ -0,0 +1,104 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.common.endpoints.http.client.HttpClient;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactory;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException;
+
+public class HttpOperatorTest {
+
+    private static final String ACTOR = "my-actor";
+    private static final String OPERATION = "my-name";
+    private static final String CLIENT = "my-client";
+    private static final String PATH = "my-path";
+    private static final long TIMEOUT = 100;
+
+    @Mock
+    private HttpClient client;
+
+    private HttpOperator oper;
+
+    /**
+     * Initializes fields, including {@link #oper}.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        oper = new HttpOperator(ACTOR, OPERATION);
+    }
+
+    @Test
+    public void testDoConfigureMapOfStringObject_testGetClient_testGetPath_testGetTimeoutSec() {
+        assertNull(oper.getClient());
+        assertNull(oper.getPath());
+        assertEquals(0L, oper.getTimeoutSec());
+
+        oper = new HttpOperator(ACTOR, OPERATION) {
+            @Override
+            protected HttpClientFactory getClientFactory() {
+                HttpClientFactory factory = mock(HttpClientFactory.class);
+                when(factory.get(CLIENT)).thenReturn(client);
+                return factory;
+            }
+        };
+
+        HttpParams params = HttpParams.builder().clientName(CLIENT).path(PATH).timeoutSec(TIMEOUT).build();
+        Map<String, Object> paramMap = Util.translateToMap(OPERATION, params);
+        oper.configure(paramMap);
+
+        assertSame(client, oper.getClient());
+        assertEquals(PATH, oper.getPath());
+        assertEquals(TIMEOUT, oper.getTimeoutSec());
+
+        // test invalid parameters
+        paramMap.remove("path");
+        assertThatThrownBy(() -> oper.configure(paramMap)).isInstanceOf(ParameterValidationRuntimeException.class);
+    }
+
+    @Test
+    public void testHttpOperator() {
+        assertEquals(ACTOR, oper.getActorName());
+        assertEquals(OPERATION, oper.getName());
+        assertEquals(ACTOR + "." + OPERATION, oper.getFullName());
+    }
+
+    @Test
+    public void testGetClient() {
+        assertNotNull(oper.getClientFactory());
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartialTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartialTest.java
new file mode 100644 (file)
index 0000000..21bc656
--- /dev/null
@@ -0,0 +1,1290 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+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.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Queue;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import lombok.Getter;
+import lombok.Setter;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture;
+import org.onap.policy.controlloop.policy.PolicyResult;
+
+public class OperatorPartialTest {
+    private static final int MAX_PARALLEL_REQUESTS = 10;
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+    private static final String ACTOR = "my-actor";
+    private static final String OPERATOR = "my-operator";
+    private static final String TARGET = "my-target";
+    private static final int TIMEOUT = 1000;
+    private static final UUID REQ_ID = UUID.randomUUID();
+
+    private static final List<PolicyResult> FAILURE_RESULTS = Arrays.asList(PolicyResult.values()).stream()
+                    .filter(result -> result != PolicyResult.SUCCESS).collect(Collectors.toList());
+
+    private VirtualControlLoopEvent event;
+    private Map<String, Object> config;
+    private ControlLoopEventContext context;
+    private MyExec executor;
+    private ControlLoopOperationParams params;
+
+    private MyOper oper;
+
+    private int numStart;
+    private int numEnd;
+
+    private Instant tstart;
+
+    private OperationOutcome opstart;
+    private OperationOutcome opend;
+
+    /**
+     * Initializes the fields, including {@link #oper}.
+     */
+    @Before
+    public void setUp() {
+        event = new VirtualControlLoopEvent();
+        event.setRequestId(REQ_ID);
+
+        config = new TreeMap<>();
+        context = new ControlLoopEventContext(event);
+        executor = new MyExec();
+
+        params = ControlLoopOperationParams.builder().completeCallback(this::completer).context(context)
+                        .executor(executor).actor(ACTOR).operation(OPERATOR).timeoutSec(TIMEOUT)
+                        .startCallback(this::starter).targetEntity(TARGET).build();
+
+        oper = new MyOper();
+        oper.configure(new TreeMap<>());
+        oper.start();
+
+        tstart = null;
+
+        opstart = null;
+        opend = null;
+    }
+
+    @Test
+    public void testOperatorPartial_testGetActorName_testGetName() {
+        assertEquals(ACTOR, oper.getActorName());
+        assertEquals(OPERATOR, oper.getName());
+        assertEquals(ACTOR + "." + OPERATOR, oper.getFullName());
+    }
+
+    @Test
+    public void testGetBlockingExecutor() throws InterruptedException {
+        CountDownLatch latch = new CountDownLatch(1);
+
+        /*
+         * Use an operator that doesn't override getBlockingExecutor().
+         */
+        OperatorPartial oper2 = new OperatorPartial(ACTOR, OPERATOR) {};
+        oper2.getBlockingExecutor().execute(() -> latch.countDown());
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+    }
+
+    @Test
+    public void testDoConfigure() {
+        oper = spy(new MyOper());
+
+        oper.configure(config);
+        verify(oper).configure(config);
+
+        // repeat - SHOULD be run again
+        oper.configure(config);
+        verify(oper, times(2)).configure(config);
+    }
+
+    @Test
+    public void testDoStart() {
+        oper = spy(new MyOper());
+
+        oper.configure(config);
+        oper.start();
+
+        verify(oper).doStart();
+
+        // others should not have been invoked
+        verify(oper, never()).doStop();
+        verify(oper, never()).doShutdown();
+    }
+
+    @Test
+    public void testDoStop() {
+        oper = spy(new MyOper());
+
+        oper.configure(config);
+        oper.start();
+        oper.stop();
+
+        verify(oper).doStop();
+
+        // should not have been re-invoked
+        verify(oper).doStart();
+
+        // others should not have been invoked
+        verify(oper, never()).doShutdown();
+    }
+
+    @Test
+    public void testDoShutdown() {
+        oper = spy(new MyOper());
+
+        oper.configure(config);
+        oper.start();
+        oper.shutdown();
+
+        verify(oper).doShutdown();
+
+        // should not have been re-invoked
+        verify(oper).doStart();
+
+        // others should not have been invoked
+        verify(oper, never()).doStop();
+    }
+
+    @Test
+    public void testStartOperation() {
+        verifyRun("testStartOperation", 1, 1, PolicyResult.SUCCESS);
+    }
+
+    /**
+     * Tests startOperation() when the operator is not running.
+     */
+    @Test
+    public void testStartOperationNotRunning() {
+        // use a new operator, one that hasn't been started yet
+        oper = new MyOper();
+        oper.configure(new TreeMap<>());
+
+        assertThatIllegalStateException().isThrownBy(() -> oper.startOperation(params));
+    }
+
+    /**
+     * Tests startOperation() when the operation has a preprocessor.
+     */
+    @Test
+    public void testStartOperationWithPreprocessor() {
+        AtomicInteger count = new AtomicInteger();
+
+        CompletableFuture<OperationOutcome> preproc = CompletableFuture.supplyAsync(() -> {
+            count.incrementAndGet();
+            return makeSuccess();
+        }, executor);
+
+        oper.setPreProcessor(preproc);
+
+        verifyRun("testStartOperationWithPreprocessor_testStartPreprocessor", 1, 1, PolicyResult.SUCCESS);
+
+        assertEquals(1, count.get());
+    }
+
+    /**
+     * Tests startOperation() with multiple running requests.
+     */
+    @Test
+    public void testStartOperationMultiple() {
+        for (int count = 0; count < MAX_PARALLEL_REQUESTS; ++count) {
+            oper.startOperation(params);
+        }
+
+        assertTrue(executor.runAll());
+
+        assertNotNull(opstart);
+        assertNotNull(opend);
+        assertEquals(PolicyResult.SUCCESS, opend.getResult());
+
+        assertEquals(MAX_PARALLEL_REQUESTS, numStart);
+        assertEquals(MAX_PARALLEL_REQUESTS, oper.getCount());
+        assertEquals(MAX_PARALLEL_REQUESTS, numEnd);
+    }
+
+    /**
+     * Tests startPreprocessor() when the preprocessor returns a failure.
+     */
+    @Test
+    public void testStartPreprocessorFailure() {
+        oper.setPreProcessor(CompletableFuture.completedFuture(makeFailure()));
+
+        verifyRun("testStartPreprocessorFailure", 1, 0, PolicyResult.FAILURE_GUARD);
+    }
+
+    /**
+     * Tests startPreprocessor() when the preprocessor throws an exception.
+     */
+    @Test
+    public void testStartPreprocessorException() {
+        // arrange for the preprocessor to throw an exception
+        oper.setPreProcessor(CompletableFuture.failedFuture(new IllegalStateException(EXPECTED_EXCEPTION)));
+
+        verifyRun("testStartPreprocessorException", 1, 0, PolicyResult.FAILURE_GUARD);
+    }
+
+    /**
+     * Tests startPreprocessor() when the pipeline is not running.
+     */
+    @Test
+    public void testStartPreprocessorNotRunning() {
+        // arrange for the preprocessor to return success, which will be ignored
+        oper.setPreProcessor(CompletableFuture.completedFuture(makeSuccess()));
+
+        oper.startOperation(params).cancel(false);
+        assertTrue(executor.runAll());
+
+        assertNull(opstart);
+        assertNull(opend);
+
+        assertEquals(0, numStart);
+        assertEquals(0, oper.getCount());
+        assertEquals(0, numEnd);
+    }
+
+    /**
+     * Tests startPreprocessor() when the preprocessor <b>builder</b> throws an exception.
+     */
+    @Test
+    public void testStartPreprocessorBuilderException() {
+        oper = new MyOper() {
+            @Override
+            protected CompletableFuture<OperationOutcome> startPreprocessorAsync(ControlLoopOperationParams params) {
+                throw new IllegalStateException(EXPECTED_EXCEPTION);
+            }
+        };
+
+        oper.configure(new TreeMap<>());
+        oper.start();
+
+        assertThatIllegalStateException().isThrownBy(() -> oper.startOperation(params));
+
+        // should be nothing in the queue
+        assertEquals(0, executor.getQueueLength());
+    }
+
+    @Test
+    public void testStartPreprocessorAsync() {
+        assertNull(oper.startPreprocessorAsync(params));
+    }
+
+    @Test
+    public void testStartOperationAsync() {
+        oper.startOperation(params);
+        assertTrue(executor.runAll());
+
+        assertEquals(1, oper.getCount());
+    }
+
+    @Test
+    public void testIsSuccess() {
+        OperationOutcome outcome = new OperationOutcome();
+
+        outcome.setResult(PolicyResult.SUCCESS);
+        assertTrue(oper.isSuccess(outcome));
+
+        for (PolicyResult failure : FAILURE_RESULTS) {
+            outcome.setResult(failure);
+            assertFalse("testIsSuccess-" + failure, oper.isSuccess(outcome));
+        }
+    }
+
+    @Test
+    public void testIsActorFailed() {
+        assertFalse(oper.isActorFailed(null));
+
+        OperationOutcome outcome = params.makeOutcome();
+
+        // incorrect outcome
+        outcome.setResult(PolicyResult.SUCCESS);
+        assertFalse(oper.isActorFailed(outcome));
+
+        outcome.setResult(PolicyResult.FAILURE_RETRIES);
+        assertFalse(oper.isActorFailed(outcome));
+
+        // correct outcome
+        outcome.setResult(PolicyResult.FAILURE);
+
+        // incorrect actor
+        outcome.setActor(TARGET);
+        assertFalse(oper.isActorFailed(outcome));
+        outcome.setActor(null);
+        assertFalse(oper.isActorFailed(outcome));
+        outcome.setActor(ACTOR);
+
+        // incorrect operation
+        outcome.setOperation(TARGET);
+        assertFalse(oper.isActorFailed(outcome));
+        outcome.setOperation(null);
+        assertFalse(oper.isActorFailed(outcome));
+        outcome.setOperation(OPERATOR);
+
+        // correct values
+        assertTrue(oper.isActorFailed(outcome));
+    }
+
+    @Test
+    public void testDoOperation() {
+        /*
+         * Use an operator that doesn't override doOperation().
+         */
+        OperatorPartial oper2 = new OperatorPartial(ACTOR, OPERATOR) {
+            @Override
+            protected Executor getBlockingExecutor() {
+                return executor;
+            }
+        };
+
+        oper2.configure(new TreeMap<>());
+        oper2.start();
+
+        oper2.startOperation(params);
+        assertTrue(executor.runAll());
+
+        assertNotNull(opend);
+        assertEquals(PolicyResult.FAILURE_EXCEPTION, opend.getResult());
+    }
+
+    @Test
+    public void testTimeout() throws Exception {
+
+        // use a real executor
+        params = params.toBuilder().executor(ForkJoinPool.commonPool()).build();
+
+        // trigger timeout very quickly
+        oper = new MyOper() {
+            @Override
+            protected long getTimeOutMillis(Integer timeoutSec) {
+                return 1;
+            }
+
+            @Override
+            protected CompletableFuture<OperationOutcome> startOperationAsync(ControlLoopOperationParams params,
+                            int attempt, OperationOutcome outcome) {
+
+                OperationOutcome outcome2 = params.makeOutcome();
+                outcome2.setResult(PolicyResult.SUCCESS);
+
+                /*
+                 * Create an incomplete future that will timeout after the operation's
+                 * timeout. If it fires before the other timer, then it will return a
+                 * SUCCESS outcome.
+                 */
+                CompletableFuture<OperationOutcome> future = new CompletableFuture<>();
+                future = future.orTimeout(1, TimeUnit.SECONDS).handleAsync((unused1, unused2) -> outcome,
+                                params.getExecutor());
+
+                return future;
+            }
+        };
+
+        oper.configure(new TreeMap<>());
+        oper.start();
+
+        assertEquals(PolicyResult.FAILURE_TIMEOUT, oper.startOperation(params).get().getResult());
+    }
+
+    /**
+     * Verifies that the timer doesn't encompass the preprocessor and doesn't stop the
+     * operation once the preprocessor completes.
+     */
+    @Test
+    public void testTimeoutInPreprocessor() throws Exception {
+
+        // use a real executor
+        params = params.toBuilder().executor(ForkJoinPool.commonPool()).build();
+
+        // trigger timeout very quickly
+        oper = new MyOper() {
+            @Override
+            protected long getTimeOutMillis(Integer timeoutSec) {
+                return 10;
+            }
+
+            @Override
+            protected Executor getBlockingExecutor() {
+                return command -> {
+                    Thread thread = new Thread(command);
+                    thread.start();
+                };
+            }
+
+            @Override
+            protected CompletableFuture<OperationOutcome> startPreprocessorAsync(ControlLoopOperationParams params) {
+
+                OperationOutcome outcome = makeSuccess();
+
+                /*
+                 * Create an incomplete future that will timeout after the operation's
+                 * timeout. If it fires before the other timer, then it will return a
+                 * SUCCESS outcome.
+                 */
+                CompletableFuture<OperationOutcome> future = new CompletableFuture<>();
+                future = future.orTimeout(200, TimeUnit.MILLISECONDS).handleAsync((unused1, unused2) -> outcome,
+                                params.getExecutor());
+
+                return future;
+            }
+        };
+
+        oper.configure(new TreeMap<>());
+        oper.start();
+
+        OperationOutcome result = oper.startOperation(params).get();
+        assertEquals(PolicyResult.SUCCESS, result.getResult());
+
+        assertNotNull(opstart);
+        assertNotNull(opend);
+        assertEquals(PolicyResult.SUCCESS, opend.getResult());
+
+        assertEquals(1, numStart);
+        assertEquals(1, oper.getCount());
+        assertEquals(1, numEnd);
+    }
+
+    /**
+     * Tests retry functions, when the count is set to zero and retries are exhausted.
+     */
+    @Test
+    public void testSetRetryFlag_testRetryOnFailure_ZeroRetries_testStartOperationAttempt() {
+        params = params.toBuilder().retry(0).build();
+        oper.setMaxFailures(10);
+
+        verifyRun("testSetRetryFlag_testRetryOnFailure_ZeroRetries", 1, 1, PolicyResult.FAILURE);
+    }
+
+    /**
+     * Tests retry functions, when the count is null and retries are exhausted.
+     */
+    @Test
+    public void testSetRetryFlag_testRetryOnFailure_NullRetries() {
+        params = params.toBuilder().retry(null).build();
+        oper.setMaxFailures(10);
+
+        verifyRun("testSetRetryFlag_testRetryOnFailure_NullRetries", 1, 1, PolicyResult.FAILURE);
+    }
+
+    /**
+     * Tests retry functions, when retries are exhausted.
+     */
+    @Test
+    public void testSetRetryFlag_testRetryOnFailure_RetriesExhausted() {
+        final int maxRetries = 3;
+        params = params.toBuilder().retry(maxRetries).build();
+        oper.setMaxFailures(10);
+
+        verifyRun("testSetRetryFlag_testRetryOnFailure_RetriesExhausted", maxRetries + 1, maxRetries + 1,
+                        PolicyResult.FAILURE_RETRIES);
+    }
+
+    /**
+     * Tests retry functions, when a success follows some retries.
+     */
+    @Test
+    public void testSetRetryFlag_testRetryOnFailure_SuccessAfterRetries() {
+        params = params.toBuilder().retry(10).build();
+
+        final int maxFailures = 3;
+        oper.setMaxFailures(maxFailures);
+
+        verifyRun("testSetRetryFlag_testRetryOnFailure_SuccessAfterRetries", maxFailures + 1, maxFailures + 1,
+                        PolicyResult.SUCCESS);
+    }
+
+    /**
+     * Tests retry functions, when the outcome is {@code null}.
+     */
+    @Test
+    public void testSetRetryFlag_testRetryOnFailure_NullOutcome() {
+
+        // arrange to return null from doOperation()
+        oper = new MyOper() {
+            @Override
+            protected OperationOutcome doOperation(ControlLoopOperationParams params, int attempt,
+                            OperationOutcome operation) {
+
+                // update counters
+                super.doOperation(params, attempt, operation);
+                return null;
+            }
+        };
+
+        oper.configure(new TreeMap<>());
+        oper.start();
+
+        verifyRun("testSetRetryFlag_testRetryOnFailure_NullOutcome", 1, 1, PolicyResult.FAILURE, null, noop());
+    }
+
+    @Test
+    public void testIsSameOperation() {
+        assertFalse(oper.isSameOperation(null));
+
+        OperationOutcome outcome = params.makeOutcome();
+
+        // wrong actor - should be false
+        outcome.setActor(null);
+        assertFalse(oper.isSameOperation(outcome));
+        outcome.setActor(TARGET);
+        assertFalse(oper.isSameOperation(outcome));
+        outcome.setActor(ACTOR);
+
+        // wrong operation - should be null
+        outcome.setOperation(null);
+        assertFalse(oper.isSameOperation(outcome));
+        outcome.setOperation(TARGET);
+        assertFalse(oper.isSameOperation(outcome));
+        outcome.setOperation(OPERATOR);
+
+        assertTrue(oper.isSameOperation(outcome));
+    }
+
+    /**
+     * Tests handleFailure() when the outcome is a success.
+     */
+    @Test
+    public void testHandlePreprocessorFailureTrue() {
+        oper.setPreProcessor(CompletableFuture.completedFuture(makeSuccess()));
+        verifyRun("testHandlePreprocessorFailureTrue", 1, 1, PolicyResult.SUCCESS);
+    }
+
+    /**
+     * Tests handleFailure() when the outcome is <i>not</i> a success.
+     */
+    @Test
+    public void testHandlePreprocessorFailureFalse() throws Exception {
+        oper.setPreProcessor(CompletableFuture.completedFuture(makeFailure()));
+        verifyRun("testHandlePreprocessorFailureFalse", 1, 0, PolicyResult.FAILURE_GUARD);
+    }
+
+    /**
+     * Tests handleFailure() when the outcome is {@code null}.
+     */
+    @Test
+    public void testHandlePreprocessorFailureNull() throws Exception {
+        // arrange to return null from the preprocessor
+        oper.setPreProcessor(CompletableFuture.completedFuture(null));
+
+        verifyRun("testHandlePreprocessorFailureNull", 1, 0, PolicyResult.FAILURE_GUARD);
+    }
+
+    @Test
+    public void testFromException() {
+        // arrange to generate an exception when operation runs
+        oper.setGenException(true);
+
+        verifyRun("testFromException", 1, 1, PolicyResult.FAILURE_EXCEPTION);
+    }
+
+    /**
+     * Tests fromException() when there is no exception.
+     */
+    @Test
+    public void testFromExceptionNoExcept() {
+        verifyRun("testFromExceptionNoExcept", 1, 1, PolicyResult.SUCCESS);
+    }
+
+    /**
+     * Tests both flavors of anyOf(), because one invokes the other.
+     */
+    @Test
+    public void testAnyOf() throws Exception {
+        // first task completes, others do not
+        List<CompletableFuture<OperationOutcome>> tasks = new LinkedList<>();
+
+        final OperationOutcome outcome = params.makeOutcome();
+
+        tasks.add(CompletableFuture.completedFuture(outcome));
+        tasks.add(new CompletableFuture<>());
+        tasks.add(new CompletableFuture<>());
+
+        CompletableFuture<OperationOutcome> result = oper.anyOf(params, tasks);
+        assertTrue(executor.runAll());
+
+        assertTrue(result.isDone());
+        assertSame(outcome, result.get());
+
+        // second task completes, others do not
+        tasks = new LinkedList<>();
+
+        tasks.add(new CompletableFuture<>());
+        tasks.add(CompletableFuture.completedFuture(outcome));
+        tasks.add(new CompletableFuture<>());
+
+        result = oper.anyOf(params, tasks);
+        assertTrue(executor.runAll());
+
+        assertTrue(result.isDone());
+        assertSame(outcome, result.get());
+
+        // third task completes, others do not
+        tasks = new LinkedList<>();
+
+        tasks.add(new CompletableFuture<>());
+        tasks.add(new CompletableFuture<>());
+        tasks.add(CompletableFuture.completedFuture(outcome));
+
+        result = oper.anyOf(params, tasks);
+        assertTrue(executor.runAll());
+
+        assertTrue(result.isDone());
+        assertSame(outcome, result.get());
+    }
+
+    /**
+     * Tests both flavors of allOf(), because one invokes the other.
+     */
+    @Test
+    public void testAllOf() throws Exception {
+        List<CompletableFuture<OperationOutcome>> tasks = new LinkedList<>();
+
+        final OperationOutcome outcome = params.makeOutcome();
+
+        CompletableFuture<OperationOutcome> future1 = new CompletableFuture<>();
+        CompletableFuture<OperationOutcome> future2 = new CompletableFuture<>();
+        CompletableFuture<OperationOutcome> future3 = new CompletableFuture<>();
+
+        tasks.add(future1);
+        tasks.add(future2);
+        tasks.add(future3);
+
+        CompletableFuture<OperationOutcome> result = oper.allOf(params, tasks);
+
+        assertTrue(executor.runAll());
+        assertFalse(result.isDone());
+        future1.complete(outcome);
+
+        // complete 3 before 2
+        assertTrue(executor.runAll());
+        assertFalse(result.isDone());
+        future3.complete(outcome);
+
+        assertTrue(executor.runAll());
+        assertFalse(result.isDone());
+        future2.complete(outcome);
+
+        // all of them are now done
+        assertTrue(executor.runAll());
+        assertTrue(result.isDone());
+        assertSame(outcome, result.get());
+    }
+
+    @Test
+    public void testCombineOutcomes() throws Exception {
+        // only one outcome
+        verifyOutcomes(0, PolicyResult.SUCCESS);
+        verifyOutcomes(0, PolicyResult.FAILURE_EXCEPTION);
+
+        // maximum is in different positions
+        verifyOutcomes(0, PolicyResult.FAILURE, PolicyResult.SUCCESS, PolicyResult.FAILURE_GUARD);
+        verifyOutcomes(1, PolicyResult.SUCCESS, PolicyResult.FAILURE, PolicyResult.FAILURE_GUARD);
+        verifyOutcomes(2, PolicyResult.SUCCESS, PolicyResult.FAILURE_GUARD, PolicyResult.FAILURE);
+
+        // null outcome
+        final List<CompletableFuture<OperationOutcome>> tasks = new LinkedList<>();
+        tasks.add(CompletableFuture.completedFuture(null));
+        CompletableFuture<OperationOutcome> result = oper.allOf(params, tasks);
+
+        assertTrue(executor.runAll());
+        assertTrue(result.isDone());
+        assertNull(result.get());
+
+        // one throws an exception during execution
+        IllegalStateException except = new IllegalStateException(EXPECTED_EXCEPTION);
+
+        tasks.clear();
+        tasks.add(CompletableFuture.completedFuture(params.makeOutcome()));
+        tasks.add(CompletableFuture.failedFuture(except));
+        tasks.add(CompletableFuture.completedFuture(params.makeOutcome()));
+        result = oper.allOf(params, tasks);
+
+        assertTrue(executor.runAll());
+        assertTrue(result.isCompletedExceptionally());
+        result.whenComplete((unused, thrown) -> assertSame(except, thrown));
+    }
+
+    private void verifyOutcomes(int expected, PolicyResult... results) throws Exception {
+        List<CompletableFuture<OperationOutcome>> tasks = new LinkedList<>();
+
+
+        OperationOutcome expectedOutcome = null;
+
+        for (int count = 0; count < results.length; ++count) {
+            OperationOutcome outcome = params.makeOutcome();
+            outcome.setResult(results[count]);
+            tasks.add(CompletableFuture.completedFuture(outcome));
+
+            if (count == expected) {
+                expectedOutcome = outcome;
+            }
+        }
+
+        CompletableFuture<OperationOutcome> result = oper.allOf(params, tasks);
+
+        assertTrue(executor.runAll());
+        assertTrue(result.isDone());
+        assertSame(expectedOutcome, result.get());
+    }
+
+    private Function<OperationOutcome, CompletableFuture<OperationOutcome>> makeTask(
+                    final OperationOutcome taskOutcome) {
+
+        return outcome -> CompletableFuture.completedFuture(taskOutcome);
+    }
+
+    @Test
+    public void testDetmPriority() {
+        assertEquals(1, oper.detmPriority(null));
+
+        OperationOutcome outcome = params.makeOutcome();
+
+        Map<PolicyResult, Integer> map = Map.of(PolicyResult.SUCCESS, 0, PolicyResult.FAILURE_GUARD, 2,
+                        PolicyResult.FAILURE_RETRIES, 3, PolicyResult.FAILURE, 4, PolicyResult.FAILURE_TIMEOUT, 5,
+                        PolicyResult.FAILURE_EXCEPTION, 6);
+
+        for (Entry<PolicyResult, Integer> ent : map.entrySet()) {
+            outcome.setResult(ent.getKey());
+            assertEquals(ent.getKey().toString(), ent.getValue().intValue(), oper.detmPriority(outcome));
+        }
+    }
+
+    /**
+     * Tests doTask(Future) when the controller is not running.
+     */
+    @Test
+    public void testDoTaskFutureNotRunning() throws Exception {
+        CompletableFuture<OperationOutcome> taskFuture = new CompletableFuture<>();
+
+        PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+        controller.complete(params.makeOutcome());
+
+        CompletableFuture<OperationOutcome> future =
+                        oper.doTask(params, controller, false, params.makeOutcome(), taskFuture);
+        assertFalse(future.isDone());
+        assertTrue(executor.runAll());
+
+        // should not have run the task
+        assertFalse(future.isDone());
+
+        // should have canceled the task future
+        assertTrue(taskFuture.isCancelled());
+    }
+
+    /**
+     * Tests doTask(Future) when the previous outcome was successful.
+     */
+    @Test
+    public void testDoTaskFutureSuccess() throws Exception {
+        CompletableFuture<OperationOutcome> taskFuture = new CompletableFuture<>();
+        final OperationOutcome taskOutcome = params.makeOutcome();
+
+        PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        CompletableFuture<OperationOutcome> future =
+                        oper.doTask(params, controller, true, params.makeOutcome(), taskFuture);
+
+        taskFuture.complete(taskOutcome);
+        assertTrue(executor.runAll());
+
+        assertTrue(future.isDone());
+        assertSame(taskOutcome, future.get());
+
+        // controller should not be done yet
+        assertFalse(controller.isDone());
+    }
+
+    /**
+     * Tests doTask(Future) when the previous outcome was failed.
+     */
+    @Test
+    public void testDoTaskFutureFailure() throws Exception {
+        CompletableFuture<OperationOutcome> taskFuture = new CompletableFuture<>();
+        final OperationOutcome failedOutcome = params.makeOutcome();
+        failedOutcome.setResult(PolicyResult.FAILURE);
+
+        PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, true, failedOutcome, taskFuture);
+        assertFalse(future.isDone());
+        assertTrue(executor.runAll());
+
+        // should not have run the task
+        assertFalse(future.isDone());
+
+        // should have canceled the task future
+        assertTrue(taskFuture.isCancelled());
+
+        // controller SHOULD be done now
+        assertTrue(controller.isDone());
+        assertSame(failedOutcome, controller.get());
+    }
+
+    /**
+     * Tests doTask(Future) when the previous outcome was failed, but not checking
+     * success.
+     */
+    @Test
+    public void testDoTaskFutureUncheckedFailure() throws Exception {
+        CompletableFuture<OperationOutcome> taskFuture = new CompletableFuture<>();
+        final OperationOutcome failedOutcome = params.makeOutcome();
+        failedOutcome.setResult(PolicyResult.FAILURE);
+
+        PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, false, failedOutcome, taskFuture);
+        assertFalse(future.isDone());
+
+        // complete the task
+        OperationOutcome taskOutcome = params.makeOutcome();
+        taskFuture.complete(taskOutcome);
+
+        assertTrue(executor.runAll());
+
+        // should have run the task
+        assertTrue(future.isDone());
+
+        assertTrue(future.isDone());
+        assertSame(taskOutcome, future.get());
+
+        // controller should not be done yet
+        assertFalse(controller.isDone());
+    }
+
+    /**
+     * Tests doTask(Function) when the controller is not running.
+     */
+    @Test
+    public void testDoTaskFunctionNotRunning() throws Exception {
+        AtomicBoolean invoked = new AtomicBoolean();
+
+        Function<OperationOutcome, CompletableFuture<OperationOutcome>> task = outcome -> {
+            invoked.set(true);
+            return CompletableFuture.completedFuture(params.makeOutcome());
+        };
+
+        PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+        controller.complete(params.makeOutcome());
+
+        CompletableFuture<OperationOutcome> future =
+                        oper.doTask(params, controller, false, task).apply(params.makeOutcome());
+        assertFalse(future.isDone());
+        assertTrue(executor.runAll());
+
+        // should not have run the task
+        assertFalse(future.isDone());
+
+        // should not have even invoked the task
+        assertFalse(invoked.get());
+    }
+
+    /**
+     * Tests doTask(Function) when the previous outcome was successful.
+     */
+    @Test
+    public void testDoTaskFunctionSuccess() throws Exception {
+        final OperationOutcome taskOutcome = params.makeOutcome();
+
+        final OperationOutcome failedOutcome = params.makeOutcome();
+
+        Function<OperationOutcome, CompletableFuture<OperationOutcome>> task = makeTask(taskOutcome);
+
+        PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, true, task).apply(failedOutcome);
+
+        assertTrue(future.isDone());
+        assertSame(taskOutcome, future.get());
+
+        // controller should not be done yet
+        assertFalse(controller.isDone());
+    }
+
+    /**
+     * Tests doTask(Function) when the previous outcome was failed.
+     */
+    @Test
+    public void testDoTaskFunctionFailure() throws Exception {
+        final OperationOutcome failedOutcome = params.makeOutcome();
+        failedOutcome.setResult(PolicyResult.FAILURE);
+
+        AtomicBoolean invoked = new AtomicBoolean();
+
+        Function<OperationOutcome, CompletableFuture<OperationOutcome>> task = outcome -> {
+            invoked.set(true);
+            return CompletableFuture.completedFuture(params.makeOutcome());
+        };
+
+        PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, true, task).apply(failedOutcome);
+        assertFalse(future.isDone());
+        assertTrue(executor.runAll());
+
+        // should not have run the task
+        assertFalse(future.isDone());
+
+        // should not have even invoked the task
+        assertFalse(invoked.get());
+
+        // controller should have the failed task
+        assertTrue(controller.isDone());
+        assertSame(failedOutcome, controller.get());
+    }
+
+    /**
+     * Tests doTask(Function) when the previous outcome was failed, but not checking
+     * success.
+     */
+    @Test
+    public void testDoTaskFunctionUncheckedFailure() throws Exception {
+        final OperationOutcome taskOutcome = params.makeOutcome();
+
+        final OperationOutcome failedOutcome = params.makeOutcome();
+        failedOutcome.setResult(PolicyResult.FAILURE);
+
+        Function<OperationOutcome, CompletableFuture<OperationOutcome>> task = makeTask(taskOutcome);
+
+        PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>();
+
+        CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, false, task).apply(failedOutcome);
+
+        assertTrue(future.isDone());
+        assertSame(taskOutcome, future.get());
+
+        // controller should not be done yet
+        assertFalse(controller.isDone());
+    }
+
+    /**
+     * Tests callbackStarted() when the pipeline has already been stopped.
+     */
+    @Test
+    public void testCallbackStartedNotRunning() {
+        AtomicReference<Future<OperationOutcome>> future = new AtomicReference<>();
+
+        /*
+         * arrange to stop the controller when the start-callback is invoked, but capture
+         * the outcome
+         */
+        params = params.toBuilder().startCallback(oper -> {
+            starter(oper);
+            future.get().cancel(false);
+        }).build();
+
+        future.set(oper.startOperation(params));
+        assertTrue(executor.runAll());
+
+        // should have only run once
+        assertEquals(1, numStart);
+    }
+
+    /**
+     * Tests callbackCompleted() when the pipeline has already been stopped.
+     */
+    @Test
+    public void testCallbackCompletedNotRunning() {
+        AtomicReference<Future<OperationOutcome>> future = new AtomicReference<>();
+
+        // arrange to stop the controller when the start-callback is invoked
+        params = params.toBuilder().startCallback(oper -> {
+            future.get().cancel(false);
+        }).build();
+
+        future.set(oper.startOperation(params));
+        assertTrue(executor.runAll());
+
+        // should not have been set
+        assertNull(opend);
+        assertEquals(0, numEnd);
+    }
+
+    @Test
+    public void testSetOutcomeControlLoopOperationOutcomeThrowable() {
+        final CompletionException timex = new CompletionException(new TimeoutException(EXPECTED_EXCEPTION));
+
+        OperationOutcome outcome;
+
+        outcome = new OperationOutcome();
+        oper.setOutcome(params, outcome, timex);
+        assertEquals(ControlLoopOperation.FAILED_MSG, outcome.getMessage());
+        assertEquals(PolicyResult.FAILURE_TIMEOUT, outcome.getResult());
+
+        outcome = new OperationOutcome();
+        oper.setOutcome(params, outcome, new IllegalStateException(EXPECTED_EXCEPTION));
+        assertEquals(ControlLoopOperation.FAILED_MSG, outcome.getMessage());
+        assertEquals(PolicyResult.FAILURE_EXCEPTION, outcome.getResult());
+    }
+
+    @Test
+    public void testSetOutcomeControlLoopOperationOutcomePolicyResult() {
+        OperationOutcome outcome;
+
+        outcome = new OperationOutcome();
+        oper.setOutcome(params, outcome, PolicyResult.SUCCESS);
+        assertEquals(ControlLoopOperation.SUCCESS_MSG, outcome.getMessage());
+        assertEquals(PolicyResult.SUCCESS, outcome.getResult());
+
+        for (PolicyResult result : FAILURE_RESULTS) {
+            outcome = new OperationOutcome();
+            oper.setOutcome(params, outcome, result);
+            assertEquals(result.toString(), ControlLoopOperation.FAILED_MSG, outcome.getMessage());
+            assertEquals(result.toString(), result, outcome.getResult());
+        }
+    }
+
+    @Test
+    public void testIsTimeout() {
+        final TimeoutException timex = new TimeoutException(EXPECTED_EXCEPTION);
+
+        assertFalse(oper.isTimeout(new IllegalStateException(EXPECTED_EXCEPTION)));
+        assertFalse(oper.isTimeout(new IllegalStateException(timex)));
+        assertFalse(oper.isTimeout(new CompletionException(new IllegalStateException(timex))));
+        assertFalse(oper.isTimeout(new CompletionException(null)));
+        assertFalse(oper.isTimeout(new CompletionException(new CompletionException(timex))));
+
+        assertTrue(oper.isTimeout(timex));
+        assertTrue(oper.isTimeout(new CompletionException(timex)));
+    }
+
+    @Test
+    public void testGetTimeOutMillis() {
+        assertEquals(TIMEOUT * 1000, oper.getTimeOutMillis(params.getTimeoutSec()));
+
+        params = params.toBuilder().timeoutSec(null).build();
+        assertEquals(0, oper.getTimeOutMillis(params.getTimeoutSec()));
+    }
+
+    private void starter(OperationOutcome oper) {
+        ++numStart;
+        tstart = oper.getStart();
+        opstart = oper;
+    }
+
+    private void completer(OperationOutcome oper) {
+        ++numEnd;
+        opend = oper;
+    }
+
+    /**
+     * Gets a function that does nothing.
+     *
+     * @param <T> type of input parameter expected by the function
+     * @return a function that does nothing
+     */
+    private <T> Consumer<T> noop() {
+        return unused -> {
+        };
+    }
+
+    private OperationOutcome makeSuccess() {
+        OperationOutcome outcome = params.makeOutcome();
+        outcome.setResult(PolicyResult.SUCCESS);
+
+        return outcome;
+    }
+
+    private OperationOutcome makeFailure() {
+        OperationOutcome outcome = params.makeOutcome();
+        outcome.setResult(PolicyResult.FAILURE);
+
+        return outcome;
+    }
+
+    /**
+     * Verifies a run.
+     *
+     * @param testName test name
+     * @param expectedCallbacks number of callbacks expected
+     * @param expectedOperations number of operation invocations expected
+     * @param expectedResult expected outcome
+     */
+    private void verifyRun(String testName, int expectedCallbacks, int expectedOperations,
+                    PolicyResult expectedResult) {
+
+        String expectedSubRequestId =
+                        (expectedResult == PolicyResult.FAILURE_EXCEPTION ? null : String.valueOf(expectedOperations));
+
+        verifyRun(testName, expectedCallbacks, expectedOperations, expectedResult, expectedSubRequestId, noop());
+    }
+
+    /**
+     * Verifies a run.
+     *
+     * @param testName test name
+     * @param expectedCallbacks number of callbacks expected
+     * @param expectedOperations number of operation invocations expected
+     * @param expectedResult expected outcome
+     * @param expectedSubRequestId expected sub request ID
+     * @param manipulator function to modify the future returned by
+     *        {@link OperatorPartial#startOperation(ControlLoopOperationParams)} before
+     *        the tasks in the executor are run
+     */
+    private void verifyRun(String testName, int expectedCallbacks, int expectedOperations, PolicyResult expectedResult,
+                    String expectedSubRequestId, Consumer<CompletableFuture<OperationOutcome>> manipulator) {
+
+        CompletableFuture<OperationOutcome> future = oper.startOperation(params);
+
+        manipulator.accept(future);
+
+        assertTrue(testName, executor.runAll());
+
+        assertEquals(testName, expectedCallbacks, numStart);
+        assertEquals(testName, expectedCallbacks, numEnd);
+
+        if (expectedCallbacks > 0) {
+            assertNotNull(testName, opstart);
+            assertNotNull(testName, opend);
+            assertEquals(testName, expectedResult, opend.getResult());
+
+            assertSame(testName, tstart, opstart.getStart());
+            assertSame(testName, tstart, opend.getStart());
+
+            try {
+                assertTrue(future.isDone());
+                assertSame(testName, opend, future.get());
+
+            } catch (InterruptedException | ExecutionException e) {
+                throw new IllegalStateException(e);
+            }
+
+            if (expectedOperations > 0) {
+                assertEquals(testName, expectedSubRequestId, opend.getSubRequestId());
+            }
+        }
+
+        assertEquals(testName, expectedOperations, oper.getCount());
+    }
+
+    private class MyOper extends OperatorPartial {
+        @Getter
+        private int count = 0;
+
+        @Setter
+        private boolean genException;
+
+        @Setter
+        private int maxFailures = 0;
+
+        @Setter
+        private CompletableFuture<OperationOutcome> preProcessor;
+
+        public MyOper() {
+            super(ACTOR, OPERATOR);
+        }
+
+        @Override
+        protected OperationOutcome doOperation(ControlLoopOperationParams params, int attempt,
+                        OperationOutcome operation) {
+            ++count;
+            if (genException) {
+                throw new IllegalStateException(EXPECTED_EXCEPTION);
+            }
+
+            operation.setSubRequestId(String.valueOf(attempt));
+
+            if (count > maxFailures) {
+                operation.setResult(PolicyResult.SUCCESS);
+            } else {
+                operation.setResult(PolicyResult.FAILURE);
+            }
+
+            return operation;
+        }
+
+        @Override
+        protected CompletableFuture<OperationOutcome> startPreprocessorAsync(ControlLoopOperationParams params) {
+            return (preProcessor != null ? preProcessor : super.startPreprocessorAsync(params));
+        }
+
+        @Override
+        protected Executor getBlockingExecutor() {
+            return executor;
+        }
+    }
+
+    /**
+     * Executor that will run tasks until the queue is empty or a maximum number of tasks
+     * have been executed.
+     */
+    private static class MyExec implements Executor {
+        private static final int MAX_TASKS = MAX_PARALLEL_REQUESTS * 100;
+
+        private Queue<Runnable> commands = new LinkedList<>();
+
+        public MyExec() {
+            // do nothing
+        }
+
+        public int getQueueLength() {
+            return commands.size();
+        }
+
+        @Override
+        public void execute(Runnable command) {
+            commands.add(command);
+        }
+
+        public boolean runAll() {
+            for (int count = 0; count < MAX_TASKS && !commands.isEmpty(); ++count) {
+                commands.remove().run();
+            }
+
+            return commands.isEmpty();
+        }
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/StartConfigPartialTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/StartConfigPartialTest.java
new file mode 100644 (file)
index 0000000..7a822c1
--- /dev/null
@@ -0,0 +1,212 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.impl;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class StartConfigPartialTest {
+    private static final IllegalArgumentException EXPECTED_EXCEPTION =
+                    new IllegalArgumentException("expected exception");
+    private static final String MY_NAME = "my-name";
+    private static final String PARAMS = "config data";
+    private static final String PARAMS2 = "config data #2";
+    private static final String PARAMSX = "config data exception";
+
+    private StartConfigPartial<String> config;
+
+    /**
+     * Creates a config whose doXxx() methods do nothing.
+     */
+    @Before
+    public void setUp() {
+        config = new StartConfigPartial<>(MY_NAME) {
+            @Override
+            protected void doConfigure(String parameters) {
+                // do nothing
+            }
+
+            @Override
+            protected void doStart() {
+                // do nothing
+            }
+
+            @Override
+            protected void doStop() {
+                // do nothing
+            }
+
+            @Override
+            protected void doShutdown() {
+                // do nothing
+            }
+        };
+
+        config = spy(config);
+    }
+
+    @Test
+    public void testConfigImpl_testGetFullName() {
+        assertEquals(MY_NAME, config.getFullName());
+    }
+
+    @Test
+    public void testIsAlive() {
+        assertFalse(config.isAlive());
+    }
+
+    @Test
+    public void testIsConfigured_testConfigure() {
+        // throw an exception during doConfigure(), but should remain unconfigured
+        assertFalse(config.isConfigured());
+        doThrow(EXPECTED_EXCEPTION).when(config).doConfigure(PARAMSX);
+        assertThatIllegalArgumentException().isThrownBy(() -> config.configure(PARAMSX)).isEqualTo(EXPECTED_EXCEPTION);
+        assertFalse(config.isConfigured());
+
+        assertFalse(config.isConfigured());
+        config.configure(PARAMS);
+        verify(config).doConfigure(PARAMS);
+        assertTrue(config.isConfigured());
+
+        // should not be able to re-configure while running
+        config.start();
+        assertThatIllegalStateException().isThrownBy(() -> config.configure(PARAMS2)).withMessageContaining(MY_NAME);
+        verify(config, never()).doConfigure(PARAMS2);
+
+        // should be able to re-configure after stopping
+        config.stop();
+        config.configure(PARAMS2);
+        verify(config).doConfigure(PARAMS2);
+        assertTrue(config.isConfigured());
+
+        // should remain configured after exception
+        doThrow(EXPECTED_EXCEPTION).when(config).doConfigure(PARAMSX);
+        assertThatIllegalArgumentException().isThrownBy(() -> config.configure(PARAMSX)).isEqualTo(EXPECTED_EXCEPTION);
+        assertTrue(config.isConfigured());
+    }
+
+    @Test
+    public void testStart() {
+        assertFalse(config.isAlive());
+
+        // can't start if not configured yet
+        assertThatIllegalStateException().isThrownBy(() -> config.start()).withMessageContaining(MY_NAME);
+        assertFalse(config.isAlive());
+
+        config.configure(PARAMS);
+
+        config.start();
+        verify(config).doStart();
+        assertTrue(config.isAlive());
+        assertTrue(config.isConfigured());
+
+        // ok to restart when running, but shouldn't invoke doStart() again
+        config.start();
+        verify(config).doStart();
+        assertTrue(config.isAlive());
+        assertTrue(config.isConfigured());
+
+        // should never have invoked these
+        verify(config, never()).doStop();
+        verify(config, never()).doShutdown();
+
+        // throw exception when started again, but should remain stopped
+        config.stop();
+        doThrow(EXPECTED_EXCEPTION).when(config).doStart();
+        assertThatIllegalArgumentException().isThrownBy(() -> config.start()).isEqualTo(EXPECTED_EXCEPTION);
+        assertFalse(config.isAlive());
+        assertTrue(config.isConfigured());
+    }
+
+    @Test
+    public void testStop() {
+        config.configure(PARAMS);
+
+        // ok to stop if not running, but shouldn't invoke doStop()
+        config.stop();
+        verify(config, never()).doStop();
+        assertFalse(config.isAlive());
+        assertTrue(config.isConfigured());
+
+        config.start();
+
+        // now stop should have an effect
+        config.stop();
+        verify(config).doStop();
+        assertFalse(config.isAlive());
+        assertTrue(config.isConfigured());
+
+        // should have only invoked this once
+        verify(config).doStart();
+
+        // should never have invoked these
+        verify(config, never()).doShutdown();
+
+        // throw exception when stopped again, but should go ahead and stop
+        config.start();
+        doThrow(EXPECTED_EXCEPTION).when(config).doStop();
+        assertThatIllegalArgumentException().isThrownBy(() -> config.stop()).isEqualTo(EXPECTED_EXCEPTION);
+        assertFalse(config.isAlive());
+        assertTrue(config.isConfigured());
+    }
+
+    @Test
+    public void testShutdown() {
+        config.configure(PARAMS);
+
+        // ok to shutdown if not running, but shouldn't invoke doShutdown()
+        config.shutdown();
+        verify(config, never()).doShutdown();
+        assertFalse(config.isAlive());
+        assertTrue(config.isConfigured());
+
+        config.start();
+
+        // now stop should have an effect
+        config.shutdown();
+        verify(config).doShutdown();
+        assertFalse(config.isAlive());
+        assertTrue(config.isConfigured());
+
+        // should have only invoked this once
+        verify(config).doStart();
+
+        // should never have invoked these
+        verify(config, never()).doStop();
+
+        // throw exception when shut down again, but should go ahead and shut down
+        config.start();
+        doThrow(EXPECTED_EXCEPTION).when(config).doShutdown();
+        assertThatIllegalArgumentException().isThrownBy(() -> config.shutdown()).isEqualTo(EXPECTED_EXCEPTION);
+        assertFalse(config.isAlive());
+        assertTrue(config.isConfigured());
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParamsTest.java
new file mode 100644 (file)
index 0000000..9dd19d5
--- /dev/null
@@ -0,0 +1,330 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+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.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.common.parameters.BeanValidationResult;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.actorserviceprovider.ActorService;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.Operator;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams.ControlLoopOperationParamsBuilder;
+import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.onap.policy.controlloop.policy.Target;
+
+public class ControlLoopOperationParamsTest {
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+    private static final String ACTOR = "my-actor";
+    private static final String OPERATION = "my-operation";
+    private static final Target TARGET = new Target();
+    private static final String TARGET_ENTITY = "my-target";
+    private static final Integer RETRY = 3;
+    private static final Integer TIMEOUT = 100;
+    private static final UUID REQ_ID = UUID.randomUUID();
+
+    @Mock
+    private Actor actor;
+
+    @Mock
+    private ActorService actorService;
+
+    @Mock
+    private Consumer<OperationOutcome> completer;
+
+    @Mock
+    private ControlLoopEventContext context;
+
+    @Mock
+    private VirtualControlLoopEvent event;
+
+    @Mock
+    private Executor executor;
+
+    @Mock
+    private CompletableFuture<OperationOutcome> operation;
+
+    @Mock
+    private Operator operator;
+
+    @Mock
+    private Consumer<OperationOutcome> starter;
+
+    private Map<String, String> payload;
+
+    private ControlLoopOperationParams params;
+    private OperationOutcome outcome;
+
+
+    /**
+     * Initializes mocks and sets {@link #params} to a fully-loaded set of parameters.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(actorService.getActor(ACTOR)).thenReturn(actor);
+        when(actor.getOperator(OPERATION)).thenReturn(operator);
+        when(operator.startOperation(any())).thenReturn(operation);
+
+        when(event.getRequestId()).thenReturn(REQ_ID);
+
+        when(context.getEvent()).thenReturn(event);
+
+        payload = new TreeMap<>();
+
+        params = ControlLoopOperationParams.builder().actorService(actorService).completeCallback(completer)
+                        .context(context).executor(executor).actor(ACTOR).operation(OPERATION).payload(payload)
+                        .retry(RETRY).target(TARGET).targetEntity(TARGET_ENTITY).timeoutSec(TIMEOUT)
+                        .startCallback(starter).build();
+
+        outcome = params.makeOutcome();
+    }
+
+    @Test
+    public void testStart() {
+        assertSame(operation, params.start());
+
+        assertThatIllegalArgumentException().isThrownBy(() -> params.toBuilder().context(null).build().start());
+    }
+
+    @Test
+    public void testGetRequestId() {
+        assertSame(REQ_ID, params.getRequestId());
+
+        // try with null context
+        assertNull(params.toBuilder().context(null).build().getRequestId());
+
+        // try with null event
+        when(context.getEvent()).thenReturn(null);
+        assertNull(params.getRequestId());
+    }
+
+    @Test
+    public void testMakeOutcome() {
+        assertEquals(ACTOR, outcome.getActor());
+        assertEquals(OPERATION, outcome.getOperation());
+        checkRemainingFields("with actor");
+    }
+
+    protected void checkRemainingFields(String testName) {
+        assertEquals(testName, TARGET_ENTITY, outcome.getTarget());
+        assertNull(testName, outcome.getStart());
+        assertNull(testName, outcome.getEnd());
+        assertNull(testName, outcome.getSubRequestId());
+        assertNotNull(testName, outcome.getResult());
+        assertNull(testName, outcome.getMessage());
+    }
+
+    @Test
+    public void testCallbackStarted() {
+        params.callbackStarted(outcome);
+        verify(starter).accept(outcome);
+
+        // modify starter to throw an exception
+        AtomicInteger count = new AtomicInteger();
+        doAnswer(args -> {
+            count.incrementAndGet();
+            throw new IllegalStateException(EXPECTED_EXCEPTION);
+        }).when(starter).accept(outcome);
+
+        params.callbackStarted(outcome);
+        verify(starter, times(2)).accept(outcome);
+        assertEquals(1, count.get());
+
+        // repeat with no start-callback - no additional calls expected
+        params.toBuilder().startCallback(null).build().callbackStarted(outcome);
+        verify(starter, times(2)).accept(outcome);
+        assertEquals(1, count.get());
+
+        // should not call complete-callback
+        verify(completer, never()).accept(any());
+    }
+
+    @Test
+    public void testCallbackCompleted() {
+        params.callbackCompleted(outcome);
+        verify(completer).accept(outcome);
+
+        // modify completer to throw an exception
+        AtomicInteger count = new AtomicInteger();
+        doAnswer(args -> {
+            count.incrementAndGet();
+            throw new IllegalStateException(EXPECTED_EXCEPTION);
+        }).when(completer).accept(outcome);
+
+        params.callbackCompleted(outcome);
+        verify(completer, times(2)).accept(outcome);
+        assertEquals(1, count.get());
+
+        // repeat with no complete-callback - no additional calls expected
+        params.toBuilder().completeCallback(null).build().callbackCompleted(outcome);
+        verify(completer, times(2)).accept(outcome);
+        assertEquals(1, count.get());
+
+        // should not call start-callback
+        verify(starter, never()).accept(any());
+    }
+
+    @Test
+    public void testValidateFields() {
+        testValidate("actor", "null", bldr -> bldr.actor(null));
+        testValidate("actorService", "null", bldr -> bldr.actorService(null));
+        testValidate("context", "null", bldr -> bldr.context(null));
+        testValidate("executor", "null", bldr -> bldr.executor(null));
+        testValidate("operation", "null", bldr -> bldr.operation(null));
+        testValidate("target", "null", bldr -> bldr.targetEntity(null));
+
+        // check edge cases
+        assertTrue(params.toBuilder().build().validate().isValid());
+
+        // these can be null
+        assertTrue(params.toBuilder().payload(null).retry(null).target(null).timeoutSec(null).startCallback(null)
+                        .completeCallback(null).build().validate().isValid());
+
+        // test with minimal fields
+        assertTrue(ControlLoopOperationParams.builder().actorService(actorService).context(context).actor(ACTOR)
+                        .operation(OPERATION).targetEntity(TARGET_ENTITY).build().validate().isValid());
+    }
+
+    private void testValidate(String fieldName, String expected,
+                    Function<ControlLoopOperationParamsBuilder, ControlLoopOperationParamsBuilder> makeInvalid) {
+
+        // original params should be valid
+        BeanValidationResult result = params.validate();
+        assertTrue(fieldName, result.isValid());
+
+        // make invalid params
+        result = makeInvalid.apply(params.toBuilder()).build().validate();
+        assertFalse(fieldName, result.isValid());
+        assertThat(result.getResult()).contains(fieldName).contains(expected);
+    }
+
+    @Test
+    public void testBuilder_testToBuilder() {
+        assertEquals(params, params.toBuilder().build());
+    }
+
+    @Test
+    public void testGetActor() {
+        assertSame(ACTOR, params.getActor());
+    }
+
+    @Test
+    public void testGetActorService() {
+        assertSame(actorService, params.getActorService());
+    }
+
+    @Test
+    public void testGetContext() {
+        assertSame(context, params.getContext());
+    }
+
+    @Test
+    public void testGetExecutor() {
+        assertSame(executor, params.getExecutor());
+
+        // should use default when unspecified
+        assertSame(ForkJoinPool.commonPool(), ControlLoopOperationParams.builder().build().getExecutor());
+    }
+
+    @Test
+    public void testGetOperation() {
+        assertSame(OPERATION, params.getOperation());
+    }
+
+    @Test
+    public void testGetPayload() {
+        assertSame(payload, params.getPayload());
+
+        // should be null when unspecified
+        assertNull(ControlLoopOperationParams.builder().build().getPayload());
+    }
+
+    @Test
+    public void testGetRetry() {
+        assertSame(RETRY, params.getRetry());
+
+        // should be null when unspecified
+        assertNull(ControlLoopOperationParams.builder().build().getRetry());
+    }
+
+    @Test
+    public void testTarget() {
+        assertSame(TARGET, params.getTarget());
+
+        // should be null when unspecified
+        assertNull(ControlLoopOperationParams.builder().build().getTarget());
+    }
+
+    @Test
+    public void testGetTimeoutSec() {
+        assertSame(TIMEOUT, params.getTimeoutSec());
+
+        // should be 300 when unspecified
+        assertEquals(Integer.valueOf(300), ControlLoopOperationParams.builder().build().getTimeoutSec());
+
+        // null should be ok too
+        assertNull(ControlLoopOperationParams.builder().timeoutSec(null).build().getTimeoutSec());
+    }
+
+    @Test
+    public void testGetStartCallback() {
+        assertSame(starter, params.getStartCallback());
+    }
+
+    @Test
+    public void testGetCompleteCallback() {
+        assertSame(completer, params.getCompleteCallback());
+    }
+
+    @Test
+    public void testGetTargetEntity() {
+        assertEquals(TARGET_ENTITY, params.getTargetEntity());
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParamsTest.java
new file mode 100644 (file)
index 0000000..6c1f538
--- /dev/null
@@ -0,0 +1,132 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.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 java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.parameters.ValidationResult;
+
+public class HttpActorParamsTest {
+
+    private static final String CONTAINER = "my-container";
+    private static final String CLIENT = "my-client";
+    private static final long TIMEOUT = 10;
+
+    private static final String PATH1 = "path #1";
+    private static final String PATH2 = "path #2";
+    private static final String URI1 = "uri #1";
+    private static final String URI2 = "uri #2";
+
+    private Map<String, String> paths;
+    private HttpActorParams params;
+
+    /**
+     * Initializes {@link #paths} with two items and {@link params} with a fully populated
+     * object.
+     */
+    @Before
+    public void setUp() {
+        paths = new TreeMap<>();
+        paths.put(PATH1, URI1);
+        paths.put(PATH2, URI2);
+
+        params = makeHttpActorParams();
+    }
+
+    @Test
+    public void testMakeOperationParameters() {
+        Function<String, Map<String, Object>> maker = params.makeOperationParameters(CONTAINER);
+        assertNull(maker.apply("unknown-operation"));
+
+        Map<String, Object> subparam = maker.apply(PATH1);
+        assertNotNull(subparam);
+        assertEquals("{clientName=my-client, path=uri #1, timeoutSec=10}", new TreeMap<>(subparam).toString());
+
+        subparam = maker.apply(PATH2);
+        assertNotNull(subparam);
+        assertEquals("{clientName=my-client, path=uri #2, timeoutSec=10}", new TreeMap<>(subparam).toString());
+    }
+
+    @Test
+    public void testDoValidation() {
+        assertThatCode(() -> params.doValidation(CONTAINER)).doesNotThrowAnyException();
+
+        // invalid param
+        params.setClientName(null);
+        assertThatThrownBy(() -> params.doValidation(CONTAINER))
+                        .isInstanceOf(ParameterValidationRuntimeException.class);
+    }
+
+    @Test
+    public void testValidate() {
+        assertTrue(params.validate(CONTAINER).isValid());
+
+        testValidateField("clientName", "null", params2 -> params2.setClientName(null));
+        testValidateField("path", "null", params2 -> params2.setPath(null));
+        testValidateField("timeoutSec", "minimum", params2 -> params2.setTimeoutSec(-1));
+
+        // check edge cases
+        params.setTimeoutSec(0);
+        assertTrue(params.validate(CONTAINER).isValid());
+
+        params.setTimeoutSec(1);
+        assertTrue(params.validate(CONTAINER).isValid());
+
+        // one path value is null
+        testValidateField(PATH2, "null", params2 -> paths.put(PATH2, null));
+    }
+
+    private void testValidateField(String fieldName, String expected, Consumer<HttpActorParams> makeInvalid) {
+
+        // original params should be valid
+        ValidationResult result = params.validate(CONTAINER);
+        assertTrue(fieldName, result.isValid());
+
+        // make invalid params
+        HttpActorParams params2 = makeHttpActorParams();
+        makeInvalid.accept(params2);
+        result = params2.validate(CONTAINER);
+        assertFalse(fieldName, result.isValid());
+        assertThat(result.getResult()).contains(CONTAINER).contains(fieldName).contains(expected);
+    }
+
+    private HttpActorParams makeHttpActorParams() {
+        HttpActorParams params2 = new HttpActorParams();
+        params2.setClientName(CLIENT);
+        params2.setTimeoutSec(TIMEOUT);
+        params2.setPath(paths);
+
+        return params2;
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParamsTest.java
new file mode 100644 (file)
index 0000000..6cf7328
--- /dev/null
@@ -0,0 +1,82 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.function.Function;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams.HttpParamsBuilder;
+
+public class HttpParamsTest {
+
+    private static final String CONTAINER = "my-container";
+    private static final String CLIENT = "my-client";
+    private static final String PATH = "my-path";
+    private static final long TIMEOUT = 10;
+
+    private HttpParams params;
+
+    @Before
+    public void setUp() {
+        params = HttpParams.builder().clientName(CLIENT).path(PATH).timeoutSec(TIMEOUT).build();
+    }
+
+    @Test
+    public void testValidate() {
+        assertTrue(params.validate(CONTAINER).isValid());
+
+        testValidateField("clientName", "null", bldr -> bldr.clientName(null));
+        testValidateField("path", "null", bldr -> bldr.path(null));
+        testValidateField("timeoutSec", "minimum", bldr -> bldr.timeoutSec(-1));
+
+        // check edge cases
+        assertTrue(params.toBuilder().timeoutSec(0).build().validate(CONTAINER).isValid());
+        assertTrue(params.toBuilder().timeoutSec(1).build().validate(CONTAINER).isValid());
+    }
+
+    @Test
+    public void testBuilder_testToBuilder() {
+        assertEquals(CLIENT, params.getClientName());
+        assertEquals(PATH, params.getPath());
+        assertEquals(TIMEOUT, params.getTimeoutSec());
+
+        assertEquals(params, params.toBuilder().build());
+    }
+
+    private void testValidateField(String fieldName, String expected,
+                    Function<HttpParamsBuilder, HttpParamsBuilder> makeInvalid) {
+
+        // original params should be valid
+        ValidationResult result = params.validate(CONTAINER);
+        assertTrue(fieldName, result.isValid());
+
+        // make invalid params
+        result = makeInvalid.apply(params.toBuilder()).build().validate(CONTAINER);
+        assertFalse(fieldName, result.isValid());
+        assertThat(result.getResult()).contains(fieldName).contains(expected);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ParameterValidationRuntimeExceptionTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ParameterValidationRuntimeExceptionTest.java
new file mode 100644 (file)
index 0000000..9879f60
--- /dev/null
@@ -0,0 +1,82 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.parameters.ObjectValidationResult;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.common.parameters.ValidationStatus;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException;
+
+public class ParameterValidationRuntimeExceptionTest {
+
+    private static final String THE_MESSAGE = "the message";
+    private static final IllegalStateException EXPECTED_EXCEPTION = new IllegalStateException("expected exception");
+
+    private ValidationResult result;
+
+    @Before
+    public void setUp() {
+        result = new ObjectValidationResult("param", null, ValidationStatus.INVALID, "null");
+    }
+
+    @Test
+    public void testParameterValidationExceptionValidationResult() {
+        ParameterValidationRuntimeException ex = new ParameterValidationRuntimeException(result);
+        assertSame(result, ex.getResult());
+        assertNull(ex.getMessage());
+    }
+
+    @Test
+    public void testParameterValidationExceptionValidationResultString() {
+        ParameterValidationRuntimeException ex = new ParameterValidationRuntimeException(THE_MESSAGE, result);
+        assertSame(result, ex.getResult());
+        assertEquals(THE_MESSAGE, ex.getMessage());
+    }
+
+    @Test
+    public void testParameterValidationExceptionValidationResultThrowable() {
+        ParameterValidationRuntimeException ex = new ParameterValidationRuntimeException(EXPECTED_EXCEPTION, result);
+        assertSame(result, ex.getResult());
+        assertEquals(EXPECTED_EXCEPTION.toString(), ex.getMessage());
+        assertEquals(EXPECTED_EXCEPTION, ex.getCause());
+    }
+
+    @Test
+    public void testParameterValidationExceptionValidationResultStringThrowable() {
+        ParameterValidationRuntimeException ex =
+                        new ParameterValidationRuntimeException(THE_MESSAGE, EXPECTED_EXCEPTION,  result);
+        assertSame(result, ex.getResult());
+        assertEquals(THE_MESSAGE, ex.getMessage());
+        assertEquals(EXPECTED_EXCEPTION, ex.getCause());
+    }
+
+    @Test
+    public void testGetResult() {
+        ParameterValidationRuntimeException ex = new ParameterValidationRuntimeException(result);
+        assertSame(result, ex.getResult());
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParamsTest.java
new file mode 100644 (file)
index 0000000..4834c98
--- /dev/null
@@ -0,0 +1,80 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.parameters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.function.Function;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.TopicParams.TopicParamsBuilder;
+
+public class TopicParamsTest {
+
+    private static final String CONTAINER = "my-container";
+    private static final String TARGET = "my-target";
+    private static final String SOURCE = "my-source";
+    private static final long TIMEOUT = 10;
+
+    private TopicParams params;
+
+    @Before
+    public void setUp() {
+        params = TopicParams.builder().target(TARGET).source(SOURCE).timeoutSec(TIMEOUT).build();
+    }
+
+    @Test
+    public void testValidate() {
+        testValidateField("target", "null", bldr -> bldr.target(null));
+        testValidateField("source", "null", bldr -> bldr.source(null));
+        testValidateField("timeoutSec", "minimum", bldr -> bldr.timeoutSec(-1));
+
+        // check edge cases
+        assertTrue(params.toBuilder().timeoutSec(0).build().validate(CONTAINER).isValid());
+        assertTrue(params.toBuilder().timeoutSec(1).build().validate(CONTAINER).isValid());
+    }
+
+    @Test
+    public void testBuilder_testToBuilder() {
+        assertEquals(TARGET, params.getTarget());
+        assertEquals(SOURCE, params.getSource());
+        assertEquals(TIMEOUT, params.getTimeoutSec());
+
+        assertEquals(params, params.toBuilder().build());
+    }
+
+    private void testValidateField(String fieldName, String expected,
+                    Function<TopicParamsBuilder, TopicParamsBuilder> makeInvalid) {
+
+        // original params should be valid
+        ValidationResult result = params.validate(CONTAINER);
+        assertTrue(fieldName, result.isValid());
+
+        // make invalid params
+        result = makeInvalid.apply(params.toBuilder()).build().validate(CONTAINER);
+        assertFalse(fieldName, result.isValid());
+        assertThat(result.getResult()).contains(fieldName).contains(expected);
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/FutureManagerTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/FutureManagerTest.java
new file mode 100644 (file)
index 0000000..de1cf0f
--- /dev/null
@@ -0,0 +1,142 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.pipeline;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.Future;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class FutureManagerTest {
+
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+
+    @Mock
+    private Future<String> future1;
+
+    @Mock
+    private Future<String> future2;
+
+    @Mock
+    private Future<String> future3;
+
+    private FutureManager mgr;
+
+    /**
+     * Initializes fields, including {@link #mgr}.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mgr = new FutureManager();
+    }
+
+    @Test
+    public void testStop() {
+        mgr.add(future1);
+        mgr.add(future2);
+        mgr.add(future3);
+
+        // arrange for one to throw an exception
+        when(future2.cancel(anyBoolean())).thenThrow(new IllegalStateException(EXPECTED_EXCEPTION));
+
+        // nothing should have been canceled yet
+        verify(future1, never()).cancel(anyBoolean());
+        verify(future2, never()).cancel(anyBoolean());
+        verify(future3, never()).cancel(anyBoolean());
+
+        assertTrue(mgr.isRunning());
+
+        // stop the controller
+
+        // stop the controller
+        mgr.stop();
+
+        // all controllers should now be stopped
+        assertFalse(mgr.isRunning());
+
+        // everything should have been invoked
+        verify(future1).cancel(anyBoolean());
+        verify(future2).cancel(anyBoolean());
+        verify(future3).cancel(anyBoolean());
+
+        // re-invoking stop should have no effect on the listeners
+        mgr.stop();
+
+        verify(future1).cancel(anyBoolean());
+        verify(future2).cancel(anyBoolean());
+        verify(future3).cancel(anyBoolean());
+    }
+
+    @Test
+    public void testAdd() {
+        // still running - this should not be invoked
+        mgr.add(future1);
+        verify(future1, never()).cancel(anyBoolean());
+
+        // re-add should have no impact
+        mgr.add(future1);
+        verify(future1, never()).cancel(anyBoolean());
+
+        mgr.stop();
+
+        verify(future1).cancel(anyBoolean());
+
+        // new additions should be invoked immediately
+        mgr.add(future2);
+        verify(future2).cancel(anyBoolean());
+
+        // should work with exceptions, too
+        when(future3.cancel(anyBoolean())).thenThrow(new IllegalStateException(EXPECTED_EXCEPTION));
+        mgr.add(future3);
+    }
+
+    @Test
+    public void testRemove() {
+        mgr.add(future1);
+        mgr.add(future2);
+
+        verify(future1, never()).cancel(anyBoolean());
+        verify(future2, never()).cancel(anyBoolean());
+
+        // remove the second
+        mgr.remove(future2);
+
+        // should be able to remove it again
+        mgr.remove(future2);
+
+        mgr.stop();
+
+        // first should have run, but not the second
+        verify(future1).cancel(anyBoolean());
+
+        verify(future2, never()).cancel(anyBoolean());
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/ListenerManagerTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/ListenerManagerTest.java
new file mode 100644 (file)
index 0000000..4a882d4
--- /dev/null
@@ -0,0 +1,134 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.pipeline;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ListenerManagerTest {
+
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+
+    @Mock
+    private Runnable runnable1;
+
+    @Mock
+    private Runnable runnable2;
+
+    @Mock
+    private Runnable runnable3;
+
+    private ListenerManager mgr;
+
+    /**
+     * Initializes fields, including {@link #mgr}.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mgr = new ListenerManager();
+    }
+
+    @Test
+    public void testStop_testIsRunning() {
+        mgr.add(runnable1);
+        mgr.add(runnable2);
+        mgr.add(runnable3);
+
+        // arrange for one to throw an exception
+        doThrow(new IllegalStateException(EXPECTED_EXCEPTION)).when(runnable2).run();
+
+        // nothing should have been canceled yet
+        verify(runnable1, never()).run();
+        verify(runnable2, never()).run();
+        verify(runnable3, never()).run();
+
+        assertTrue(mgr.isRunning());
+
+        // stop the controller
+        mgr.stop();
+
+        // all controllers should now be stopped
+        assertFalse(mgr.isRunning());
+
+        // everything should have been invoked
+        verify(runnable1).run();
+        verify(runnable2).run();
+        verify(runnable3).run();
+
+        // re-invoking stop should have no effect on the listeners
+        mgr.stop();
+
+        verify(runnable1).run();
+        verify(runnable2).run();
+        verify(runnable3).run();
+    }
+
+    @Test
+    public void testAdd() {
+        // still running - this should not be invoked
+        mgr.add(runnable1);
+        verify(runnable1, never()).run();
+
+        mgr.stop();
+
+        verify(runnable1).run();
+
+        // new additions should be invoked immediately
+        mgr.add(runnable2);
+        verify(runnable2).run();
+
+        // should work with exceptions, too
+        doThrow(new IllegalStateException(EXPECTED_EXCEPTION)).when(runnable3).run();
+        mgr.add(runnable3);
+    }
+
+    @Test
+    public void testRemove() {
+        mgr.add(runnable1);
+        mgr.add(runnable2);
+
+        verify(runnable1, never()).run();
+        verify(runnable2, never()).run();
+
+        // remove the second
+        mgr.remove(runnable2);
+
+        // should be able to remove it again
+        mgr.remove(runnable2);
+
+        mgr.stop();
+
+        // first should have run, but not the second
+        verify(runnable1).run();
+
+        verify(runnable2, never()).run();
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFutureTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFutureTest.java
new file mode 100644 (file)
index 0000000..a6b11ef
--- /dev/null
@@ -0,0 +1,471 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actorserviceprovider.pipeline;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class PipelineControllerFutureTest {
+    private static final IllegalStateException EXPECTED_EXCEPTION = new IllegalStateException("expected exception");
+    private static final String TEXT = "some text";
+
+    @Mock
+    private Runnable runnable1;
+
+    @Mock
+    private Runnable runnable2;
+
+    @Mock
+    private Future<String> future1;
+
+    @Mock
+    private Future<String> future2;
+
+    @Mock
+    private Executor executor;
+
+
+    private CompletableFuture<String> compFuture;
+    private PipelineControllerFuture<String> controller;
+
+
+    /**
+     * Initializes fields, including {@link #controller}. Adds all runners and futures to
+     * the controller.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        compFuture = spy(new CompletableFuture<>());
+
+        controller = new PipelineControllerFuture<>();
+
+        controller.add(runnable1);
+        controller.add(future1);
+        controller.add(runnable2);
+        controller.add(future2);
+    }
+
+    @Test
+    public void testCancel_testAddFutureOfFBoolean_testAddRunnable__testIsRunning() {
+        assertTrue(controller.isRunning());
+
+        assertTrue(controller.cancel(false));
+
+        assertTrue(controller.isCancelled());
+        assertFalse(controller.isRunning());
+
+        verifyStopped();
+
+        // re-invoke; nothing should change
+        assertTrue(controller.cancel(true));
+
+        assertTrue(controller.isCancelled());
+        assertFalse(controller.isRunning());
+
+        verifyStopped();
+    }
+
+    @Test
+    public void testCompleteT() throws Exception {
+        assertTrue(controller.complete(TEXT));
+        assertEquals(TEXT, controller.get());
+
+        verifyStopped();
+
+        // repeat - disallowed
+        assertFalse(controller.complete(TEXT));
+    }
+
+    @Test
+    public void testCompleteExceptionallyThrowable() {
+        assertTrue(controller.completeExceptionally(EXPECTED_EXCEPTION));
+        assertThatThrownBy(() -> controller.get()).hasCause(EXPECTED_EXCEPTION);
+
+        verifyStopped();
+
+        // repeat - disallowed
+        assertFalse(controller.completeExceptionally(EXPECTED_EXCEPTION));
+    }
+
+    @Test
+    public void testCompleteAsyncSupplierOfQextendsTExecutor() throws Exception {
+        CompletableFuture<String> future = controller.completeAsync(() -> TEXT, executor);
+
+        // haven't stopped anything yet
+        assertFalse(future.isDone());
+        verify(runnable1, never()).run();
+
+        // get the operation and run it
+        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
+        verify(executor).execute(captor.capture());
+        captor.getValue().run();
+
+        // should be done now
+        assertTrue(future.isDone());
+
+        assertEquals(TEXT, future.get());
+
+        verifyStopped();
+    }
+
+    /**
+     * Tests completeAsync(executor) when canceled before execution.
+     */
+    @Test
+    public void testCompleteAsyncSupplierOfQextendsTExecutorCanceled() throws Exception {
+        CompletableFuture<String> future = controller.completeAsync(() -> TEXT, executor);
+
+        assertTrue(future.cancel(false));
+
+        verifyStopped();
+
+        assertTrue(future.isDone());
+
+        assertThatThrownBy(() -> controller.get()).isInstanceOf(CancellationException.class);
+    }
+
+    @Test
+    public void testCompleteAsyncSupplierOfQextendsT() throws Exception {
+        CompletableFuture<String> future = controller.completeAsync(() -> TEXT);
+        assertEquals(TEXT, future.get());
+
+        verifyStopped();
+    }
+
+    /**
+     * Tests completeAsync() when canceled.
+     */
+    @Test
+    public void testCompleteAsyncSupplierOfQextendsTCanceled() throws Exception {
+        CountDownLatch canceled = new CountDownLatch(1);
+
+        // run async, but await until canceled
+        CompletableFuture<String> future = controller.completeAsync(() -> {
+            try {
+                canceled.await();
+            } catch (InterruptedException e) {
+                // do nothing
+            }
+
+            return TEXT;
+        });
+
+        assertTrue(future.cancel(false));
+
+        // let the future run now
+        canceled.countDown();
+
+        verifyStopped();
+
+        assertTrue(future.isDone());
+
+        assertThatThrownBy(() -> controller.get()).isInstanceOf(CancellationException.class);
+    }
+
+    @Test
+    public void testCompleteOnTimeoutTLongTimeUnit() throws Exception {
+        CountDownLatch stopped = new CountDownLatch(1);
+        controller.add(() -> stopped.countDown());
+
+        CompletableFuture<String> future = controller.completeOnTimeout(TEXT, 1, TimeUnit.MILLISECONDS);
+
+        assertEquals(TEXT, future.get());
+
+        /*
+         * Must use latch instead of verifyStopped(), because the runnables may be
+         * executed asynchronously.
+         */
+        assertTrue(stopped.await(5, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Tests completeOnTimeout() when completed before the timeout.
+     */
+    @Test
+    public void testCompleteOnTimeoutTLongTimeUnitNoTimeout() throws Exception {
+        CompletableFuture<String> future = controller.completeOnTimeout("timed out", 5, TimeUnit.SECONDS);
+        controller.complete(TEXT);
+
+        assertEquals(TEXT, future.get());
+
+        verifyStopped();
+    }
+
+    /**
+     * Tests completeOnTimeout() when canceled before the timeout.
+     */
+    @Test
+    public void testCompleteOnTimeoutTLongTimeUnitCanceled() {
+        CompletableFuture<String> future = controller.completeOnTimeout(TEXT, 5, TimeUnit.SECONDS);
+        assertTrue(future.cancel(true));
+
+        assertThatThrownBy(() -> controller.get()).isInstanceOf(CancellationException.class);
+
+        verifyStopped();
+    }
+
+    @Test
+    public void testNewIncompleteFuture() {
+        PipelineControllerFuture<String> future = controller.newIncompleteFuture();
+        assertNotNull(future);
+        assertTrue(future instanceof PipelineControllerFuture);
+        assertNotSame(controller, future);
+        assertFalse(future.isDone());
+    }
+
+    @Test
+    public void testDelayedComplete() throws Exception {
+        controller.add(runnable1);
+
+        BiConsumer<String, Throwable> stopper = controller.delayedComplete();
+
+        // shouldn't have run yet
+        assertTrue(controller.isRunning());
+        verify(runnable1, never()).run();
+
+        stopper.accept(TEXT, null);
+
+        assertTrue(controller.isDone());
+        assertEquals(TEXT, controller.get());
+
+        assertFalse(controller.isRunning());
+        verify(runnable1).run();
+
+        // re-invoke; nothing should change
+        stopper.accept(TEXT, EXPECTED_EXCEPTION);
+        assertFalse(controller.isCompletedExceptionally());
+
+        assertFalse(controller.isRunning());
+        verify(runnable1).run();
+    }
+
+    /**
+     * Tests delayedComplete() when an exception is generated.
+     */
+    @Test
+    public void testDelayedCompleteWithException() throws Exception {
+        controller.add(runnable1);
+
+        BiConsumer<String, Throwable> stopper = controller.delayedComplete();
+
+        // shouldn't have run yet
+        assertTrue(controller.isRunning());
+        verify(runnable1, never()).run();
+
+        stopper.accept(TEXT, EXPECTED_EXCEPTION);
+
+        assertTrue(controller.isDone());
+        assertThatThrownBy(() -> controller.get()).hasCause(EXPECTED_EXCEPTION);
+
+        assertFalse(controller.isRunning());
+        verify(runnable1).run();
+
+        // re-invoke; nothing should change
+        stopper.accept(TEXT, null);
+        assertTrue(controller.isCompletedExceptionally());
+
+        assertFalse(controller.isRunning());
+        verify(runnable1).run();
+    }
+
+    @Test
+    public void testDelayedRemoveFutureOfF() throws Exception {
+        BiConsumer<String, Throwable> remover = controller.delayedRemove(future1);
+
+        remover.accept(TEXT, EXPECTED_EXCEPTION);
+
+        // should not have completed the controller
+        assertFalse(controller.isDone());
+
+        verify(future1, never()).cancel(anyBoolean());
+
+        controller.delayedComplete().accept(TEXT, EXPECTED_EXCEPTION);
+
+        verify(future1, never()).cancel(anyBoolean());
+        verify(future2).cancel(anyBoolean());
+    }
+
+    @Test
+    public void testDelayedRemoveRunnable() throws Exception {
+        BiConsumer<String, Throwable> remover = controller.delayedRemove(runnable1);
+
+        remover.accept(TEXT, EXPECTED_EXCEPTION);
+
+        // should not have completed the controller
+        assertFalse(controller.isDone());
+
+        verify(runnable1, never()).run();
+
+        controller.delayedComplete().accept(TEXT, EXPECTED_EXCEPTION);
+
+        verify(runnable1, never()).run();
+        verify(runnable2).run();
+    }
+
+    @Test
+    public void testRemoveFutureOfF_testRemoveRunnable() {
+        controller.remove(runnable2);
+        controller.remove(future1);
+
+        controller.cancel(true);
+
+        verify(runnable1).run();
+        verify(runnable2, never()).run();
+        verify(future1, never()).cancel(anyBoolean());
+        verify(future2).cancel(anyBoolean());
+    }
+
+    /**
+     * Tests both wrap() methods.
+     */
+    @Test
+    public void testWrap() throws Exception {
+        controller = spy(controller);
+
+        CompletableFuture<String> future = controller.wrap(compFuture);
+        verify(controller, never()).remove(compFuture);
+
+        compFuture.complete(TEXT);
+        assertEquals(TEXT, future.get());
+
+        verify(controller).remove(compFuture);
+    }
+
+    /**
+     * Tests wrap(), when the controller is not running.
+     */
+    @Test
+    public void testWrapNotRunning() throws Exception {
+        controller.cancel(false);
+        controller = spy(controller);
+
+        assertFalse(controller.wrap(compFuture).isDone());
+        verify(controller, never()).add(compFuture);
+        verify(controller, never()).remove(compFuture);
+
+        verify(compFuture).cancel(anyBoolean());
+    }
+
+    /**
+     * Tests wrap(), when the future throws an exception.
+     */
+    @Test
+    public void testWrapException() throws Exception {
+        controller = spy(controller);
+
+        CompletableFuture<String> future = controller.wrap(compFuture);
+        verify(controller, never()).remove(compFuture);
+
+        compFuture.completeExceptionally(EXPECTED_EXCEPTION);
+        assertThatThrownBy(() -> future.get()).hasCause(EXPECTED_EXCEPTION);
+
+        verify(controller).remove(compFuture);
+    }
+
+    @Test
+    public void testWrapFunction() throws Exception {
+
+        Function<String, CompletableFuture<String>> func = controller.wrap(input -> {
+            compFuture.complete(input);
+            return compFuture;
+        });
+
+        CompletableFuture<String> future = func.apply(TEXT);
+        assertTrue(compFuture.isDone());
+
+        assertEquals(TEXT, future.get());
+
+        // should not have completed the controller
+        assertFalse(controller.isDone());
+    }
+
+    /**
+     * Tests add(Function) when the controller is canceled after the future is added.
+     */
+    @Test
+    public void testWrapFunctionCancel() throws Exception {
+        Function<String, CompletableFuture<String>> func = controller.wrap(input -> compFuture);
+
+        CompletableFuture<String> future = func.apply(TEXT);
+        assertFalse(future.isDone());
+
+        assertFalse(compFuture.isDone());
+
+        // cancel - should propagate
+        controller.cancel(false);
+
+        verify(compFuture).cancel(anyBoolean());
+    }
+
+    /**
+     * Tests add(Function) when the controller is not running.
+     */
+    @Test
+    public void testWrapFunctionNotRunning() {
+        AtomicReference<String> value = new AtomicReference<>();
+
+        Function<String, CompletableFuture<String>> func = controller.wrap(input -> {
+            value.set(input);
+            return compFuture;
+        });
+
+        controller.cancel(false);
+
+        CompletableFuture<String> fut = func.apply(TEXT);
+        assertNotSame(compFuture, fut);
+        assertFalse(fut.isDone());
+
+        assertNull(value.get());
+    }
+
+    private void verifyStopped() {
+        verify(runnable1).run();
+        verify(runnable2).run();
+        verify(future1).cancel(anyBoolean());
+        verify(future2).cancel(anyBoolean());
+    }
+}
diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/resources/logback-test.xml b/models-interactions/model-actors/actorServiceProvider/src/test/resources/logback-test.xml
new file mode 100644 (file)
index 0000000..c7fe46e
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ============LICENSE_START=======================================================
+  ONAP
+  ================================================================================
+  Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+  ================================================================================
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  ============LICENSE_END=========================================================
+-->
+
+<configuration>
+
+    <contextName>Actors</contextName>
+    <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
+
+    <!-- USE FOR STD OUT ONLY -->
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <Pattern>%d %level  %msg%n</Pattern>
+        </encoder>
+    </appender>
+
+    <root level="warn">
+        <appender-ref ref="STDOUT" />
+    </root>
+
+    <!-- this is required for UtilTest -->
+    <logger name="org.onap.policy.controlloop.actorserviceprovider.Util" level="info" additivity="false">
+        <appender-ref ref="STDOUT" />
+    </logger>
+</configuration>
index 10457de..11c0132 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  * controlloop
  * ================================================================================
- * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved.
+ * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved.
  * Modifications Copyright (C) 2019 Nordix Foundation.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,6 +32,9 @@ public class ControlLoopOperation implements Serializable {
 
     private static final long serialVersionUID = 8662706581293017099L;
 
+    public static final String SUCCESS_MSG = "Success";
+    public static final String FAILED_MSG = "Failed";
+
     private String actor;
     private String operation;
     private String target;
index 7612eeb..fa18cb2 100644 (file)
@@ -2,7 +2,7 @@
  * ============LICENSE_START=======================================================
  * Copyright (C) 2018 Huawei. All rights reserved.
  * ================================================================================
- * Modifications Copyright (C) 2019 AT&T Intellectual Property. All rights reserved
+ * Modifications Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved
  * Modifications Copyright (C) 2019 Nordix Foundation.
  * Modifications Copyright (C) 2019 Samsung Electronics Co., Ltd.
  * ================================================================================
@@ -37,6 +37,8 @@ import org.onap.policy.sdnc.util.Serialization;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+// TODO this class will be deleted
+
 public final class SdncManager implements Runnable {
 
     private String sdncUrlBase;