| /** |
| * 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 { |
| |
| } |
| } |