From 184e1ed3e83effc39b6f7f92c9f4b9d874c4cfe7 Mon Sep 17 00:00:00 2001
From: "Benjamin, Max (mb388a)" <mb388a@us.att.com>
Date: Mon, 17 Sep 2018 10:52:09 -0400
Subject: add single transaction api to aaiclient

fixed marshalling when issuing a patch request
added support for single transaction api in A&AI

Change-Id: Icf755f547523cc7dbf931e198177847a5a1c6ea1
Issue-ID: SO-1060
Signed-off-by: Benjamin, Max (mb388a) <mb388a@us.att.com>
---
 .../main/java/org/onap/so/client/RestClient.java   |   2 +-
 .../java/org/onap/so/client/aai/AAIObjectType.java |   3 +
 .../org/onap/so/client/aai/AAIPatchConverter.java  |  81 +++++++
 .../org/onap/so/client/aai/AAIResourcesClient.java |   9 +
 .../java/org/onap/so/client/aai/AAIRestClient.java |  53 +---
 .../so/client/aai/AAISingleTransactionClient.java  | 267 +++++++++++++++++++++
 .../onap/so/client/aai/AAITransactionalClient.java |  39 ++-
 .../aai/entities/bulkprocess/OperationBody.java    |   3 +
 .../bulkprocess/OperationBodySerializer.java       |  54 +++++
 .../singletransaction/OperationBodyRequest.java    |  88 +++++++
 .../OperationBodyRequestSerializer.java            |  54 +++++
 .../singletransaction/OperationBodyResponse.java   |  71 ++++++
 .../SingleTransactionRequest.java                  |  45 ++++
 .../SingleTransactionResponse.java                 |  47 ++++
 .../onap/so/client/aai/AAIPatchConverterTest.java  | 102 ++++++++
 .../org/onap/so/client/aai/AAIRestClientTest.java  |  72 +-----
 .../client/aai/AAISingleTransactionClientTest.java | 133 ++++++++++
 .../so/client/aai/AAITransactionalClientTest.java  |  13 +
 .../aai/singletransaction/sample-request.json      |  26 ++
 .../singletransaction/sample-response-failure.json |  30 +++
 .../aai/singletransaction/sample-response.json     |  22 ++
 21 files changed, 1110 insertions(+), 104 deletions(-)
 create mode 100644 common/src/main/java/org/onap/so/client/aai/AAIPatchConverter.java
 create mode 100644 common/src/main/java/org/onap/so/client/aai/AAISingleTransactionClient.java
 create mode 100644 common/src/main/java/org/onap/so/client/aai/entities/bulkprocess/OperationBodySerializer.java
 create mode 100644 common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyRequest.java
 create mode 100644 common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyRequestSerializer.java
 create mode 100644 common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyResponse.java
 create mode 100644 common/src/main/java/org/onap/so/client/aai/entities/singletransaction/SingleTransactionRequest.java
 create mode 100644 common/src/main/java/org/onap/so/client/aai/entities/singletransaction/SingleTransactionResponse.java
 create mode 100644 common/src/test/java/org/onap/so/client/aai/AAIPatchConverterTest.java
 create mode 100644 common/src/test/java/org/onap/so/client/aai/AAISingleTransactionClientTest.java
 create mode 100644 common/src/test/resources/__files/aai/singletransaction/sample-request.json
 create mode 100644 common/src/test/resources/__files/aai/singletransaction/sample-response-failure.json
 create mode 100644 common/src/test/resources/__files/aai/singletransaction/sample-response.json

(limited to 'common/src')

diff --git a/common/src/main/java/org/onap/so/client/RestClient.java b/common/src/main/java/org/onap/so/client/RestClient.java
index 631850a01f..1a453c6b2f 100644
--- a/common/src/main/java/org/onap/so/client/RestClient.java
+++ b/common/src/main/java/org/onap/so/client/RestClient.java
@@ -271,7 +271,7 @@ public abstract class RestClient {
 		return format(method("DELETE", obj), resultClass).orElse(null);
 	}
 	
