package com.tinkerpop.rexster.kibbles.batch; import com.tinkerpop.blueprints.CloseableIterable; import com.tinkerpop.blueprints.Edge; import com.tinkerpop.blueprints.Element; import com.tinkerpop.blueprints.Graph; import com.tinkerpop.blueprints.Index; import com.tinkerpop.blueprints.IndexableGraph; import com.tinkerpop.blueprints.Vertex; import com.tinkerpop.blueprints.util.io.graphson.GraphSONMode; import com.tinkerpop.blueprints.util.io.graphson.GraphSONUtility; import com.tinkerpop.rexster.RexsterApplicationGraph; import com.tinkerpop.rexster.RexsterResourceContext; import com.tinkerpop.rexster.Tokens; import com.tinkerpop.rexster.extension.AbstractRexsterExtension; import com.tinkerpop.rexster.extension.ExtensionApi; import com.tinkerpop.rexster.extension.ExtensionDefinition; import com.tinkerpop.rexster.extension.ExtensionDescriptor; import com.tinkerpop.rexster.extension.ExtensionMethod; import com.tinkerpop.rexster.extension.ExtensionNaming; import com.tinkerpop.rexster.extension.ExtensionPoint; import com.tinkerpop.rexster.extension.ExtensionResponse; import com.tinkerpop.rexster.extension.HttpMethod; import com.tinkerpop.rexster.extension.RexsterContext; import com.tinkerpop.rexster.util.ElementHelper; import com.tinkerpop.rexster.util.RequestObjectHelper; import org.apache.log4j.Logger; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONObject; import javax.ws.rs.core.Response; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * This extension allows batch/transactional operations on a graph. */ @ExtensionNaming(namespace = BatchExtension.EXTENSION_NAMESPACE, name = BatchExtension.EXTENSION_NAME) public class BatchExtension extends AbstractRexsterExtension { private static final Logger logger = Logger.getLogger(BatchExtension.class); public static final String EXTENSION_NAMESPACE = "tp"; public static final String EXTENSION_NAME = "batch"; private static final String WILDCARD = "*"; private static final String API_SHOW_TYPES = "displays the properties of the elements with their native data type (default is false)"; private static final String API_VALUES = "a list of element identifiers or index values to retrieve from the graph"; private static final String API_RETURN_KEYS = "an array of element property keys to return (default is to return all element properties)"; private static final String API_TYPE = "specifies whether to retrieve by identifier or index (default is id)" ; private static final String API_KEY = "specifies the index key"; @ExtensionDefinition(extensionPoint = ExtensionPoint.GRAPH, method = HttpMethod.GET, path = "vertices") @ExtensionDescriptor(description = "get a set of vertices from the graph.", api = { @ExtensionApi(parameterName = Tokens.REXSTER + "." + Tokens.SHOW_TYPES, description = API_SHOW_TYPES), @ExtensionApi(parameterName = Tokens.REXSTER + "." + Tokens.RETURN_KEYS, description = API_RETURN_KEYS), @ExtensionApi(parameterName = "values", description = API_VALUES), @ExtensionApi(parameterName = "type", description = API_TYPE), @ExtensionApi(parameterName = "key", description = API_KEY) }) public ExtensionResponse getVertices(@RexsterContext final RexsterResourceContext context, @RexsterContext final Graph graph) { final JSONObject requestObject = context.getRequestObject(); final JSONArray values = requestObject.optJSONArray("values"); final String type = requestObject.optString("type", "id"); final String key = requestObject.optString("key"); final ExtensionResponse error = checkParameters(context, values, type, key); if (error != null) { return error; } final boolean showTypes = RequestObjectHelper.getShowTypes(requestObject); final GraphSONMode mode = showTypes ? GraphSONMode.EXTENDED : GraphSONMode.NORMAL; final Set<String> returnKeys = RequestObjectHelper.getReturnKeys(requestObject, WILDCARD); try { final JSONArray jsonArray = new JSONArray(); if (type.equals("id")) { for (int ix = 0; ix < values.length(); ix++) { final Vertex vertexFound = graph.getVertex(ElementHelper.getTypedPropertyValue(values.optString(ix))); if (vertexFound != null) { jsonArray.put(GraphSONUtility.jsonFromElement(vertexFound, returnKeys, mode)); } } } else if (type.equals("index")) { Index idx = ((IndexableGraph)graph).getIndex(key, Vertex.class); for (int ix = 0; ix < values.length(); ix++) { CloseableIterable<Vertex> verticesFound = idx.get(key, ElementHelper.getTypedPropertyValue(values.optString(ix))); for (Vertex vertex : verticesFound) { jsonArray.put(GraphSONUtility.jsonFromElement(vertex, returnKeys, mode)); } verticesFound.close(); } } else if (type.equals("keyindex")) { for (int ix = 0; ix < values.length(); ix++) { Iterable<Vertex> verticesFound = graph.getVertices(key, ElementHelper.getTypedPropertyValue(values.optString(ix))); for (Vertex vertex : verticesFound) { jsonArray.put(GraphSONUtility.jsonFromElement(vertex, returnKeys, mode)); } } } final HashMap<String, Object> resultMap = new HashMap<String, Object>(); resultMap.put(Tokens.SUCCESS, true); resultMap.put(Tokens.RESULTS, jsonArray); final JSONObject resultObject = new JSONObject(resultMap); return ExtensionResponse.ok(resultObject); } catch (Exception mqe) { logger.error(mqe); return ExtensionResponse.error( "Error retrieving batch of vertices [" + values + "]", generateErrorJson()); } } @ExtensionDefinition(extensionPoint = ExtensionPoint.GRAPH, method = HttpMethod.GET, path = "edges") @ExtensionDescriptor(description = "get a set of edges from the graph.", api = { @ExtensionApi(parameterName = Tokens.REXSTER + "." + Tokens.SHOW_TYPES, description = API_SHOW_TYPES), @ExtensionApi(parameterName = Tokens.REXSTER + "." + Tokens.RETURN_KEYS, description = API_RETURN_KEYS), @ExtensionApi(parameterName = "values", description = API_VALUES), @ExtensionApi(parameterName = "type", description = API_TYPE), @ExtensionApi(parameterName = "key", description = API_KEY) }) public ExtensionResponse getEdges(@RexsterContext final RexsterResourceContext context, @RexsterContext final Graph graph) { final JSONObject requestObject = context.getRequestObject(); final JSONArray values = requestObject.optJSONArray("values"); final String type = requestObject.optString("type", "id"); final String key = requestObject.optString("key"); final ExtensionResponse error = checkParameters(context, values, type, key); if (error != null) { return error; } final boolean showTypes = RequestObjectHelper.getShowTypes(requestObject); final GraphSONMode mode = showTypes ? GraphSONMode.EXTENDED : GraphSONMode.NORMAL; final Set<String> returnKeys = RequestObjectHelper.getReturnKeys(requestObject, WILDCARD); try { final JSONArray jsonArray = new JSONArray(); if (type.equals("id")) { for (int ix = 0; ix < values.length(); ix++) { final Edge edgeFound = graph.getEdge(ElementHelper.getTypedPropertyValue(values.optString(ix))); if (edgeFound != null) { jsonArray.put(GraphSONUtility.jsonFromElement(edgeFound, returnKeys, mode)); } } } else if (type.equals("index")) { Index idx = ((IndexableGraph)graph).getIndex(key, Edge.class); for (int ix = 0; ix < values.length(); ix++) { CloseableIterable<Edge> edgesFound = idx.get(key, ElementHelper.getTypedPropertyValue(values.optString(ix))); for (Edge edge : edgesFound) { jsonArray.put(GraphSONUtility.jsonFromElement(edge, returnKeys, mode)); } edgesFound.close(); } } else if (type.equals("keyindex")) { for (int ix = 0; ix < values.length(); ix++) { Iterable<Edge> edgesFound = graph.getEdges(key, ElementHelper.getTypedPropertyValue(values.optString(ix))); for (Edge edge : edgesFound) { jsonArray.put(GraphSONUtility.jsonFromElement(edge, returnKeys, mode)); } } } final HashMap<String, Object> resultMap = new HashMap<String, Object>(); resultMap.put(Tokens.SUCCESS, true); resultMap.put(Tokens.RESULTS, jsonArray); final JSONObject resultObject = new JSONObject(resultMap); return ExtensionResponse.ok(resultObject); } catch (Exception mqe) { logger.error(mqe); return ExtensionResponse.error( "Error retrieving batch of edges [" + values + "]", generateErrorJson()); } } @ExtensionDefinition(extensionPoint = ExtensionPoint.GRAPH, method = HttpMethod.POST, path = "tx", autoCommitTransaction = true) @ExtensionDescriptor(description = "post a transaction to the graph.") public ExtensionResponse postTx(@RexsterContext RexsterResourceContext context, @RexsterContext Graph graph, @RexsterContext RexsterApplicationGraph rag) { final JSONObject transactionJson = context.getRequestObject(); if (transactionJson == null) { final ExtensionMethod extMethod = context.getExtensionMethod(); return ExtensionResponse.error( "no transaction JSON posted", null, Response.Status.BAD_REQUEST.getStatusCode(), null, generateErrorJson(extMethod.getExtensionApiAsJson())); } try { final JSONArray txArray = transactionJson.optJSONArray("tx"); String currentAction; for (int ix = 0; ix < txArray.length(); ix++) { final JSONObject txElement = txArray.optJSONObject(ix); currentAction = txElement.optString("_action"); if (currentAction.equals("create")) { create(txElement, graph); } else if (currentAction.equals("update")) { update(txElement, graph); } else if (currentAction.equals("delete")) { delete(txElement, graph); } } final Map<String, Object> resultMap = new HashMap<String, Object>(); resultMap.put(Tokens.SUCCESS, true); resultMap.put("txProcessed", txArray.length()); return ExtensionResponse.ok(new JSONObject(resultMap)); } catch (IllegalArgumentException iae) { logger.error(iae); final ExtensionMethod extMethod = context.getExtensionMethod(); return ExtensionResponse.error( iae.getMessage(), null, Response.Status.BAD_REQUEST.getStatusCode(), null, generateErrorJson(extMethod.getExtensionApiAsJson())); } catch (Exception ex) { logger.error(ex); return ExtensionResponse.error( "Error executing transaction: " + ex.getMessage(), generateErrorJson()); } } private void create(final JSONObject elementAsJson, final Graph graph) throws Exception { final String id = elementAsJson.optString(Tokens._ID); final String elementType = elementAsJson.optString(Tokens._TYPE); if (elementType == null) { throw new IllegalArgumentException("each element in the transaction must have an " + Tokens._TYPE + " key"); } if (!elementType.equals(Tokens.VERTEX) && !elementType.equals(Tokens.EDGE)) { throw new IllegalArgumentException("the " + Tokens._TYPE + " element in the transaction must be either " + Tokens.VERTEX + " or " + Tokens.EDGE); } Element graphElementCreated = null; if (elementType.equals(Tokens.VERTEX)) { graphElementCreated = graph.getVertex(id); if (graphElementCreated != null) { throw new Exception("Vertex with id " + id + " already exists."); } graphElementCreated = graph.addVertex(id); } else if (elementType.equals(Tokens.EDGE)) { String inV = null; Object temp = elementAsJson.opt(Tokens._IN_V); if (null != temp) inV = temp.toString(); String outV = null; temp = elementAsJson.opt(Tokens._OUT_V); if (null != temp) outV = temp.toString(); String label = null; temp = elementAsJson.opt(Tokens._LABEL); if (null != temp) label = temp.toString(); if (outV == null || inV == null || outV.isEmpty() || inV.isEmpty()) { throw new IllegalArgumentException("an edge must specify a " + Tokens._IN_V + " and " + Tokens._OUT_V); } graphElementCreated = graph.getEdge(id); if (graphElementCreated != null) { throw new Exception("Edge with id " + id + " already exists."); } // there is no edge but the in/out vertex params and label are present so // validate that the vertexes are present before creating the edge final Vertex out = graph.getVertex(outV); final Vertex in = graph.getVertex(inV); if (out != null && in != null) { // in/out vertexes are found so edge can be created graphElementCreated = graph.addEdge(id, out, in, label); } else { throw new Exception("the " + Tokens._IN_V + " or " + Tokens._OUT_V + " vertices could not be found."); } } if (graphElementCreated != null) { Iterator keys = elementAsJson.keys(); while (keys.hasNext()) { String key = keys.next().toString(); if (!key.startsWith(Tokens.UNDERSCORE)) { graphElementCreated.setProperty(key, ElementHelper.getTypedPropertyValue(elementAsJson.getString(key))); } } } } private void update(final JSONObject elementAsJson, final Graph graph) throws Exception { final String id = elementAsJson.optString(Tokens._ID); if (id == null || id.isEmpty()) { throw new IllegalArgumentException("each element in the transaction must have an " + Tokens._ID + " key"); } final String elementType = elementAsJson.optString(Tokens._TYPE); if (elementType == null) { throw new IllegalArgumentException("each element in the transaction must have an " + Tokens._TYPE + " key"); } if (!elementType.equals(Tokens.VERTEX) && !elementType.equals(Tokens.EDGE)) { throw new IllegalArgumentException("the " + Tokens._TYPE + " element in the transaction must be either " + Tokens.VERTEX + " or " + Tokens.EDGE); } Element graphElementUpdated = null; if (elementType.equals(Tokens.VERTEX)) { graphElementUpdated = graph.getVertex(id); } else if (elementType.equals(Tokens.EDGE)) { graphElementUpdated = graph.getEdge(id); } if (graphElementUpdated != null) { Iterator keys = elementAsJson.keys(); while (keys.hasNext()) { String key = keys.next().toString(); if (!key.startsWith(Tokens.UNDERSCORE)) { graphElementUpdated.setProperty(key, ElementHelper.getTypedPropertyValue(elementAsJson.getString(key))); } } } } private void delete(final JSONObject elementAsJson, final Graph graph) throws Exception { final String id = elementAsJson.optString(Tokens._ID); if (id == null || id.isEmpty()) { throw new IllegalArgumentException("each element in the transaction must have an " + Tokens._ID + " key"); } final String elementType = elementAsJson.optString(Tokens._TYPE); if (elementType == null) { throw new IllegalArgumentException("each element in the transaction must have an " + Tokens._TYPE + " key"); } if (!elementType.equals(Tokens.VERTEX) && !elementType.equals(Tokens.EDGE)) { throw new IllegalArgumentException("the " + Tokens._TYPE + " element in the transaction must be either " + Tokens.VERTEX + " or " + Tokens.EDGE); } final JSONArray keysToDelete = elementAsJson.optJSONArray("_keys"); Element graphElementDeleted = null; if (elementType.equals(Tokens.VERTEX)) { graphElementDeleted = graph.getVertex(id); } else if (elementType.equals(Tokens.EDGE)) { graphElementDeleted = graph.getEdge(id); } if (graphElementDeleted != null) { if (keysToDelete != null && keysToDelete.length() > 0) { // just delete keys from the element for (int ix = 0; ix < keysToDelete.length(); ix++) { graphElementDeleted.removeProperty(keysToDelete.optString(ix)); } } else { // delete the whole element if (elementType.equals(Tokens.VERTEX)) { graph.removeVertex((Vertex) graphElementDeleted); } else if (elementType.equals(Tokens.EDGE)) { graph.removeEdge((Edge) graphElementDeleted); } } } } private ExtensionResponse checkParameters(RexsterResourceContext context, JSONArray values, String type, String key) { final ExtensionMethod extMethod = context.getExtensionMethod(); String errorMessage = null; if (values == null || values.length() == 0) { errorMessage = "the values parameter cannot be empty"; } else if ((type.equals("index") || type.equals("keyindex")) && key.isEmpty()) { errorMessage = "the key parameter cannot be empty"; } return (errorMessage != null) ? ExtensionResponse.error( errorMessage, null, Response.Status.BAD_REQUEST.getStatusCode(), null, extMethod != null ? generateErrorJson(extMethod.getExtensionApiAsJson()) : null) : null; } }