blob: 26abe2e4e27e22c4c5c3345d53602edf04596d72 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.atlas.repository.graphdb.titan1.graphson;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.atlas.repository.graphdb.AtlasEdge;
import org.apache.atlas.repository.graphdb.AtlasElement;
import org.apache.atlas.repository.graphdb.AtlasVertex;
import org.apache.atlas.repository.graphdb.titan1.graphson.AtlasElementPropertyConfig.ElementPropertiesRule;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.codehaus.jettison.json.JSONTokener;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* This class was largely removed from tinkerpop 1. We're adding it back here to
* avoid changing the format of the JSON that we produce.
*
* Helps write individual graph elements to TinkerPop JSON format known as
* GraphSON.
*
* @author Stephen Mallette (http://stephen.genoprime.com)
*/
public final class AtlasGraphSONUtility {
private static final JsonNodeFactory JSON_NODE_FACTORY = JsonNodeFactory.instance;
private static final ObjectMapper MAPPER = new ObjectMapper();
private final AtlasGraphSONMode mode;
private final List<String> vertexPropertyKeys;
private final List<String> edgePropertyKeys;
private final ElementPropertiesRule vertexPropertiesRule;
private final ElementPropertiesRule edgePropertiesRule;
private final boolean normalized;
private final boolean includeReservedVertexId;
private final boolean includeReservedEdgeId;
private final boolean includeReservedVertexType;
private final boolean includeReservedEdgeType;
private final boolean includeReservedEdgeLabel;
private final boolean includeReservedEdgeOutV;
private final boolean includeReservedEdgeInV;
/**
* A GraphSONUtility that includes the specified properties.
*/
private AtlasGraphSONUtility(final AtlasGraphSONMode mode, final Set<String> vertexPropertyKeySet,
final Set<String> edgePropertyKeySet) {
AtlasElementPropertyConfig config = AtlasElementPropertyConfig.includeProperties(vertexPropertyKeySet,
edgePropertyKeySet);
this.vertexPropertyKeys = config.getVertexPropertyKeys();
this.edgePropertyKeys = config.getEdgePropertyKeys();
this.vertexPropertiesRule = config.getVertexPropertiesRule();
this.edgePropertiesRule = config.getEdgePropertiesRule();
this.normalized = config.isNormalized();
this.mode = mode;
this.includeReservedVertexId = includeReservedKey(mode, AtlasGraphSONTokens.INTERNAL_ID, vertexPropertyKeys,
this.vertexPropertiesRule);
this.includeReservedEdgeId = includeReservedKey(mode, AtlasGraphSONTokens.INTERNAL_ID, edgePropertyKeys,
this.edgePropertiesRule);
this.includeReservedVertexType = includeReservedKey(mode, AtlasGraphSONTokens.INTERNAL_TYPE, vertexPropertyKeys,
this.vertexPropertiesRule);
this.includeReservedEdgeType = includeReservedKey(mode, AtlasGraphSONTokens.INTERNAL_TYPE, edgePropertyKeys,
this.edgePropertiesRule);
this.includeReservedEdgeLabel = includeReservedKey(mode, AtlasGraphSONTokens.INTERNAL_LABEL, edgePropertyKeys,
this.edgePropertiesRule);
this.includeReservedEdgeOutV = includeReservedKey(mode, AtlasGraphSONTokens.INTERNAL_OUT_V, edgePropertyKeys,
this.edgePropertiesRule);
this.includeReservedEdgeInV = includeReservedKey(mode, AtlasGraphSONTokens.INTERNAL_IN_V, edgePropertyKeys,
this.edgePropertiesRule);
}
/*
* Creates GraphSON for a single graph element.
*/
private JSONObject jsonFromElement(final AtlasElement element) throws JSONException {
final ObjectNode objectNode = this.objectNodeFromElement(element);
try {
return new JSONObject(new JSONTokener(MAPPER.writeValueAsString(objectNode)));
} catch (IOException ioe) {
// repackage this as a JSONException...seems sensible as the caller will only know about
// the jettison object not being created
throw new JSONException(ioe);
}
}
/**
* Creates GraphSON for a single graph element.
*/
private ObjectNode objectNodeFromElement(final AtlasElement element) {
final boolean isEdge = element instanceof AtlasEdge;
final boolean showTypes = mode == AtlasGraphSONMode.EXTENDED;
final List<String> propertyKeys = isEdge ? this.edgePropertyKeys : this.vertexPropertyKeys;
final ElementPropertiesRule elementPropertyConfig = isEdge ? this.edgePropertiesRule
: this.vertexPropertiesRule;
final ObjectNode jsonElement = createJSONMap(
createPropertyMap(element, propertyKeys, elementPropertyConfig, normalized), propertyKeys, showTypes);
if ((isEdge && this.includeReservedEdgeId) || (!isEdge && this.includeReservedVertexId)) {
putObject(jsonElement, AtlasGraphSONTokens.INTERNAL_ID, element.getId());
}
// it's important to keep the order of these straight. check AtlasEdge first and then AtlasVertex because there
// are graph implementations that have AtlasEdge extend from AtlasVertex
if (element instanceof AtlasEdge) {
final AtlasEdge edge = (AtlasEdge) element;
if (this.includeReservedEdgeId) {
putObject(jsonElement, AtlasGraphSONTokens.INTERNAL_ID, element.getId());
}
if (this.includeReservedEdgeType) {
jsonElement.put(AtlasGraphSONTokens.INTERNAL_TYPE, AtlasGraphSONTokens.EDGE);
}
if (this.includeReservedEdgeOutV) {
putObject(jsonElement, AtlasGraphSONTokens.INTERNAL_OUT_V, edge.getOutVertex().getId());
}
if (this.includeReservedEdgeInV) {
putObject(jsonElement, AtlasGraphSONTokens.INTERNAL_IN_V, edge.getInVertex().getId());
}
if (this.includeReservedEdgeLabel) {
jsonElement.put(AtlasGraphSONTokens.INTERNAL_LABEL, edge.getLabel());
}
} else if (element instanceof AtlasVertex) {
if (this.includeReservedVertexId) {
putObject(jsonElement, AtlasGraphSONTokens.INTERNAL_ID, element.getId());
}
if (this.includeReservedVertexType) {
jsonElement.put(AtlasGraphSONTokens.INTERNAL_TYPE, AtlasGraphSONTokens.VERTEX);
}
}
return jsonElement;
}
/**
* Creates a Jettison JSONObject from a graph element.
*
* @param element
* the graph element to convert to JSON.
* @param propertyKeys
* The property getPropertyKeys() at the root of the element to
* serialize. If null, then all getPropertyKeys() are serialized.
* @param mode
* the type of GraphSON to be generated.
*/
public static JSONObject jsonFromElement(final AtlasElement element, final Set<String> propertyKeys,
final AtlasGraphSONMode mode)
throws JSONException {
final AtlasGraphSONUtility graphson = element instanceof AtlasEdge
? new AtlasGraphSONUtility(mode, null, propertyKeys)
: new AtlasGraphSONUtility(mode, propertyKeys, null);
return graphson.jsonFromElement(element);
}
private static ObjectNode objectNodeFromElement(final AtlasElement element, final List<String> propertyKeys,
final AtlasGraphSONMode mode) {
final AtlasGraphSONUtility graphson = element instanceof AtlasEdge
? new AtlasGraphSONUtility(mode, null, new HashSet<String>(propertyKeys))
: new AtlasGraphSONUtility(mode, new HashSet<String>(propertyKeys), null);
return graphson.objectNodeFromElement(element);
}
private static boolean includeReservedKey(final AtlasGraphSONMode mode, final String key,
final List<String> propertyKeys, final ElementPropertiesRule rule) {
// the key is always included in modes other than compact. if it is compact, then validate that the
// key is in the property key list
return mode != AtlasGraphSONMode.COMPACT || includeKey(key, propertyKeys, rule);
}
private static boolean includeKey(final String key, final List<String> propertyKeys,
final ElementPropertiesRule rule) {
if (propertyKeys == null) {
// when null always include the key and shortcut this piece
return true;
}
// default the key situation. if it's included then it should be explicitly defined in the
// property getPropertyKeys() list to be included or the reverse otherwise
boolean keySituation = rule == ElementPropertiesRule.INCLUDE;
switch (rule) {
case INCLUDE:
keySituation = propertyKeys.contains(key);
break;
case EXCLUDE:
keySituation = !propertyKeys.contains(key);
break;
default:
throw new RuntimeException("Unhandled rule: " + rule);
}
return keySituation;
}
private static ArrayNode createJSONList(final List<Object> list, final List<String> propertyKeys,
final boolean showTypes) {
final ArrayNode jsonList = JSON_NODE_FACTORY.arrayNode();
for (Object item : list) {
if (item instanceof AtlasElement) {
jsonList.add(objectNodeFromElement((AtlasElement) item, propertyKeys,
showTypes ? AtlasGraphSONMode.EXTENDED : AtlasGraphSONMode.NORMAL));
} else if (item instanceof List) {
jsonList.add(createJSONList((List<Object>) item, propertyKeys, showTypes));
} else if (item instanceof Map) {
jsonList.add(createJSONMap((Map<String, Object>) item, propertyKeys, showTypes));
} else if (item != null && item.getClass().isArray()) {
jsonList.add(createJSONList(convertArrayToList(item), propertyKeys, showTypes));
} else {
addObject(jsonList, item);
}
}
return jsonList;
}
private static ObjectNode createJSONMap(final Map<String, Object> map, final List<String> propertyKeys,
final boolean showTypes) {
final ObjectNode jsonMap = JSON_NODE_FACTORY.objectNode();
for (Object key : map.keySet()) {
Object value = map.get(key);
if (value != null) {
if (value instanceof List) {
value = createJSONList((List<Object>) value, propertyKeys, showTypes);
} else if (value instanceof Map) {
value = createJSONMap((Map<String, Object>) value, propertyKeys, showTypes);
} else if (value instanceof AtlasElement) {
value = objectNodeFromElement((AtlasElement) value, propertyKeys,
showTypes ? AtlasGraphSONMode.EXTENDED : AtlasGraphSONMode.NORMAL);
} else if (value.getClass().isArray()) {
value = createJSONList(convertArrayToList(value), propertyKeys, showTypes);
}
}
putObject(jsonMap, key.toString(), getValue(value, showTypes));
}
return jsonMap;
}
private static void addObject(final ArrayNode jsonList, final Object value) {
if (value == null) {
jsonList.add((JsonNode) null);
} else if (value.getClass() == Boolean.class) {
jsonList.add((Boolean) value);
} else if (value.getClass() == Long.class) {
jsonList.add((Long) value);
} else if (value.getClass() == Integer.class) {
jsonList.add((Integer) value);
} else if (value.getClass() == Float.class) {
jsonList.add((Float) value);
} else if (value.getClass() == Double.class) {
jsonList.add((Double) value);
} else if (value.getClass() == Byte.class) {
jsonList.add((Byte) value);
} else if (value.getClass() == Short.class) {
jsonList.add((Short) value);
} else if (value.getClass() == String.class) {
jsonList.add((String) value);
} else if (value instanceof ObjectNode) {
jsonList.add((ObjectNode) value);
} else if (value instanceof ArrayNode) {
jsonList.add((ArrayNode) value);
} else {
jsonList.add(value.toString());
}
}
private static void putObject(final ObjectNode jsonMap, final String key, final Object value) {
if (value == null) {
jsonMap.put(key, (JsonNode) null);
} else if (value.getClass() == Boolean.class) {
jsonMap.put(key, (Boolean) value);
} else if (value.getClass() == Long.class) {
jsonMap.put(key, (Long) value);
} else if (value.getClass() == Integer.class) {
jsonMap.put(key, (Integer) value);
} else if (value.getClass() == Float.class) {
jsonMap.put(key, (Float) value);
} else if (value.getClass() == Double.class) {
jsonMap.put(key, (Double) value);
} else if (value.getClass() == Short.class) {
jsonMap.put(key, (Short) value);
} else if (value.getClass() == Byte.class) {
jsonMap.put(key, (Byte) value);
} else if (value.getClass() == String.class) {
jsonMap.put(key, (String) value);
} else if (value instanceof ObjectNode) {
jsonMap.put(key, (ObjectNode) value);
} else if (value instanceof ArrayNode) {
jsonMap.put(key, (ArrayNode) value);
} else {
jsonMap.put(key, value.toString());
}
}
private static Map<String, Object> createPropertyMap(final AtlasElement element, final List<String> propertyKeys,
final ElementPropertiesRule rule, final boolean normalized) {
final Map<String, Object> map = new HashMap<String, Object>();
final List<String> propertyKeyList;
if (normalized) {
final List<String> sorted = new ArrayList<String>(element.getPropertyKeys());
Collections.sort(sorted);
propertyKeyList = sorted;
} else {
propertyKeyList = new ArrayList<String>(element.getPropertyKeys());
}
if (propertyKeys == null) {
for (String key : propertyKeyList) {
final Object valToPutInMap = element.getProperty(key, Object.class);
if (valToPutInMap != null) {
map.put(key, valToPutInMap);
}
}
} else {
if (rule == ElementPropertiesRule.INCLUDE) {
for (String key : propertyKeys) {
final Object valToPutInMap = element.getProperty(key, Object.class);
if (valToPutInMap != null) {
map.put(key, valToPutInMap);
}
}
} else {
for (String key : propertyKeyList) {
if (!propertyKeys.contains(key)) {
final Object valToPutInMap = element.getProperty(key, Object.class);
if (valToPutInMap != null) {
map.put(key, valToPutInMap);
}
}
}
}
}
return map;
}
private static Object getValue(Object value, final boolean includeType) {
Object returnValue = value;
// if the includeType is set to true then show the data types of the properties
if (includeType) {
// type will be one of: map, list, string, long, int, double, float.
// in the event of a complex object it will call a toString and store as a
// string
String type = determineType(value);
ObjectNode valueAndType = JSON_NODE_FACTORY.objectNode();
valueAndType.put(AtlasGraphSONTokens.TYPE, type);
if (type.equals(AtlasGraphSONTokens.TYPE_LIST)) {
// values of lists must be accumulated as ObjectNode objects under the value key.
// will return as a ArrayNode. called recursively to traverse the entire
// object graph of each item in the array.
ArrayNode list = (ArrayNode) value;
// there is a set of values that must be accumulated as an array under a key
ArrayNode valueArray = valueAndType.putArray(AtlasGraphSONTokens.VALUE);
for (int ix = 0; ix < list.size(); ix++) {
// the value of each item in the array is a node object from an ArrayNode...must
// get the value of it.
addObject(valueArray, getValue(getTypedValueFromJsonNode(list.get(ix)), includeType));
}
} else if (type.equals(AtlasGraphSONTokens.TYPE_MAP)) {
// maps are converted to a ObjectNode. called recursively to traverse
// the entire object graph within the map.
ObjectNode convertedMap = JSON_NODE_FACTORY.objectNode();
ObjectNode jsonObject = (ObjectNode) value;
Iterator<?> keyIterator = jsonObject.fieldNames();
while (keyIterator.hasNext()) {
Object key = keyIterator.next();
// no need to getValue() here as this is already a ObjectNode and should have type info
convertedMap.put(key.toString(), jsonObject.get(key.toString()));
}
valueAndType.put(AtlasGraphSONTokens.VALUE, convertedMap);
} else {
// this must be a primitive value or a complex object. if a complex
// object it will be handled by a call to toString and stored as a
// string value
putObject(valueAndType, AtlasGraphSONTokens.VALUE, value);
}
// this goes back as a JSONObject with data type and value
returnValue = valueAndType;
}
return returnValue;
}
private static Object getTypedValueFromJsonNode(JsonNode node) {
Object theValue = null;
if (node != null && !node.isNull()) {
if (node.isBoolean()) {
theValue = node.booleanValue();
} else if (node.isDouble()) {
theValue = node.doubleValue();
} else if (node.isFloatingPointNumber()) {
theValue = node.floatValue();
} else if (node.isInt()) {
theValue = node.intValue();
} else if (node.isLong()) {
theValue = node.longValue();
} else if (node.isTextual()) {
theValue = node.textValue();
} else if (node.isArray()) {
// this is an array so just send it back so that it can be
// reprocessed to its primitive components
theValue = node;
} else if (node.isObject()) {
// this is an object so just send it back so that it can be
// reprocessed to its primitive components
theValue = node;
} else {
theValue = node.textValue();
}
}
return theValue;
}
private static List<Object> convertArrayToList(final Object value) {
final ArrayList<Object> list = new ArrayList<Object>();
int arrlength = Array.getLength(value);
for (int i = 0; i < arrlength; i++) {
Object object = Array.get(value, i);
list.add(object);
}
return list;
}
private static String determineType(final Object value) {
String type = AtlasGraphSONTokens.TYPE_STRING;
if (value == null) {
type = AtlasGraphSONTokens.TYPE_UNKNOWN;
} else if (value.getClass() == Double.class) {
type = AtlasGraphSONTokens.TYPE_DOUBLE;
} else if (value.getClass() == Float.class) {
type = AtlasGraphSONTokens.TYPE_FLOAT;
} else if (value.getClass() == Byte.class) {
type = AtlasGraphSONTokens.TYPE_BYTE;
} else if (value.getClass() == Short.class) {
type = AtlasGraphSONTokens.TYPE_SHORT;
} else if (value.getClass() == Integer.class) {
type = AtlasGraphSONTokens.TYPE_INTEGER;
} else if (value.getClass() == Long.class) {
type = AtlasGraphSONTokens.TYPE_LONG;
} else if (value.getClass() == Boolean.class) {
type = AtlasGraphSONTokens.TYPE_BOOLEAN;
} else if (value instanceof ArrayNode) {
type = AtlasGraphSONTokens.TYPE_LIST;
} else if (value instanceof ObjectNode) {
type = AtlasGraphSONTokens.TYPE_MAP;
}
return type;
}
static class ElementFactory {
}
}