-	private Response method(String method, Object entity) {
+	public Response method(String method, Object entity) {
 		RetryPolicy policy = new RetryPolicy();
 		
 		List<Predicate<Throwable>> items = retryOn();
diff --git a/common/src/main/java/org/onap/so/client/aai/AAIObjectType.java b/common/src/main/java/org/onap/so/client/aai/AAIObjectType.java
index a5d8f12e83..4b646f9ed7 100644
--- a/common/src/main/java/org/onap/so/client/aai/AAIObjectType.java
+++ b/common/src/main/java/org/onap/so/client/aai/AAIObjectType.java
@@ -27,6 +27,7 @@ import org.onap.aai.annotations.Metadata;
 import org.onap.aai.domain.yang.AllottedResource;
 import org.onap.aai.domain.yang.CloudRegion;
 import org.onap.aai.domain.yang.Collection;
+import org.onap.aai.domain.yang.Complex;
 import org.onap.aai.domain.yang.Configuration;
 import org.onap.aai.domain.yang.Customer;
 import org.onap.aai.domain.yang.GenericVnf;
@@ -64,6 +65,7 @@ public enum AAIObjectType implements GraphInventoryObjectType {
 	CUSTOMER(AAINamespaceConstants.BUSINESS, Customer.class),
 	GENERIC_QUERY("/search", "/generic-query"),
 	BULK_PROCESS("/bulkprocess", ""),
+	SINGLE_TRANSACTION("/bulk/single-transaction", ""),
 	GENERIC_VNF(AAINamespaceConstants.NETWORK, GenericVnf.class),
 	VF_MODULE(AAIObjectType.GENERIC_VNF.uriTemplate(), VfModule.class),
 	L3_NETWORK(AAINamespaceConstants.NETWORK, L3Network.class),
@@ -98,6 +100,7 @@ public enum AAIObjectType implements GraphInventoryObjectType {
 	COLLECTION(AAINamespaceConstants.NETWORK, Collection.class),
 	VNFC(AAINamespaceConstants.NETWORK, Vnfc.class),
 	VLAN_TAG(AAINamespaceConstants.NETWORK, VlanTag.class),
+	COMPLEX(AAINamespaceConstants.CLOUD_INFRASTRUCTURE, Complex.class),
 	UNKNOWN("", "");
 
 	private final String uriTemplate;
diff --git a/common/src/main/java/org/onap/so/client/aai/AAIPatchConverter.java b/common/src/main/java/org/onap/so/client/aai/AAIPatchConverter.java
new file mode 100644
index 0000000000..6ccb592409
--- /dev/null
+++ b/common/src/main/java/org/onap/so/client/aai/AAIPatchConverter.java
@@ -0,0 +1,81 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 - 2018 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai;
+
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.onap.so.client.graphinventory.exceptions.GraphInventoryPatchDepthExceededException;
+import org.onap.so.jsonpath.JsonPathUtil;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+public class AAIPatchConverter {
+
+	private static final AAICommonObjectMapperProvider standardProvider = new AAICommonObjectMapperProvider();
+	private static final AAICommonObjectMapperPatchProvider patchProvider = new AAICommonObjectMapperPatchProvider();
+	private static final Pattern LOCATE_COMPLEX_OBJECT = Pattern.compile("^((?!relationship-list).)+?\\['[^\\[\\]]+?'\\]$");
+
+	
+	protected String convertPatchFormat(Object obj) {
+		return validatePatchObject(marshallObjectToPatchFormat(obj));
+	}
+	
+	protected String validatePatchObject(String payload) {
+		if (hasComplexObject(payload)) {
+			throw new GraphInventoryPatchDepthExceededException(payload);
+		}
+		
+		return payload;
+	}
+	
+	/** validates client side that json does not include any complex objects
+	 * relationship-list is omitted from this validation
+	 */
+	protected boolean hasComplexObject(String json) {
+		if (json.isEmpty()) {
+			return false;
+		}
+		String complex = "$.*.*";
+		String array = "$.*.*.*";
+		List<String> result = JsonPathUtil.getInstance().getPathList(json, complex);
+		List<String> result2 = JsonPathUtil.getInstance().getPathList(json, array);
+		
+		result.addAll(result2);
+		return result.stream().anyMatch(item -> LOCATE_COMPLEX_OBJECT.matcher(item).find());
+	}
+	
+	protected String marshallObjectToPatchFormat(Object obj) {
+		Object value = obj;
+		try {
+			if (!(obj instanceof Map || obj instanceof String)) {
+				value = patchProvider.getMapper().writeValueAsString(obj);
+			} else if (obj instanceof Map) {
+				value = standardProvider.getMapper().writeValueAsString(obj);
+			}
+		} catch (JsonProcessingException e) {
+			value = "{}";
+		}
+		
+		return (String)value;
+	}
+}
diff --git a/common/src/main/java/org/onap/so/client/aai/AAIResourcesClient.java b/common/src/main/java/org/onap/so/client/aai/AAIResourcesClient.java
index 072534d6f6..7e4397ec29 100644
--- a/common/src/main/java/org/onap/so/client/aai/AAIResourcesClient.java
+++ b/common/src/main/java/org/onap/so/client/aai/AAIResourcesClient.java
@@ -309,6 +309,15 @@ public class AAIResourcesClient extends AAIClient {
 		return new AAITransactionalClient(this.getVersion());
 	}
 	
+	/**
+	 * Starts a transaction groups multiple A&AI mutations
+	 * 
+	 * @return
+	 */
+	public AAISingleTransactionClient beginSingleTransaction() {
+		return new AAISingleTransactionClient(this.getVersion());
+	}
+	
 	private AAIUri addParams(Optional<Depth> depth, boolean nodesOnly, AAIUri uri) {
 		AAIUri clone = uri.clone();
 		if (depth.isPresent()) {
diff --git a/common/src/main/java/org/onap/so/client/aai/AAIRestClient.java b/common/src/main/java/org/onap/so/client/aai/AAIRestClient.java
index 2bd5f118c0..ac6e939e9e 100644
--- a/common/src/main/java/org/onap/so/client/aai/AAIRestClient.java
+++ b/common/src/main/java/org/onap/so/client/aai/AAIRestClient.java
@@ -20,6 +20,8 @@
 
 package org.onap.so.client.aai;
 
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+
 import java.net.URI;
 import java.util.List;
 import java.util.Map;
@@ -41,9 +43,9 @@ public class AAIRestClient extends RestClientSSL {
 
 	private final AAIProperties aaiProperties;
 	private static final AAICommonObjectMapperProvider standardProvider = new AAICommonObjectMapperProvider();
-	private static final AAICommonObjectMapperPatchProvider patchProvider = new AAICommonObjectMapperPatchProvider();
-	private static final Pattern LOCATE_COMPLEX_OBJECT = Pattern.compile("^((?!relationship-list).)+?\\['[^\\[\\]]+?'\\]$");
 
+	private final AAIPatchConverter patchConverter = new AAIPatchConverter();
+	
 	protected AAIRestClient(AAIProperties props, URI uri) {
 		super(props, Optional.of(uri));
 		this.aaiProperties = props;
@@ -79,53 +81,20 @@ public class AAIRestClient extends RestClientSSL {
 
 	@Override
 	public Response patch(Object obj) {
-		String value = convertObjectToPatchFormat(obj);
-		validatePatchObject(value);
-		return super.patch(value);
+		return super.patch(convertToPatchFormat(obj));
 	}
 
 	@Override
 	public <T> T patch(Object obj, Class<T> resultClass) {
-		String value = convertObjectToPatchFormat(obj);
-		validatePatchObject(value);
-		return super.patch(value, resultClass);
+		return super.patch(convertToPatchFormat(obj), resultClass);
 	}
 	
-	protected String convertObjectToPatchFormat(Object obj) {
-		Object value = obj;
-		try {
-			if (!(obj instanceof Map || obj instanceof String)) {
-				value = patchProvider.getMapper().writeValueAsString(obj);
-			} else if (obj instanceof Map) {
-				value = standardProvider.getMapper().writeValueAsString(obj);
-			}
-		} catch (JsonProcessingException e) {
-			value = "{}";
-		}
-		
-		return (String)value;
+	protected AAIPatchConverter getPatchConverter() {
+		return this.patchConverter;
 	}
 	
-	
-	protected void validatePatchObject(String payload) {
-		if (hasComplexObject(payload)) {
-			throw new GraphInventoryPatchDepthExceededException(payload);
-		}
-	}
-	
-	/** validates client side that json does not include any complex objects
-	 * relationship-list is omitted from this validation
-	 */
-	protected boolean hasComplexObject(String json) {
-		if (json.isEmpty()) {
-			return false;
-		}
-		String complex = "$.*.*";
-		String array = "$.*.*.*";
-		List<String> result = JsonPathUtil.getInstance().getPathList(json, complex);
-		List<String> result2 = JsonPathUtil.getInstance().getPathList(json, array);
-		
-		result.addAll(result2);
-		return result.stream().anyMatch(item -> LOCATE_COMPLEX_OBJECT.matcher(item).find());
+	protected String convertToPatchFormat(Object obj) {
+		return getPatchConverter().convertPatchFormat(obj);
 	}
+
 }
diff --git a/common/src/main/java/org/onap/so/client/aai/AAISingleTransactionClient.java b/common/src/main/java/org/onap/so/client/aai/AAISingleTransactionClient.java
new file mode 100644
index 0000000000..2ecdb7c480
--- /dev/null
+++ b/common/src/main/java/org/onap/so/client/aai/AAISingleTransactionClient.java
@@ -0,0 +1,267 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 - 2018 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.GenericType;
+
+import org.onap.aai.domain.yang.Relationship;
+import org.onap.so.client.RestClient;
+import org.onap.so.client.aai.entities.AAIEdgeLabel;
+import org.onap.so.client.aai.entities.AAIError;
+import org.onap.so.client.aai.entities.bulkprocess.Transactions;
+import org.onap.so.client.aai.entities.singletransaction.OperationBodyRequest;
+import org.onap.so.client.aai.entities.singletransaction.OperationBodyResponse;
+import org.onap.so.client.aai.entities.singletransaction.SingleTransactionRequest;
+import org.onap.so.client.aai.entities.singletransaction.SingleTransactionResponse;
+import org.onap.so.client.aai.entities.uri.AAIResourceUri;
+import org.onap.so.client.aai.entities.uri.AAIUri;
+import org.onap.so.client.aai.entities.uri.AAIUriFactory;
+import org.onap.so.client.graphinventory.exceptions.BulkProcessFailed;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Joiner;
+
+public class AAISingleTransactionClient extends AAIClient {
+
+	private final SingleTransactionRequest request;
+	private final AAIVersion version;
+	private int actionCount = 0;
+	
+	private final AAIPatchConverter patchConverter = new AAIPatchConverter();
+	
+	protected AAISingleTransactionClient(AAIVersion version) {
+		super();
+		this.version = version;
+		this.request = new SingleTransactionRequest();
+	}
+
+	/**
+	 * creates a new object in A&AI
+	 * 
+	 * @param obj - can be any object which will marshal into a valid A&AI payload
+	 * @param uri
+	 * @return
+	 */
+	public AAISingleTransactionClient create(AAIResourceUri uri, Object obj) {
+		request.getOperations().add(new OperationBodyRequest().withAction("put").withUri(uri.build().toString()).withBody(obj));
+		incrementActionAmount();
+		return this;
+	}
+
+	/**
+	 * creates a new object in A&AI with no payload body
+	 * 
+	 * @param uri
+	 * @return
+	 */
+	public AAISingleTransactionClient createEmpty(AAIResourceUri uri) {
+		request.getOperations().add(new OperationBodyRequest().withAction("put").withUri(uri.build().toString()).withBody(new HashMap<String, String>()));
+		incrementActionAmount();
+		return this;
+	}
+
+	/**
+	 * Adds a relationship between two objects in A&AI 
+	 * @param uriA
+	 * @param uriB
+	 * @return
+	 */
+	public AAISingleTransactionClient connect(AAIResourceUri uriA, AAIResourceUri uriB) {
+		AAIResourceUri uriAClone = uriA.clone();
+		request.getOperations().add(new OperationBodyRequest().withAction("put").withUri(uriAClone.relationshipAPI().build().toString()).withBody(this.buildRelationship(uriB)));
+		incrementActionAmount();
+		return this;
+	}
+
+	/**
+	 * relationship between multiple objects in A&AI - connects A to all objects specified in list
+	 * 
+	 * @param uriA
+	 * @param uris
+	 * @return
+	 */
+	public AAISingleTransactionClient connect(AAIResourceUri uriA, List<AAIResourceUri> uris) {
+		for (AAIResourceUri uri : uris) {
+			this.connect(uriA, uri);
+		}
+		return this;
+	}
+	
+	public AAISingleTransactionClient connect(AAIResourceUri uriA, AAIResourceUri uriB, AAIEdgeLabel label) {
+		AAIResourceUri uriAClone = uriA.clone();
+		RestClient aaiRC = this.createClient(uriAClone.relationshipAPI());
+		aaiRC.put(this.buildRelationship(uriB, label));
+		return this;
+	}
+	
+	public AAISingleTransactionClient connect(AAIResourceUri uriA, List<AAIResourceUri> uris, AAIEdgeLabel label) {
+		for (AAIResourceUri uri : uris) {
+			this.connect(uriA, uri, label);
+		}
+		return this;
+	}
+
+	/**
+	 * Removes relationship from two objects in A&AI
+	 * 
+	 * @param uriA
+	 * @param uriB
+	 * @return
+	 */
+	public AAISingleTransactionClient disconnect(AAIResourceUri uriA, AAIResourceUri uriB) {
+		AAIResourceUri uriAClone = uriA.clone();
+		request.getOperations().add(new OperationBodyRequest().withAction("delete").withUri(uriAClone.relationshipAPI().build().toString()).withBody(this.buildRelationship(uriB)));
+		incrementActionAmount();
+		return this;
+	}
+
+	/**
+	 * Removes relationship from multiple objects - disconnects A from all objects specified in list
+	 * @param uriA
+	 * @param uris
+	 * @return
+	 */
+	public AAISingleTransactionClient disconnect(AAIResourceUri uriA, List<AAIResourceUri> uris) {
+		for (AAIResourceUri uri : uris) {
+			this.disconnect(uriA, uri);
+		}
+		return this;
+	}
+	/**
+	 * Deletes object from A&AI. Automatically handles resource-version.
+	 * 
+	 * @param uri
+	 * @return
+	 */
+	public AAISingleTransactionClient delete(AAIResourceUri uri) {
+		AAIResourcesClient client = new AAIResourcesClient();
+		AAIResourceUri clone = uri.clone();
+		Map<String, Object> result = client.get(new GenericType<Map<String, Object>>(){}, clone)
+				.orElseThrow(() -> new NotFoundException(clone.build() + " does not exist in A&AI"));
+		String resourceVersion = (String) result.get("resource-version");
+		request.getOperations().add(new OperationBodyRequest().withAction("delete").withUri(clone.resourceVersion(resourceVersion).build().toString()).withBody(""));
+		incrementActionAmount();
+		return this;
+	}
+
+	/**
+	 * @param obj - can be any object which will marshal into a valid A&AI payload
+	 * @param uri
+	 * @return
+	 */
+	public AAISingleTransactionClient update(AAIResourceUri uri, Object obj) {
+		
+		final String payload = getPatchConverter().convertPatchFormat(obj);
+		request.getOperations().add(new OperationBodyRequest().withAction("patch").withUri(uri.build().toString()).withBody(payload));
+		incrementActionAmount();
+		return this;
+	}
+
+	private void incrementActionAmount() {
+		actionCount++;
+	}
+	/**
+	 * Executes all created transactions in A&AI
+	 * @throws BulkProcessFailed 
+	 */
+	public void execute() throws BulkProcessFailed {
+		RestClient client = this.createClient(AAIUriFactory.createResourceUri(AAIObjectType.SINGLE_TRANSACTION));
+		try {
+			SingleTransactionResponse response = client.post(this.request, SingleTransactionResponse.class);
+			if (response != null) {
+				final Optional<String> errorMessage = this.locateErrorMessages(response);
+				if (errorMessage.isPresent()) {
+					throw new BulkProcessFailed("One or more transactions failed in A&AI. Check logs for payloads.\nMessages:\n" + errorMessage.get());
+				}
+			} else {
+				throw new BulkProcessFailed("Transactions acccepted by A&AI, but there was no response. Unsure of result.");
+			}
+		} finally {
+			this.request.getOperations().clear();
+			this.actionCount = 0;
+		}
+	}
+
+	protected Optional<String> locateErrorMessages(SingleTransactionResponse response) {
+		final List<String> errorMessages = new ArrayList<>();
+		final ObjectMapper mapper = new ObjectMapper();
+		
+		for (OperationBodyResponse body : response.getOperationResponses()) {
+			if (Optional.ofNullable(body.getResponseStatusCode()).orElse(400) > 300) {
+				AAIError error;
+				try {
+					error = mapper.readValue(mapper.writeValueAsString(body.getResponseBody()), AAIError.class);
+				} catch (IOException e) {
+					logger.error("could not parse error object from A&AI", e);
+					error = new AAIError();
+				}
+				AAIErrorFormatter formatter = new AAIErrorFormatter(error);
+				String outputMessage = formatter.getMessage();
+				errorMessages.add(outputMessage);
+			}
+		}
+		
+		if (!errorMessages.isEmpty()) {
+			return Optional.of(Joiner.on("\n").join(errorMessages));
+		} else {
+			return Optional.empty();
+		}
+	}
+	
+	private Relationship buildRelationship(AAIResourceUri uri) {
+		return buildRelationship(uri, Optional.empty());
+	}
+	
+	private Relationship buildRelationship(AAIResourceUri uri, AAIEdgeLabel label) {
+		return buildRelationship(uri, Optional.of(label));
+	}
+	private Relationship buildRelationship(AAIResourceUri uri, Optional<AAIEdgeLabel> label) {
+		final Relationship result = new Relationship();
+		result.setRelatedLink(uri.build().toString());
+		if (label.isPresent()) {
+			result.setRelationshipLabel(label.toString());
+		}
+		return result;
+	}
+
+	@Override
+	protected AAIVersion getVersion() {
+		return this.version;
+	}
+	
+	protected SingleTransactionRequest getRequest() {
+		return this.request;
+	}
+	
+	protected AAIPatchConverter getPatchConverter() {
+		return this.patchConverter;
+	}
+}
diff --git a/common/src/main/java/org/onap/so/client/aai/AAITransactionalClient.java b/common/src/main/java/org/onap/so/client/aai/AAITransactionalClient.java
index 884d2aaec6..118a3edf1c 100644
--- a/common/src/main/java/org/onap/so/client/aai/AAITransactionalClient.java
+++ b/common/src/main/java/org/onap/so/client/aai/AAITransactionalClient.java
@@ -20,6 +20,8 @@
 
 package org.onap.so.client.aai;
 
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -34,6 +36,7 @@ import javax.ws.rs.core.Response;
 
 import org.onap.aai.domain.yang.Relationship;
 import org.onap.so.client.RestClient;
+import org.onap.so.client.aai.entities.AAIEdgeLabel;
 import org.onap.so.client.aai.entities.AAIError;
 import org.onap.so.client.aai.entities.bulkprocess.OperationBody;
 import org.onap.so.client.aai.entities.bulkprocess.Transaction;
@@ -54,6 +57,9 @@ public class AAITransactionalClient extends AAIClient {
 	private Transaction currentTransaction;
 	private final AAIVersion version;
 	private int actionCount = 0;
+	
+	private final AAIPatchConverter patchConverter = new AAIPatchConverter();
+	
 	protected AAITransactionalClient(AAIVersion version) {
 		super();
 		this.version = version;
@@ -129,6 +135,20 @@ public class AAITransactionalClient extends AAIClient {
 		return this;
 	}
 	
+	public AAITransactionalClient connect(AAIResourceUri uriA, AAIResourceUri uriB, AAIEdgeLabel label) {
+		AAIResourceUri uriAClone = uriA.clone();
+		RestClient aaiRC = this.createClient(uriAClone.relationshipAPI());
+		aaiRC.put(this.buildRelationship(uriB, label));
+		return this;
+	}
+	
+	public AAITransactionalClient connect(AAIResourceUri uriA, List<AAIResourceUri> uris, AAIEdgeLabel label) {
+		for (AAIResourceUri uri : uris) {
+			this.connect(uriA, uri, label);
+		}
+		return this;
+	}
+	
 	/**
 	 * Removes relationship from two objects in A&AI
 	 * 
@@ -178,7 +198,8 @@ public class AAITransactionalClient extends AAIClient {
 	 * @return
 	 */
 	public AAITransactionalClient update(AAIResourceUri uri, Object obj) {
-		currentTransaction.getPatch().add(new OperationBody().withUri(uri.build().toString()).withBody(obj));
+		final String payload = getPatchConverter().convertPatchFormat(obj);
+		currentTransaction.getPatch().add(new OperationBody().withUri(uri.build().toString()).withBody(payload));
 		incrementActionAmount();
 		return this;
 	}
@@ -247,9 +268,19 @@ public class AAITransactionalClient extends AAIClient {
 			return Optional.empty();
 		}
 	}
-	private Relationship buildRelationship(AAIUri uri) {
+	private Relationship buildRelationship(AAIResourceUri uri) {
+		return buildRelationship(uri, Optional.empty());
+	}
+	
+	private Relationship buildRelationship(AAIResourceUri uri, AAIEdgeLabel label) {
+		return buildRelationship(uri, Optional.of(label));
+	}
+	private Relationship buildRelationship(AAIResourceUri uri, Optional<AAIEdgeLabel> label) {
 		final Relationship result = new Relationship();
 		result.setRelatedLink(uri.build().toString());
+		if (label.isPresent()) {
+			result.setRelationshipLabel(label.toString());
+		}
 		return result;
 	}
 
@@ -261,4 +292,8 @@ public class AAITransactionalClient extends AAIClient {
 	protected Transactions getTransactions() {
 		return this.transactions;
 	}
+	
+	protected AAIPatchConverter getPatchConverter() {
+		return this.patchConverter;
+	}
 }
diff --git a/common/src/main/java/org/onap/so/client/aai/entities/bulkprocess/OperationBody.java b/common/src/main/java/org/onap/so/client/aai/entities/bulkprocess/OperationBody.java
index 1803440edd..4b2aac1364 100644
--- a/common/src/main/java/org/onap/so/client/aai/entities/bulkprocess/OperationBody.java
+++ b/common/src/main/java/org/onap/so/client/aai/entities/bulkprocess/OperationBody.java
@@ -23,6 +23,8 @@ package org.onap.so.client.aai.entities.bulkprocess;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.annotation.JsonRawValue;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 
 @JsonInclude(JsonInclude.Include.NON_NULL)
 @JsonPropertyOrder({
@@ -34,6 +36,7 @@ public class OperationBody {
 @JsonProperty("uri")
 private String uri;
 @JsonProperty("body")
+@JsonSerialize(using = OperationBodySerializer.class)
 private Object body;
 
 @JsonProperty("uri")
diff --git a/common/src/main/java/org/onap/so/client/aai/entities/bulkprocess/OperationBodySerializer.java b/common/src/main/java/org/onap/so/client/aai/entities/bulkprocess/OperationBodySerializer.java
new file mode 100644
index 0000000000..2981e0deef
--- /dev/null
+++ b/common/src/main/java/org/onap/so/client/aai/entities/bulkprocess/OperationBodySerializer.java
@@ -0,0 +1,54 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 - 2018 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai.entities.bulkprocess;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class OperationBodySerializer extends StdSerializer<Object> {
+
+	private static final long serialVersionUID = 5367385969270400106L;
+
+	public OperationBodySerializer() {
+		this(null);
+	}
+	public OperationBodySerializer(Class<Object> t) {
+		super(t);
+	}
+
+	@Override
+	public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
+			throws IOException, JsonProcessingException {
+		
+		if (value instanceof String) {
+			gen.writeRawValue((String)value);
+		} else {
+			gen.writeObject(value);
+		}
+		
+	}
+
+}
+
diff --git a/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyRequest.java b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyRequest.java
new file mode 100644
index 0000000000..f2626e9e43
--- /dev/null
+++ b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyRequest.java
@@ -0,0 +1,88 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai.entities.singletransaction;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.annotation.JsonRawValue;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({
+"action",
+"uri",
+"body"
+})
+public class OperationBodyRequest {
+
+@JsonProperty("action")
+private String action;
+@JsonProperty("uri")
+private String uri;
+@JsonProperty("body")
+@JsonSerialize(using = OperationBodyRequestSerializer.class)
+private Object body;
+
+
+public String getAction() {
+	return action;
+}
+
+public void setAction(String action) {
+	this.action = action;
+}
+
+public OperationBodyRequest withAction(String action) {
+	this.action = action;
+	return this;
+}
+@JsonProperty("uri")
+public String getUri() {
+return uri;
+}
+
+@JsonProperty("uri")
+public void setUri(String uri) {
+this.uri = uri;
+}
+
+public OperationBodyRequest withUri(String uri) {
+this.uri = uri;
+return this;
+}
+
+@JsonProperty("body")
+public Object getBody() {
+return body;
+}
+
+@JsonProperty("body")
+public void setBody(Object body) {
+this.body = body;
+}
+
+public OperationBodyRequest withBody(Object body) {
+this.body = body;
+return this;
+}
+
+}
diff --git a/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyRequestSerializer.java b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyRequestSerializer.java
new file mode 100644
index 0000000000..170719962e
--- /dev/null
+++ b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyRequestSerializer.java
@@ -0,0 +1,54 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 - 2018 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai.entities.singletransaction;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+public class OperationBodyRequestSerializer extends StdSerializer<Object> {
+
+	private static final long serialVersionUID = 5367385969270400106L;
+
+	public OperationBodyRequestSerializer() {
+		this(null);
+	}
+	public OperationBodyRequestSerializer(Class<Object> t) {
+		super(t);
+	}
+
+	@Override
+	public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
+			throws IOException, JsonProcessingException {
+		
+		if (value instanceof String) {
+			gen.writeRawValue((String)value);
+		} else {
+			gen.writeObject(value);
+		}
+		
+	}
+
+}
+
diff --git a/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyResponse.java b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyResponse.java
new file mode 100644
index 0000000000..71f65b50db
--- /dev/null
+++ b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/OperationBodyResponse.java
@@ -0,0 +1,71 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 - 2018 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai.entities.singletransaction;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@JsonPropertyOrder({
+"action",
+"uri",
+"response-status-code",
+"response-body"
+})
+public class OperationBodyResponse {
+
+	@JsonProperty("action")
+	public String action;
+	@JsonProperty("uri")
+	public String uri;
+	@JsonProperty("response-status-code")
+	public Integer responseStatusCode;
+	@JsonProperty("response-body")
+	public Object responseBody;
+	
+	public String getAction() {
+		return action;
+	}
+	public void setAction(String action) {
+		this.action = action;
+	}
+	public String getUri() {
+		return uri;
+	}
+	public void setUri(String uri) {
+		this.uri = uri;
+	}
+	@JsonProperty("response-status-code")
+	public Integer getResponseStatusCode() {
+		return responseStatusCode;
+	}
+	@JsonProperty("response-status-code")
+	public void setResponseStatusCode(Integer responseStatusCode) {
+		this.responseStatusCode = responseStatusCode;
+	}
+	@JsonProperty("response-body")
+	public Object getResponseBody() {
+		return responseBody;
+	}
+	@JsonProperty("response-body")
+	public void getResponseBody(Object responseBody) {
+		this.responseBody = responseBody;
+	}
+}
diff --git a/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/SingleTransactionRequest.java b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/SingleTransactionRequest.java
new file mode 100644
index 0000000000..0d392c453f
--- /dev/null
+++ b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/SingleTransactionRequest.java
@@ -0,0 +1,45 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 - 2018 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai.entities.singletransaction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SingleTransactionRequest {
+
+	@JsonProperty("operations")
+	public List<OperationBodyRequest> operations;
+	
+	public List<OperationBodyRequest> getOperations() {
+		
+		if (operations == null) {
+			operations = new ArrayList<>();
+		}
+		
+		return operations;
+	}
+	
+	public void setOperations(List<OperationBodyRequest> operations) {
+		this.operations = operations;
+	}
+}
diff --git a/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/SingleTransactionResponse.java b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/SingleTransactionResponse.java
new file mode 100644
index 0000000000..db251b5b3b
--- /dev/null
+++ b/common/src/main/java/org/onap/so/client/aai/entities/singletransaction/SingleTransactionResponse.java
@@ -0,0 +1,47 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 - 2018 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai.entities.singletransaction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SingleTransactionResponse {
+
+	@JsonProperty("operation-responses")
+	public List<OperationBodyResponse> operationResponses;
+
+	@JsonProperty("operation-responses")
+	public List<OperationBodyResponse> getOperationResponses() {
+		if (operationResponses == null) {
+			operationResponses = new ArrayList<>();
+		}
+		return operationResponses;
+	}
+
+	@JsonProperty("operation-responses")
+	public void setOperationResponses(List<OperationBodyResponse> operationResponses) {
+		this.operationResponses = operationResponses;
+	}
+	
+	
+}
diff --git a/common/src/test/java/org/onap/so/client/aai/AAIPatchConverterTest.java b/common/src/test/java/org/onap/so/client/aai/AAIPatchConverterTest.java
new file mode 100644
index 0000000000..008b612cd8
--- /dev/null
+++ b/common/src/test/java/org/onap/so/client/aai/AAIPatchConverterTest.java
@@ -0,0 +1,102 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 - 2018 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.onap.aai.domain.yang.GenericVnf;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+
+@RunWith(MockitoJUnitRunner.class)
+public class AAIPatchConverterTest {
+
+	private ObjectMapper mapper = new AAICommonObjectMapperProvider().getMapper();
+
+	@Test
+	public void convertObjectToPatchFormatTest() throws URISyntaxException, JsonParseException, JsonMappingException, IOException {
+		AAIPatchConverter validator = new AAIPatchConverter();
+		GenericVnf vnf = new GenericVnf();
+		vnf.setIpv4Loopback0Address("");
+		String result = validator.marshallObjectToPatchFormat(vnf);
+		GenericVnf resultObj = mapper.readValue(result.toString(), GenericVnf.class);
+		assertTrue("expect object to become a String to prevent double marshalling", result instanceof String);
+		assertNull("expect null because of custom mapper", resultObj.getIpv4Loopback0Address());
+		
+	}
+	
+	@Test
+	public void convertStringToPatchFormatTest() throws URISyntaxException, JsonParseException, JsonMappingException, IOException {
+		AAIPatchConverter validator = new AAIPatchConverter();
+		String payload = "{\"ipv4-loopback0-address\":\"\"}";
+		String result = validator.marshallObjectToPatchFormat(payload);
+		
+		assertEquals("expect no change", payload, result);
+	}
+	
+	@Test
+	public void convertMapToPatchFormatTest() throws URISyntaxException, JsonParseException, JsonMappingException, IOException {
+		AAIPatchConverter validator = new AAIPatchConverter();
+		HashMap<String, String> map = new HashMap<>();
+		map.put("ipv4-loopback0-address", "");
+		String result = validator.marshallObjectToPatchFormat(map);
+		
+		assertEquals("expect string", "{\"ipv4-loopback0-address\":\"\"}", result);
+	}
+	
+	@Test
+	public void hasComplexObjectTest() {
+		AAIPatchConverter validator = new AAIPatchConverter();
+		String hasNesting = "{ \"hello\" : \"world\", \"nested\" : { \"key\" : \"value\" } }";
+		String noNesting = "{ \"hello\" : \"world\" }";
+		String arrayCase = "{ \"hello\" : \"world\", \"nestedSimple\" : [\"value1\" , \"value2\"], \"nestedComplex\" : [{\"key\" : \"value\"}]}";
+		String empty = "{}";
+		String arrayCaseSimpleOnly = "{ \"hello\" : \"world\", \"nestedSimple\" : [\"value1\" , \"value2\"]}";
+		String relationshipListCaseNesting = "{ \"hello\" : \"world\", \"nestedSimple\" : [\"value1\" , \"value2\"], \"relationship-list\" : [{\"key\" : \"value\"}], \"nested\" : { \"key\" : \"value\" }}";
+		String relationshipListCase = "{ \"hello\" : \"world\", \"nestedSimple\" : [\"value1\" , \"value2\"], \"relationship-list\" : [{\"key\" : \"value\"}]}";
+		String nothing = "";
+		
+		assertTrue("expect has nesting", validator.hasComplexObject(hasNesting));
+		assertFalse("expect no nesting", validator.hasComplexObject(noNesting));
+		assertTrue("expect has nesting", validator.hasComplexObject(arrayCase));
+		assertFalse("expect no nesting", validator.hasComplexObject(empty));
+		assertFalse("expect no nesting", validator.hasComplexObject(arrayCaseSimpleOnly));
+		assertFalse("expect no nesting", validator.hasComplexObject(relationshipListCase));
+		assertTrue("expect has nesting", validator.hasComplexObject(relationshipListCaseNesting));
+		assertFalse("expect no nesting", validator.hasComplexObject(nothing));
+	}
+	
+}
diff --git a/common/src/test/java/org/onap/so/client/aai/AAIRestClientTest.java b/common/src/test/java/org/onap/so/client/aai/AAIRestClientTest.java
index f2e371c999..752c49eb5b 100644
--- a/common/src/test/java/org/onap/so/client/aai/AAIRestClientTest.java
+++ b/common/src/test/java/org/onap/so/client/aai/AAIRestClientTest.java
@@ -21,18 +21,16 @@
 package org.onap.so.client.aai;
 
 import static org.hamcrest.CoreMatchers.containsString;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
-import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.HashMap;
 
 import javax.ws.rs.core.Response;
 
@@ -42,11 +40,9 @@ import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
-import org.onap.aai.domain.yang.GenericVnf;
+import org.onap.so.client.RestClientSSL;
 import org.onap.so.client.graphinventory.exceptions.GraphInventoryPatchDepthExceededException;
 
-import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.databind.JsonMappingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 @RunWith(MockitoJUnitRunner.class)
@@ -60,65 +56,23 @@ public class AAIRestClientTest {
 	@Rule
 	public ExpectedException thrown = ExpectedException.none();
 	
-	@Test
-	public void convertObjectToPatchFormatTest() throws URISyntaxException, JsonParseException, JsonMappingException, IOException {
-		AAIRestClient client = new AAIRestClient(props, new URI(""));
-		GenericVnf vnf = new GenericVnf();
-		vnf.setIpv4Loopback0Address("");
-		String result = client.convertObjectToPatchFormat(vnf);
-		GenericVnf resultObj = mapper.readValue(result.toString(), GenericVnf.class);
-		assertTrue("expect object to become a String to prevent double marshalling", result instanceof String);
-		assertNull("expect null because of custom mapper", resultObj.getIpv4Loopback0Address());
-		
-	}
-	
-	@Test
-	public void convertStringToPatchFormatTest() throws URISyntaxException, JsonParseException, JsonMappingException, IOException {
-		AAIRestClient client = new AAIRestClient(props, new URI(""));
-		String payload = "{\"ipv4-loopback0-address\":\"\"}";
-		String result = client.convertObjectToPatchFormat(payload);
-		
-		assertEquals("expect no change", payload, result);
-	}
-	
-	@Test
-	public void convertMapToPatchFormatTest() throws URISyntaxException, JsonParseException, JsonMappingException, IOException {
-		AAIRestClient client = new AAIRestClient(props, new URI(""));
-		HashMap<String, String> map = new HashMap<>();
-		map.put("ipv4-loopback0-address", "");
-		String result = client.convertObjectToPatchFormat(map);
-		
-		assertEquals("expect string", "{\"ipv4-loopback0-address\":\"\"}", result);
-	}
-	
 	@Test
 	public void failPatchOnComplexObject() throws URISyntaxException {
 		AAIRestClient client = new AAIRestClient(props, new URI(""));
 		this.thrown.expect(GraphInventoryPatchDepthExceededException.class); 
 		this.thrown.expectMessage(containsString("Object exceeds allowed depth for update action"));
 		client.patch("{ \"hello\" : \"world\", \"nestedSimple\" : [\"value1\" , \"value2\"], \"relationship-list\" : [{\"key\" : \"value\"}], \"nested\" : { \"key\" : \"value\" }}");
-
 	}
 	
 	@Test
-	public void hasComplexObjectTest() throws URISyntaxException {
+	public void verifyPatchValidation() throws URISyntaxException {
 		AAIRestClient client = new AAIRestClient(props, new URI(""));
-		String hasNesting = "{ \"hello\" : \"world\", \"nested\" : { \"key\" : \"value\" } }";
-		String noNesting = "{ \"hello\" : \"world\" }";
-		String arrayCase = "{ \"hello\" : \"world\", \"nestedSimple\" : [\"value1\" , \"value2\"], \"nestedComplex\" : [{\"key\" : \"value\"}]}";
-		String empty = "{}";
-		String arrayCaseSimpleOnly = "{ \"hello\" : \"world\", \"nestedSimple\" : [\"value1\" , \"value2\"]}";
-		String relationshipListCaseNesting = "{ \"hello\" : \"world\", \"nestedSimple\" : [\"value1\" , \"value2\"], \"relationship-list\" : [{\"key\" : \"value\"}], \"nested\" : { \"key\" : \"value\" }}";
-		String relationshipListCase = "{ \"hello\" : \"world\", \"nestedSimple\" : [\"value1\" , \"value2\"], \"relationship-list\" : [{\"key\" : \"value\"}]}";
-		String nothing = "";
-		
-		assertTrue("expect has nesting", client.hasComplexObject(hasNesting));
-		assertFalse("expect no nesting", client.hasComplexObject(noNesting));
-		assertTrue("expect has nesting", client.hasComplexObject(arrayCase));
-		assertFalse("expect no nesting", client.hasComplexObject(empty));
-		assertFalse("expect no nesting", client.hasComplexObject(arrayCaseSimpleOnly));
-		assertFalse("expect no nesting", client.hasComplexObject(relationshipListCase));
-		assertTrue("expect has nesting", client.hasComplexObject(relationshipListCaseNesting));
-		assertFalse("expect no nesting", client.hasComplexObject(nothing));
+		AAIRestClient spy = spy(client);
+		AAIPatchConverter patchValidatorMock = mock(AAIPatchConverter.class);
+		doReturn(patchValidatorMock).when(spy).getPatchConverter();
+		String payload = "{}";
+		doReturn(Response.ok().build()).when(spy).method(eq("PATCH"), any());
+		spy.patch(payload);
+		verify(patchValidatorMock, times(1)).convertPatchFormat(eq((Object)payload));
 	}
 }
diff --git a/common/src/test/java/org/onap/so/client/aai/AAISingleTransactionClientTest.java b/common/src/test/java/org/onap/so/client/aai/AAISingleTransactionClientTest.java
new file mode 100644
index 0000000000..8c42686e5f
--- /dev/null
+++ b/common/src/test/java/org/onap/so/client/aai/AAISingleTransactionClientTest.java
@@ -0,0 +1,133 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 - 2018 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.so.client.aai;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.aai.domain.yang.Pserver;
+import org.onap.aai.domain.yang.v9.Complex;
+import org.onap.so.client.aai.entities.singletransaction.SingleTransactionRequest;
+import org.onap.so.client.aai.entities.singletransaction.SingleTransactionResponse;
+import org.onap.so.client.aai.entities.uri.AAIResourceUri;
+import org.onap.so.client.aai.entities.uri.AAIUriFactory;
+import org.onap.so.client.defaultproperties.DefaultAAIPropertiesImpl;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+public class AAISingleTransactionClientTest {
+
+	private final static String AAI_JSON_FILE_LOCATION = "src/test/resources/__files/aai/singletransaction/";
+	AAIResourceUri uriA = AAIUriFactory.createResourceUri(AAIObjectType.PSERVER, "pserver-hostname");
+	AAIResourceUri uriB = AAIUriFactory.createResourceUri(AAIObjectType.COMPLEX, "my-complex");
+	
+	ObjectMapper mapper;
+	
+	@Before
+	public void before() throws JsonParseException, JsonMappingException, IOException {
+		mapper = new AAICommonObjectMapperProvider().getMapper();
+		mapper.enable(SerializationFeature.INDENT_OUTPUT);
+	}
+	
+	@Test
+	public void testRequest() throws IOException {
+		AAIResourcesClient client = createClient();
+		Pserver pserver = new Pserver();
+		pserver.setHostname("pserver-hostname");
+		pserver.setFqdn("pserver-bulk-process-single-transactions-multiple-actions-1-fqdn");
+		Pserver pserver2 = new Pserver();
+		pserver2.setFqdn("patched-fqdn");
+		Complex complex = new Complex();
+		complex.setCity("my-city");
+		AAISingleTransactionClient singleTransaction = 
+		client.beginSingleTransaction()
+			.create(uriA, pserver)
+			.update(uriA, pserver2)
+			.create(uriB, complex);
+		
+		
+		SingleTransactionRequest actual = singleTransaction.getRequest();
+		
+		SingleTransactionRequest expected = mapper.readValue(this.getJson("sample-request.json"), SingleTransactionRequest.class);
+		
+		JSONAssert.assertEquals(mapper.writeValueAsString(expected),mapper.writeValueAsString(actual), false);
+	}
+	
+	@Test
+	public void testFailure() throws IOException {
+		AAIResourcesClient client = createClient();
+		AAISingleTransactionClient singleTransaction = client.beginSingleTransaction();
+		SingleTransactionResponse expected = mapper.readValue(this.getJson("sample-response-failure.json"), SingleTransactionResponse.class);
+		Optional<String> errorMessage = singleTransaction.locateErrorMessages(expected);
+		
+		assertThat(expected.getOperationResponses().size(), greaterThan(0));
+		assertThat(errorMessage.isPresent(), equalTo(true));
+		
+	}
+	
+	@Test
+	public void testSuccessResponse() throws IOException {
+		AAIResourcesClient client = createClient();
+		AAISingleTransactionClient singleTransaction = client.beginSingleTransaction();
+		SingleTransactionResponse expected = mapper.readValue(this.getJson("sample-response.json"), SingleTransactionResponse.class);
+		Optional<String> errorMessage = singleTransaction.locateErrorMessages(expected);
+		
+		assertThat(expected.getOperationResponses().size(), greaterThan(0));
+		assertThat(errorMessage.isPresent(), equalTo(false));
+		
+	}
+	
+	@Test
+	public void confirmPatchFormat() {
+		AAISingleTransactionClient singleTransaction = spy(new AAISingleTransactionClient(AAIVersion.LATEST));
+		AAIPatchConverter mock = mock(AAIPatchConverter.class);
+		doReturn(mock).when(singleTransaction).getPatchConverter();
+		singleTransaction.update(uriA, "{}");
+		verify(mock, times(1)).convertPatchFormat(any());
+	}
+	private String getJson(String filename) throws IOException {
+		 return new String(Files.readAllBytes(Paths.get(AAI_JSON_FILE_LOCATION + filename)));
+	}
+	
+	private AAIResourcesClient createClient() {
+		AAIResourcesClient client = spy(new AAIResourcesClient());
+		doReturn(new DefaultAAIPropertiesImpl()).when(client).getRestProperties();
+		return client;
+	}
+}
diff --git a/common/src/test/java/org/onap/so/client/aai/AAITransactionalClientTest.java b/common/src/test/java/org/onap/so/client/aai/AAITransactionalClientTest.java
index f6ee826a78..cbf8d67a82 100644
--- a/common/src/test/java/org/onap/so/client/aai/AAITransactionalClientTest.java
+++ b/common/src/test/java/org/onap/so/client/aai/AAITransactionalClientTest.java
@@ -21,8 +21,12 @@
 package org.onap.so.client.aai;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import java.io.IOException;
 import java.nio.file.Files;
@@ -136,6 +140,15 @@ public class AAITransactionalClientTest {
 		assertEquals(transactions.locateErrorMessages(getJson("response-failure.json")).get(), "another error message\nmy great error");
 	}
 	
+	@Test
+	public void confirmPatchFormat() {
+		AAITransactionalClient client = spy(new AAITransactionalClient(AAIVersion.LATEST));
+		AAIPatchConverter mock = mock(AAIPatchConverter.class);
+		doReturn(mock).when(client).getPatchConverter();
+		client.update(uriA, "{}");
+		verify(mock, times(1)).convertPatchFormat(any());
+	}
+	
 	private String getJson(String filename) throws IOException {
 		 return new String(Files.readAllBytes(Paths.get(AAI_JSON_FILE_LOCATION + filename)));
 	}
diff --git a/common/src/test/resources/__files/aai/singletransaction/sample-request.json b/common/src/test/resources/__files/aai/singletransaction/sample-request.json
new file mode 100644
index 0000000000..f0761a07b6
--- /dev/null
+++ b/common/src/test/resources/__files/aai/singletransaction/sample-request.json
@@ -0,0 +1,26 @@
+{
+  "operations": [
+    {
+      "action": "put",
+      "uri": "/cloud-infrastructure/pservers/pserver/pserver-hostname",
+      "body": {
+        "hostname": "pserver-hostname",
+        "fqdn": "pserver-bulk-process-single-transactions-multiple-actions-1-fqdn"
+      }
+    },
+    {
+      "action": "patch",
+      "uri": "/cloud-infrastructure/pservers/pserver/pserver-hostname",
+      "body": {
+        "fqdn": "patched-fqdn"
+      }
+    },
+    {
+      "action": "put",
+      "uri": "/cloud-infrastructure/complexes/complex/my-complex",
+      "body": {
+        "city": "my-city"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/common/src/test/resources/__files/aai/singletransaction/sample-response-failure.json b/common/src/test/resources/__files/aai/singletransaction/sample-response-failure.json
new file mode 100644
index 0000000000..d0b0e39924
--- /dev/null
+++ b/common/src/test/resources/__files/aai/singletransaction/sample-response-failure.json
@@ -0,0 +1,30 @@
+{
+	"operation-responses": [
+		{
+			"action": "put",
+			"uri": "/cloud-infrastructure/pservers/pserver/pserver-hostname",
+			"response-status-code": 201,
+			"response-body": null
+		},
+		{
+			"action": "patch",
+			"uri": "/cloud-infrastructure/pservers/pserver/pserver-hostname",
+			"response-status-code": 200,
+			"response-body": null
+		},
+		{
+			"action": "put",
+			"uri": "/cloud-infrastructure/complexes/complex/my-complex",
+			"response-status-code": 400,
+			"response-body": {
+				"requestError": {
+					"serviceException": {
+						"messageId": "SVC3003",
+						"text": "another error message",
+						"variables": []
+					}
+				}
+			}
+		}
+	]
+}
\ No newline at end of file
diff --git a/common/src/test/resources/__files/aai/singletransaction/sample-response.json b/common/src/test/resources/__files/aai/singletransaction/sample-response.json
new file mode 100644
index 0000000000..a5b322ee2e
--- /dev/null
+++ b/common/src/test/resources/__files/aai/singletransaction/sample-response.json
@@ -0,0 +1,22 @@
+{
+  "operation-responses": [
+    {
+      "action": "put",
+      "uri": "/cloud-infrastructure/pservers/pserver/pserver-hostname",
+      "response-status-code": 201,
+      "response-body": null
+    },
+    {
+      "action": "patch",
+      "uri": "/cloud-infrastructure/pservers/pserver/pserver-hostname",
+      "response-status-code": 200,
+      "response-body": null
+    },
+    {
+      "action": "put",
+      "uri": "/cloud-infrastructure/complexes/complex/my-complex",
+      "response-status-code": 201,
+      "response-body": null
+    }
+  ]
+}
\ No newline at end of file
-- 
cgit