aboutsummaryrefslogtreecommitdiffstats
path: root/ajsc-aai/src/main/java/org/openecomp/aai/rest/BulkAddConsumer.java
diff options
context:
space:
mode:
Diffstat (limited to 'ajsc-aai/src/main/java/org/openecomp/aai/rest/BulkAddConsumer.java')
-rw-r--r--ajsc-aai/src/main/java/org/openecomp/aai/rest/BulkAddConsumer.java412
1 files changed, 412 insertions, 0 deletions
diff --git a/ajsc-aai/src/main/java/org/openecomp/aai/rest/BulkAddConsumer.java b/ajsc-aai/src/main/java/org/openecomp/aai/rest/BulkAddConsumer.java
new file mode 100644
index 0000000..3d19f87
--- /dev/null
+++ b/ajsc-aai/src/main/java/org/openecomp/aai/rest/BulkAddConsumer.java
@@ -0,0 +1,412 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * org.openecomp.aai
+ * ================================================================================
+ * 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.openecomp.aai.rest;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Encoded;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.javatuples.Pair;
+import org.openecomp.aai.exceptions.AAIException;
+import org.openecomp.aai.introspection.Introspector;
+import org.openecomp.aai.introspection.Loader;
+import org.openecomp.aai.introspection.ModelType;
+import org.openecomp.aai.introspection.Version;
+import org.openecomp.aai.logging.LogLine;
+import org.openecomp.aai.logging.LogLineBuilder;
+import org.openecomp.aai.parsers.query.QueryParser;
+import org.openecomp.aai.rest.db.DBRequest;
+import org.openecomp.aai.rest.db.HttpEntry;
+import org.openecomp.aai.rest.util.ValidateEncoding;
+import org.openecomp.aai.serialization.engines.QueryStyle;
+import org.openecomp.aai.serialization.engines.TransactionalGraphEngine;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import com.thinkaurelius.titan.core.TitanTransaction;
+
+/**
+ * The Class BulkAddConsumer.
+ */
+/*
+ * The purpose of this endpoint is to allow a client to add
+ * multiple objects with one request. It may take
+ * one or more transaction objects containing one or more
+ * objects to add.
+ * The transactions are independent of each other -
+ * if one fails, its effects are rolled back, but the others' aren't.
+ * Within a single transaction, if adding one object fails, all the others'
+ * changes are rolled back.
+ */
+@Path("{version: v[8]}/bulkadd")
+public class BulkAddConsumer extends RESTAPI {
+
+ /** The introspector factory type. */
+ private ModelType introspectorFactoryType = ModelType.MOXY;
+
+ /** The query style. */
+ private QueryStyle queryStyle = QueryStyle.TRAVERSAL;
+
+ /**
+ * Bulk add.
+ *
+ * @param content the content
+ * @param versionParam the version param
+ * @param uri the uri
+ * @param headers the headers
+ * @param info the info
+ * @param req the req
+ * @return the response
+ */
+ @PUT
+ @Consumes({ MediaType.APPLICATION_JSON})
+ @Produces({ MediaType.APPLICATION_JSON})
+ public Response bulkAdd(String content, @PathParam("version")String versionParam, @PathParam("uri") @Encoded String uri, @Context HttpHeaders headers, @Context UriInfo info, @Context HttpServletRequest req){
+
+ String transId = headers.getRequestHeaders().getFirst("X-TransactionId");
+ String sourceOfTruth = headers.getRequestHeaders().getFirst("X-FromAppId");
+
+ LogLineBuilder llBuilder = new LogLineBuilder(transId, sourceOfTruth);
+ LogLine logline = llBuilder.build(COMPONENT, HttpMethod.PUT.toString());
+
+ String outputMediaType = getMediaType(headers.getAcceptableMediaTypes());
+ Version version = Version.valueOf(versionParam);
+ HttpEntry httpEntry = new HttpEntry(version, introspectorFactoryType, queryStyle, llBuilder);
+
+
+ Response response = null;
+
+ /* A Response will be generated for each object in each transaction.
+ * To keep track of what came from where to give organized feedback to the client,
+ * we keep responses from a given transaction together in one list (hence all being a list of lists)
+ * and pair each response with its matching URI (which will be null if there wasn't one).
+ */
+ List<List<Pair<URI, Response>>> allResponses = new ArrayList<List<Pair<URI, Response>>>();
+
+ try {
+ //TODO add auth check when this endpoint added to that auth properties files
+
+ Loader loader = httpEntry.getLoader();
+ TransactionalGraphEngine dbEngine = httpEntry.getDbEngine();
+
+ JsonArray transactions = getTransactions(content);
+
+ for (int i = 0; i < transactions.size(); i++){
+ TitanTransaction g = dbEngine.getGraph().newTransaction();
+ URI thisUri = null;
+ List<Pair<URI, Introspector>> tuples = new ArrayList<Pair<URI, Introspector>>();
+ try {
+ JsonElement transObj = transactions.get(i);
+ if (!(transObj instanceof JsonObject)) {
+ throw new AAIException("AAI_6111", "input payload does not follow bulk add interface");
+ }
+ JsonObject transaction = transObj.getAsJsonObject();
+
+ fillObjectTuplesFromTransaction(tuples, transaction, loader, dbEngine, outputMediaType);
+ if (tuples.size() == 0) {
+ //case where user sends a validly formatted transactions object but
+ //which has no actual things in it for A&AI to do anything with
+ //assuming we should count this as a user error
+ throw new AAIException("AAI_6118", "payload had no objects to operate on");
+ }
+
+ List<DBRequest> requests = new ArrayList<>();
+ for (Pair<URI, Introspector> tuple : tuples){
+ thisUri = tuple.getValue0();
+ QueryParser uriQuery = dbEngine.getQueryBuilder().createQueryFromURI(thisUri);
+ DBRequest request = new DBRequest(HttpMethod.PUT, thisUri, uriQuery, tuple.getValue1(), headers, info, transId);
+ requests.add(request);
+ }
+
+ Pair<Boolean, List<Pair<URI, Response>>> results = httpEntry.process(g, requests, sourceOfTruth);
+ List<Pair<URI, Response>> responses = results.getValue1();
+ allResponses.add(responses);
+ if (results.getValue0()) { //everything was processed without error
+ g.commit();
+ } else { //something failed
+ g.rollback();
+ }
+ } catch (Exception e) {
+ /* While httpEntry.process handles its exceptions, exceptions thrown in earlier helpers
+ * bubbles up to here. As we want to tie error messages to the URI of the object that caused
+ * them, we catch here, generate a Response, bundle it with that URI, and move on.
+ */
+ if (tuples.size() != 0) { //failed somewhere in the middle of tuple-filling
+ Pair<URI,Introspector> lastTuple = tuples.get(tuples.size()-1); //last one in there was the problem
+ if (lastTuple.getValue1() == null){
+ //failed out before thisUri could be set but after tuples started being filled
+ thisUri = lastTuple.getValue0();
+ }
+ } //else failed out on empty payload so tuples never filled (or failed out even earlier than tuple-filling)
+ addExceptionCaseFailureResponse(allResponses, e, i, thisUri, headers, info, HttpMethod.PUT, logline);
+ g.rollback();
+ continue; /* if an exception gets thrown within a transaction we want to keep going to
+ the next transaction, not break out of the whole request */
+ }
+ }
+
+ String returnPayload = generateResponsePayload(allResponses);
+
+ //unless a top level error gets thrown, we want to 201 bc the client wanted a "fire and forget" kind of setup
+ response = Response
+ .status(Status.OK)
+ .entity(returnPayload)
+ .build();
+ } catch (AAIException e) { //these catches needed for handling top level errors in payload parsing where the whole request must fail out
+ response = consumerExceptionResponseGenerator(headers, info, HttpMethod.PUT, e, logline);
+ } catch(JsonSyntaxException e) {
+ AAIException ex = new AAIException("AAI_6111");
+ response = consumerExceptionResponseGenerator(headers, info, HttpMethod.PUT, ex, logline);
+ } catch (Exception e ) {
+ AAIException ex = new AAIException("AAI_4000", e);
+ response = consumerExceptionResponseGenerator(headers, info, HttpMethod.PUT, ex, logline);
+ }
+
+ return response;
+ }
+
+
+ /**
+ * Gets the transactions.
+ *
+ * @param content - input JSON payload string
+ * @return JsonArray - the array of transactions
+ * @throws AAIException the AAI exception
+ * @throws JsonSyntaxException Parses and breaks the single payload into an array of individual transaction
+ * bodies to be processed.
+ */
+ private JsonArray getTransactions(String content) throws AAIException, JsonSyntaxException {
+ JsonParser parser = new JsonParser();
+
+ JsonObject input = parser.parse(content).getAsJsonObject();
+
+ if (!(input.has("transactions"))) {
+ throw new AAIException("AAI_6118", "input payload does not follow bulk add interface - missing \"transactions\"");
+ }
+ JsonElement transactionsObj = input.get("transactions");
+
+ if (!(transactionsObj.isJsonArray())){
+ throw new AAIException("AAI_6111", "input payload does not follow bulk add interface");
+ }
+ JsonArray transactions = transactionsObj.getAsJsonArray();
+ if (transactions.size() == 0) {
+ //case where user sends a validly formatted transactions object but
+ //which has no actual things in it for A&AI to do anything with
+ //assuming we should count this as a user error
+ throw new AAIException("AAI_6118", "payload had no objects to operate on");
+ }
+ return transactions;
+ }
+
+ /**
+ * Fill object tuples from transaction.
+ *
+ * @param tuples the tuples
+ * @param transaction - JSON body containing the objects to be added
+ * each object must have a URI and an object body
+ * @param loader the loader
+ * @param dbEngine the db engine
+ * @param inputMediaType the input media type
+ * @return list of tuples containing each introspector-wrapped object and its given URI
+ * @throws AAIException the AAI exception
+ * @throws JsonSyntaxException the json syntax exception
+ * @throws UnsupportedEncodingException Walks through the given transaction and unmarshals each object in it, then bundles each
+ * with its URI.
+ */
+ private void fillObjectTuplesFromTransaction(List<Pair<URI, Introspector>> tuples, JsonObject transaction, Loader loader, TransactionalGraphEngine dbEngine, String inputMediaType) throws AAIException, JsonSyntaxException, UnsupportedEncodingException {
+
+ if (!(transaction.has("put"))){
+ throw new AAIException("AAI_6118", "input payload does not follow bulk add interface - missing \"put\"");
+ }
+
+ JsonElement transArrayObj = transaction.get("put"); //for right now only PUTS, later could be DELETEs or whatever too
+ if (!(transArrayObj.isJsonArray())) {
+ throw new AAIException("AAI_6111", "input payload does not follow bulk add interface");
+ }
+
+ JsonArray transArray = transArrayObj.getAsJsonArray(); //these contents are the items to be added
+ for (int i=0; i<transArray.size(); i++) {
+ Pair<URI, Introspector> tuple = Pair.with(null, null);
+
+ try {
+ JsonElement itemObj = transArray.get(i);
+ if (!(itemObj.isJsonObject())) {
+ throw new AAIException("AAI_6111", "input payload does not follow bulk add interface");
+ }
+
+ JsonObject item = itemObj.getAsJsonObject();
+ JsonElement itemURIfield = item.get("uri");
+ if (itemURIfield == null) {
+ throw new AAIException("AAI_6118", "must include object uri");
+ }
+ String uriStr = itemURIfield.getAsString();
+ URI uri = UriBuilder.fromPath(uriStr).build();
+
+ /* adding the uri as soon as we have one (valid or not) lets us
+ * keep any errors with their corresponding uris for client feedback
+ */
+ tuple = Pair.with(uri, null);
+
+ if (!ValidateEncoding.getInstance().validate(uri)) {
+ throw new AAIException("AAI_3008", "uri=" + uri.getPath());
+ }
+
+ QueryParser uriQuery = dbEngine.getQueryBuilder().createQueryFromURI(uri);
+ String objName = uriQuery.getResultType();
+
+ if (!(item.has("body"))){
+ throw new AAIException("AAI_6118", "input payload does not follow bulk add interface - missing \"body\"");
+ }
+ JsonElement bodyObj = item.get("body");
+ if (!(bodyObj.isJsonObject())) {
+ throw new AAIException("AAI_6111", "input payload does not follow bulk add interface");
+ }
+ Gson gson = new Gson();
+
+ String bodyStr = gson.toJson(bodyObj);
+
+ Introspector obj = loader.unmarshal(objName, bodyStr, org.openecomp.aai.rest.MediaType.getEnum(inputMediaType));
+ if (obj == null) {
+ throw new AAIException("AAI_3000", "object could not be unmarshalled:" + bodyStr);
+ }
+
+ this.validateIntrospector(obj, loader, uri, true);
+ tuple = Pair.with(uri, obj);
+ tuples.add(tuple);
+ } catch (AAIException e) {
+ // even if tuple doesn't have a uri or body, this way we keep all information associated with this error together
+ // even if both are null, that indicates how the input was messed up, so still useful to carry around like this
+ tuples.add(tuple);
+ throw e; //rethrow so the right response is generated on the level above
+ }
+ }
+ }
+
+ /**
+ * Generate response payload.
+ *
+ * @param allResponses - the list of the lists of responses from every action in every transaction requested
+ * @return A json string of similar format to the bulk add interface which for each response includes
+ * the original URI and a body with the status code of the response and the error message.
+ *
+ * Creates the payload for a single unified response from all responses generated
+ */
+ private String generateResponsePayload(List<List<Pair<URI,Response>>> allResponses){
+ JsonObject ret = new JsonObject();
+ JsonArray retArr = new JsonArray();
+
+ for(List<Pair<URI,Response>> responses : allResponses){
+ JsonObject tResp = new JsonObject();
+ JsonArray tArrResp = new JsonArray();
+
+ for (Pair<URI,Response> r : responses) {
+ JsonObject indPayload = new JsonObject();
+
+ URI origURI = r.getValue0();
+ if (origURI != null) {
+ indPayload.addProperty("uri", origURI.getPath());
+ } else {
+ indPayload.addProperty("uri", (String)null);
+ }
+
+ JsonObject body = new JsonObject();
+
+ int rStatus = r.getValue1().getStatus();
+ String rContents = null;
+
+ rContents = (String)r.getValue1().getEntity();
+
+ body.addProperty(new Integer(rStatus).toString(), rContents);
+ indPayload.add("body", body);
+
+ tArrResp.add(indPayload);
+ }
+
+ tResp.add("put", tArrResp);
+ retArr.add(tResp);
+ }
+ ret.add("transaction", retArr);
+ Gson gson = new GsonBuilder().serializeNulls().create();
+ String jsonStr = gson.toJson(ret);
+ return jsonStr;
+ }
+
+ /**
+ * Adds the exception case failure response.
+ *
+ * @param allResponses the all responses
+ * @param e the e
+ * @param index - index of which transaction was being processed when the exception was thrown
+ * @param thisUri the this uri
+ * @param headers the headers
+ * @param info the info
+ * @param templateAction the template action
+ * @param logline Generates a Response based on the given exception and adds it to the collection of responses for this request.
+ */
+ private void addExceptionCaseFailureResponse(List<List<Pair<URI, Response>>> allResponses, Exception e, int index, URI thisUri, HttpHeaders headers, UriInfo info, HttpMethod templateAction, LogLine logline){
+ AAIException ex = null;
+
+ if (!(e instanceof AAIException)){
+ ex = new AAIException("AAI_4000", e); //bc exception response generator needs AAIException
+ } else {
+ ex = (AAIException)e;
+ }
+
+ if (allResponses.size() != (index+1)) {
+ //index+1 bc if all transactions thus far have had a response list added
+ //the size will be one more than the current index (since those are offset by 1)
+
+ //this transaction doesn't have a response list yet, so create one
+ Response failResp = consumerExceptionResponseGenerator(headers, info, templateAction, ex, logline);
+ Pair<URI, Response> uriResp = Pair.with(thisUri, failResp);
+ List<Pair<URI, Response>> transRespList = new ArrayList<Pair<URI,Response>>();
+ transRespList.add(uriResp);
+ allResponses.add(transRespList);
+ } else {
+ //this transaction already has a response list, so add this failure response to it
+ Response failResp = consumerExceptionResponseGenerator(headers, info, templateAction, ex, logline);
+ Pair<URI, Response> uriResp = Pair.with(thisUri, failResp);
+ List<Pair<URI, Response>> tResps = allResponses.get(index);
+ tResps.add(uriResp);
+ }
+ }
+}