package com.orientechnologies.orient.graph.graphml;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MappingJsonFactory;
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;
import com.tinkerpop.blueprints.Direction;
import com.tinkerpop.blueprints.Edge;
import com.tinkerpop.blueprints.Element;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.blueprints.util.io.graphson.ElementFactory;
import com.tinkerpop.blueprints.util.io.graphson.ElementPropertyConfig;
import com.tinkerpop.blueprints.util.io.graphson.GraphSONMode;
import com.tinkerpop.blueprints.util.io.graphson.GraphSONTokens;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.codehaus.jettison.json.JSONTokener;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.util.*;

import static com.tinkerpop.blueprints.util.io.graphson.ElementPropertyConfig.ElementPropertiesRule;

/**
 * Helps write individual graph elements to TinkerPop JSON format known as GraphSON.
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class OGraphSONUtility {

  private static final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance;
  private static final JsonFactory     jsonFactory     = new MappingJsonFactory();
  private static final String          EMPTY_STRING    = "";

  private static final ObjectMapper    mapper          = new ObjectMapper();

  private final GraphSONMode           mode;
  private final List<String>           vertexPropertyKeys;
  private final List<String>           edgePropertyKeys;
  private final ElementFactory         factory;
  private final boolean                hasEmbeddedTypes;
  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 GraphSONUtiltiy that includes all properties of vertices and edges.
   */
  public OGraphSONUtility(final GraphSONMode mode, final ElementFactory factory) {
    this(mode, factory, ElementPropertyConfig.AllProperties);
  }

  /**
   * A GraphSONUtility that includes the specified properties.
   */
  public OGraphSONUtility(final GraphSONMode mode, final ElementFactory factory, final Set<String> vertexPropertyKeys,
      final Set<String> edgePropertyKeys) {
    this(mode, factory, ElementPropertyConfig.includeProperties(vertexPropertyKeys, edgePropertyKeys));
  }

  public OGraphSONUtility(final GraphSONMode mode, final ElementFactory factory, final ElementPropertyConfig config) {
    this.vertexPropertyKeys = config.getVertexPropertyKeys();
    this.edgePropertyKeys = config.getEdgePropertyKeys();
    this.vertexPropertiesRule = config.getVertexPropertiesRule();
    this.edgePropertiesRule = config.getEdgePropertiesRule();
    this.normalized = config.isNormalized();

    this.mode = mode;
    this.factory = factory;
    this.hasEmbeddedTypes = mode == GraphSONMode.EXTENDED;

    this.includeReservedVertexId = includeReservedKey(mode, GraphSONTokens._ID, vertexPropertyKeys, this.vertexPropertiesRule);
    this.includeReservedEdgeId = includeReservedKey(mode, GraphSONTokens._ID, edgePropertyKeys, this.edgePropertiesRule);
    this.includeReservedVertexType = includeReservedKey(mode, GraphSONTokens._TYPE, vertexPropertyKeys, this.vertexPropertiesRule);
    this.includeReservedEdgeType = includeReservedKey(mode, GraphSONTokens._TYPE, edgePropertyKeys, this.edgePropertiesRule);
    this.includeReservedEdgeLabel = includeReservedKey(mode, GraphSONTokens._LABEL, edgePropertyKeys, this.edgePropertiesRule);
    this.includeReservedEdgeOutV = includeReservedKey(mode, GraphSONTokens._OUT_V, edgePropertyKeys, this.edgePropertiesRule);
    this.includeReservedEdgeInV = includeReservedKey(mode, GraphSONTokens._IN_V, edgePropertyKeys, this.edgePropertiesRule);
  }

  /**
   * Creates a vertex from GraphSON using settings supplied in the constructor.
   */
  public Vertex vertexFromJson(final JSONObject json) throws IOException {
    return this.vertexFromJson(json.toString());
  }

  /**
   * Creates a vertex from GraphSON using settings supplied in the constructor.
   */
  public Vertex vertexFromJson(final String json) throws IOException {
    final JsonParser jp = jsonFactory.createParser(json);
    final JsonNode node = jp.readValueAsTree();
    return this.vertexFromJson(node);
  }

  /**
   * Creates a vertex from GraphSON using settings supplied in the constructor.
   */
  public Vertex vertexFromJson(final InputStream json) throws IOException {
    final JsonParser jp = jsonFactory.createParser(json);
    final JsonNode node = jp.readValueAsTree();
    return this.vertexFromJson(node);
  }

  /**
   * Creates a vertex from GraphSON using settings supplied in the constructor.
   */
  public Vertex vertexFromJson(final JsonNode json) throws IOException {
    final Map<String, Object> props = readProperties(json, true, this.hasEmbeddedTypes);

    final Object vertexId = getTypedValueFromJsonNode(json.get(GraphSONTokens._ID));
    final Vertex v = factory.createVertex(vertexId);

    for (Map.Entry<String, Object> entry : props.entrySet()) {
      // if (this.vertexPropertyKeys == null || vertexPropertyKeys.contains(entry.getKey())) {
      if (includeKey(entry.getKey(), vertexPropertyKeys, this.vertexPropertiesRule)) {
        v.setProperty(entry.getKey(), entry.getValue());
      }
    }

    return v;
  }

  /**
   * Creates an edge from GraphSON using settings supplied in the constructor.
   */
  public Edge edgeFromJson(final JSONObject json, final Vertex out, final Vertex in) throws IOException {
    return this.edgeFromJson(json.toString(), out, in);
  }

  /**
   * Creates an edge from GraphSON using settings supplied in the constructor.
   */
  public Edge edgeFromJson(final String json, final Vertex out, final Vertex in) throws IOException {
    final JsonParser jp = jsonFactory.createParser(json);
    final JsonNode node = jp.readValueAsTree();
    return this.edgeFromJson(node, out, in);
  }

  /**
   * Creates an edge from GraphSON using settings supplied in the constructor.
   */
  public Edge edgeFromJson(final InputStream json, final Vertex out, final Vertex in) throws IOException {
    final JsonParser jp = jsonFactory.createParser(json);
    final JsonNode node = jp.readValueAsTree();
    return this.edgeFromJson(node, out, in);
  }

  /**
   * Creates an edge from GraphSON using settings supplied in the constructor.
   */
  public Edge edgeFromJson(final JsonNode json, final Vertex out, final Vertex in) throws IOException {
    final Map<String, Object> props = OGraphSONUtility.readProperties(json, true, this.hasEmbeddedTypes);

    final Object edgeId = getTypedValueFromJsonNode(json.get(GraphSONTokens._ID));
    final JsonNode nodeLabel = json.get(GraphSONTokens._LABEL);

    // assigned an empty string edge label in cases where one does not exist. this gets around the requirement
    // that blueprints graphs have a non-null label while ensuring that GraphSON can stay flexible in parsing
    // partial bits from the JSON. Not sure if there is any gotchas developing out of this.
    final String label = nodeLabel == null ? EMPTY_STRING : nodeLabel.textValue();

    final Edge e = factory.createEdge(edgeId, out, in, label);

    for (Map.Entry<String, Object> entry : props.entrySet()) {
      // if (this.edgePropertyKeys == null || this.edgePropertyKeys.contains(entry.getKey())) {
      if (includeKey(entry.getKey(), edgePropertyKeys, this.edgePropertiesRule)) {
        e.setProperty(entry.getKey(), entry.getValue());
      }
    }

    return e;
  }

  /**
   * Creates GraphSON for a single graph element.
   */
  public JSONObject jsonFromElement(final Element 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.
   */
  public ObjectNode objectNodeFromElement(final Element element) {
    final boolean isEdge = element instanceof Edge;
    final boolean showTypes = mode == GraphSONMode.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, GraphSONTokens._ID, element.getId());
    }

    // it's important to keep the order of these straight. check Edge first and then Vertex because there
    // are graph implementations that have Edge extend from Vertex
    if (element instanceof Edge) {
      final Edge edge = (Edge) element;

      if (this.includeReservedEdgeId) {
        putObject(jsonElement, GraphSONTokens._ID, element.getId());
      }

      if (this.includeReservedEdgeType) {
        jsonElement.put(GraphSONTokens._TYPE, GraphSONTokens.EDGE);
      }

      if (this.includeReservedEdgeOutV) {
        putObject(jsonElement, GraphSONTokens._OUT_V, edge.getVertex(Direction.OUT).getId());
      }

      if (this.includeReservedEdgeInV) {
        putObject(jsonElement, GraphSONTokens._IN_V, edge.getVertex(Direction.IN).getId());
      }

      if (this.includeReservedEdgeLabel) {
        jsonElement.put(GraphSONTokens._LABEL, edge.getLabel());
      }
    } else if (element instanceof Vertex) {
      if (this.includeReservedVertexId) {
        putObject(jsonElement, GraphSONTokens._ID, element.getId());
      }

      if (this.includeReservedVertexType) {
        jsonElement.put(GraphSONTokens._TYPE, GraphSONTokens.VERTEX);
      }
    }

    return jsonElement;
  }

  /**
   * Reads an individual Vertex from JSON. The vertex must match the accepted GraphSON format.
   *
   * @param json
   *          a single vertex in GraphSON format as Jettison JSONObject
   * @param factory
   *          the factory responsible for constructing graph elements
   * @param mode
   *          the mode of the GraphSON
   * @param propertyKeys
   *          a list of keys to include on reading of element properties
   */
  public static Vertex vertexFromJson(final JSONObject json, final ElementFactory factory, final GraphSONMode mode,
      final Set<String> propertyKeys) throws IOException {
    final OGraphSONUtility graphson = new OGraphSONUtility(mode, factory, propertyKeys, null);
    return graphson.vertexFromJson(json);
  }

  /**
   * Reads an individual Vertex from JSON. The vertex must match the accepted GraphSON format.
   *
   * @param json
   *          a single vertex in GraphSON format as a String.
   * @param factory
   *          the factory responsible for constructing graph elements
   * @param mode
   *          the mode of the GraphSON
   * @param propertyKeys
   *          a list of keys to include on reading of element properties
   */
  public static Vertex vertexFromJson(final String json, final ElementFactory factory, final GraphSONMode mode,
      final Set<String> propertyKeys) throws IOException {
    final OGraphSONUtility graphson = new OGraphSONUtility(mode, factory, propertyKeys, null);
    return graphson.vertexFromJson(json);
  }

  /**
   * Reads an individual Vertex from JSON. The vertex must match the accepted GraphSON format.
   *
   * @param json
   *          a single vertex in GraphSON format as an InputStream.
   * @param factory
   *          the factory responsible for constructing graph elements
   * @param mode
   *          the mode of the GraphSON
   * @param propertyKeys
   *          a list of keys to include on reading of element properties
   */
  public static Vertex vertexFromJson(final InputStream json, final ElementFactory factory, final GraphSONMode mode,
      final Set<String> propertyKeys) throws IOException {
    final OGraphSONUtility graphson = new OGraphSONUtility(mode, factory, propertyKeys, null);
    return graphson.vertexFromJson(json);
  }

  /**
   * Reads an individual Vertex from JSON. The vertex must match the accepted GraphSON format.
   *
   * @param json
   *          a single vertex in GraphSON format as Jackson JsonNode
   * @param factory
   *          the factory responsible for constructing graph elements
   * @param mode
   *          the mode of the GraphSON
   * @param propertyKeys
   *          a list of keys to include on reading of element properties
   */
  public static Vertex vertexFromJson(final JsonNode json, final ElementFactory factory, final GraphSONMode mode,
      final Set<String> propertyKeys) throws IOException {
    final OGraphSONUtility graphson = new OGraphSONUtility(mode, factory, propertyKeys, null);
    return graphson.vertexFromJson(json);
  }

  /**
   * Reads an individual Edge from JSON. The edge must match the accepted GraphSON format.
   *
   * @param json
   *          a single edge in GraphSON format as a Jettison JSONObject
   * @param factory
   *          the factory responsible for constructing graph elements
   * @param mode
   *          the mode of the GraphSON
   * @param propertyKeys
   *          a list of keys to include when reading of element properties
   */
  public static Edge edgeFromJson(final JSONObject json, final Vertex out, final Vertex in, final ElementFactory factory,
      final GraphSONMode mode, final Set<String> propertyKeys) throws IOException {
    final OGraphSONUtility graphson = new OGraphSONUtility(mode, factory, null, propertyKeys);
    return graphson.edgeFromJson(json, out, in);
  }

  /**
   * Reads an individual Edge from JSON. The edge must match the accepted GraphSON format.
   *
   * @param json
   *          a single edge in GraphSON format as a String
   * @param factory
   *          the factory responsible for constructing graph elements
   * @param mode
   *          the mode of the GraphSON
   * @param propertyKeys
   *          a list of keys to include when reading of element properties
   */
  public static Edge edgeFromJson(final String json, final Vertex out, final Vertex in, final ElementFactory factory,
      final GraphSONMode mode, final Set<String> propertyKeys) throws IOException {
    final OGraphSONUtility graphson = new OGraphSONUtility(mode, factory, null, propertyKeys);
    return graphson.edgeFromJson(json, out, in);
  }

  /**
   * Reads an individual Edge from JSON. The edge must match the accepted GraphSON format.
   *
   * @param json
   *          a single edge in GraphSON format as an InputStream
   * @param factory
   *          the factory responsible for constructing graph elements
   * @param mode
   *          the mode of the GraphSON
   * @param propertyKeys
   *          a list of keys to include when reading of element properties
   */
  public static Edge edgeFromJson(final InputStream json, final Vertex out, final Vertex in, final ElementFactory factory,
      final GraphSONMode mode, final Set<String> propertyKeys) throws IOException {
    final OGraphSONUtility graphson = new OGraphSONUtility(mode, factory, null, propertyKeys);
    return graphson.edgeFromJson(json, out, in);
  }

  /**
   * Reads an individual Edge from JSON. The edge must match the accepted GraphSON format.
   *
   * @param json
   *          a single edge in GraphSON format as a Jackson JsonNode
   * @param factory
   *          the factory responsible for constructing graph elements
   * @param mode
   *          the mode of the GraphSON
   * @param propertyKeys
   *          a list of keys to include when reading of element properties
   */
  public static Edge edgeFromJson(final JsonNode json, final Vertex out, final Vertex in, final ElementFactory factory,
      final GraphSONMode mode, final Set<String> propertyKeys) throws IOException {
    final OGraphSONUtility graphson = new OGraphSONUtility(mode, factory, null, propertyKeys);
    return graphson.edgeFromJson(json, out, in);
  }

  /**
   * Creates a Jettison JSONObject from a graph element.
   *
   * @param element
   *          the graph element to convert to JSON.
   * @param propertyKeys
   *          The property keys at the root of the element to serialize. If null, then all keys are serialized.
   * @param mode
   *          the type of GraphSON to be generated.
   */
  public static JSONObject jsonFromElement(final Element element, final Set<String> propertyKeys, final GraphSONMode mode)
      throws JSONException {
    final OGraphSONUtility graphson = element instanceof Edge ? new OGraphSONUtility(mode, null, null, propertyKeys)
        : new OGraphSONUtility(mode, null, propertyKeys, null);
    return graphson.jsonFromElement(element);
  }

  /**
   * Creates a Jackson ObjectNode from a graph element.
   *
   * @param element
   *          the graph element to convert to JSON.
   * @param propertyKeys
   *          The property keys at the root of the element to serialize. If null, then all keys are serialized.
   * @param mode
   *          The type of GraphSON to generate.
   */
  public static ObjectNode objectNodeFromElement(final Element element, final Set<String> propertyKeys, final GraphSONMode mode) {
    final OGraphSONUtility graphson = element instanceof Edge ? new OGraphSONUtility(mode, null, null, propertyKeys)
        : new OGraphSONUtility(mode, null, propertyKeys, null);
    return graphson.objectNodeFromElement(element);
  }

  private static ObjectNode objectNodeFromElement(final Element element, final List<String> propertyKeys, final GraphSONMode mode) {
    final OGraphSONUtility graphson = element instanceof Edge
        ? new OGraphSONUtility(mode, null, null, new HashSet<String>(propertyKeys))
        : new OGraphSONUtility(mode, null, new HashSet<String>(propertyKeys), null);
    return graphson.objectNodeFromElement(element);
  }

  static Map<String, Object> readProperties(final JsonNode node, final boolean ignoreReservedKeys, final boolean hasEmbeddedTypes) {
    final Map<String, Object> map = new HashMap<String, Object>();

    final Iterator<Map.Entry<String, JsonNode>> iterator = node.fields();
    while (iterator.hasNext()) {
      final Map.Entry<String, JsonNode> entry = iterator.next();

      if (!ignoreReservedKeys || !isReservedKey(entry.getKey())) {
        // it generally shouldn't be as such but graphson containing null values can't be shoved into
        // element property keys or it will result in error
        final Object o = readProperty(entry.getValue(), hasEmbeddedTypes);
        if (o != null) {
          map.put(entry.getKey(), o);
        }
      }
    }

    return map;
  }

  private static boolean includeReservedKey(final GraphSONMode 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 != GraphSONMode.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 keys 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;
    }

    return keySituation;
  }

  private static boolean isReservedKey(final String key) {
    return key.equals(GraphSONTokens._ID) || key.equals(GraphSONTokens._TYPE) || key.equals(GraphSONTokens._LABEL)
        || key.equals(GraphSONTokens._OUT_V) || key.equals(GraphSONTokens._IN_V);
  }

  private static Object readProperty(final JsonNode node, final boolean hasEmbeddedTypes) {
    final Object propertyValue;

    if (hasEmbeddedTypes) {
      if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_UNKNOWN)) {
        propertyValue = null;
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_BOOLEAN)) {
        propertyValue = node.get(GraphSONTokens.VALUE).booleanValue();
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_FLOAT)) {
        propertyValue = Float.parseFloat(node.get(GraphSONTokens.VALUE).asText());
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_BYTE)) {
        propertyValue = Byte.parseByte(node.get(GraphSONTokens.VALUE).asText());
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_SHORT)) {
        propertyValue = Short.parseShort(node.get(GraphSONTokens.VALUE).asText());
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_DOUBLE)) {
        propertyValue = node.get(GraphSONTokens.VALUE).doubleValue();
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_INTEGER)) {
        propertyValue = node.get(GraphSONTokens.VALUE).intValue();
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_LONG)) {
        propertyValue = node.get(GraphSONTokens.VALUE).longValue();
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_STRING)) {
        propertyValue = node.get(GraphSONTokens.VALUE).textValue();
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_LIST)) {
        propertyValue = readProperties(node.get(GraphSONTokens.VALUE).elements(), hasEmbeddedTypes);
      } else if (node.get(GraphSONTokens.TYPE).textValue().equals(GraphSONTokens.TYPE_MAP)) {
        propertyValue = readProperties(node.get(GraphSONTokens.VALUE), false, hasEmbeddedTypes);
      } else {
        propertyValue = node.textValue();
      }
    } else {
      if (node.isNull()) {
        propertyValue = null;
      } else if (node.isBoolean()) {
        propertyValue = node.booleanValue();
      } else if (node.isDouble()) {
        propertyValue = node.doubleValue();
      } else if (node.isInt()) {
        propertyValue = node.intValue();
      } else if (node.isLong()) {
        propertyValue = node.longValue();
      } else if (node.isTextual()) {
        propertyValue = node.textValue();
      } else if (node.isArray()) {
        propertyValue = readProperties(node.elements(), hasEmbeddedTypes);
      } else if (node.isObject()) {
        propertyValue = readProperties(node, false, hasEmbeddedTypes);
      } else {
        propertyValue = node.textValue();
      }
    }

    return propertyValue;
  }

  private static List readProperties(final Iterator<JsonNode> listOfNodes, final boolean hasEmbeddedTypes) {
    final List array = new ArrayList();

    while (listOfNodes.hasNext()) {
      array.add(readProperty(listOfNodes.next(), hasEmbeddedTypes));
    }

    return array;
  }

  private static ArrayNode createJSONList(final List list, final List<String> propertyKeys, final boolean showTypes) {
    final ArrayNode jsonList = jsonNodeFactory.arrayNode();
    for (Object item : list) {
      if (item instanceof Element) {
        jsonList.add(objectNodeFromElement((Element) item, propertyKeys, showTypes ? GraphSONMode.EXTENDED : GraphSONMode.NORMAL));
      } else if (item instanceof List) {
        jsonList.add(createJSONList((List) item, propertyKeys, showTypes));
      } else if (item instanceof Map) {
        jsonList.add(createJSONMap((Map) 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 map, final List<String> propertyKeys, final boolean showTypes) {
    final ObjectNode jsonMap = jsonNodeFactory.objectNode();
    for (Object key : map.keySet()) {
      Object value = map.get(key);
      if (value != null) {
        if (value instanceof List) {
          value = createJSONList((List) value, propertyKeys, showTypes);
        } else if (value instanceof Map) {
          value = createJSONMap((Map) value, propertyKeys, showTypes);
        } else if (value instanceof Element) {
          value = objectNodeFromElement((Element) value, propertyKeys, showTypes ? GraphSONMode.EXTENDED : GraphSONMode.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 createPropertyMap(final Element element, final List<String> propertyKeys, final ElementPropertiesRule rule,
      final boolean normalized) {
    final Map 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);
        if (valToPutInMap != null) {
          map.put(key, valToPutInMap);
        }
      }
    } else {
      if (rule == ElementPropertiesRule.INCLUDE) {
        for (String key : propertyKeys) {
          final Object valToPutInMap = element.getProperty(key);
          if (valToPutInMap != null) {
            map.put(key, valToPutInMap);
          }
        }
      } else {
        for (String key : propertyKeyList) {
          if (!propertyKeys.contains(key)) {
            final Object valToPutInMap = element.getProperty(key);
            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 = jsonNodeFactory.objectNode();
      valueAndType.put(GraphSONTokens.TYPE, type);

      if (type.equals(GraphSONTokens.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(GraphSONTokens.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(GraphSONTokens.TYPE_MAP)) {

        // maps are converted to a ObjectNode. called recursively to traverse
        // the entire object graph within the map.
        ObjectNode convertedMap = jsonNodeFactory.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(GraphSONTokens.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, GraphSONTokens.VALUE, value);
      }

      // this goes back as a JSONObject with data type and value
      returnValue = valueAndType;
    }

    return returnValue;
  }

  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 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 = GraphSONTokens.TYPE_STRING;
    if (value == null) {
      type = GraphSONTokens.TYPE_UNKNOWN;
    } else if (value.getClass() == Double.class) {
      type = GraphSONTokens.TYPE_DOUBLE;
    } else if (value.getClass() == Float.class) {
      type = GraphSONTokens.TYPE_FLOAT;
    } else if (value.getClass() == Byte.class) {
      type = GraphSONTokens.TYPE_BYTE;
    } else if (value.getClass() == Short.class) {
      type = GraphSONTokens.TYPE_SHORT;
    } else if (value.getClass() == Integer.class) {
      type = GraphSONTokens.TYPE_INTEGER;
    } else if (value.getClass() == Long.class) {
      type = GraphSONTokens.TYPE_LONG;
    } else if (value.getClass() == Boolean.class) {
      type = GraphSONTokens.TYPE_BOOLEAN;
    } else if (value instanceof ArrayNode) {
      type = GraphSONTokens.TYPE_LIST;
    } else if (value instanceof ObjectNode) {
      type = GraphSONTokens.TYPE_MAP;
    }

    return type;
  }
}


package com.orientechnologies.orient.etl.http;

import com.orientechnologies.common.thread.OThreadPoolExecutorWithLogging;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.etl.util.OMigrationConfigManager;
import com.orientechnologies.orient.server.OServer;

import java.io.File;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * Created by gabriele on 27/02/17.
 */
public class OETLHandler {

  private ExecutorService pool       = new OThreadPoolExecutorWithLogging(1, 1, 0L, TimeUnit.MILLISECONDS,
      new LinkedBlockingQueue<>());
  private OETLJob         currentJob = null;

  public OETLHandler() {
  }

  /**
   * Executes import with configuration;
   */
  public void executeImport(ODocument cfg, OServer server) {

    OETLJob job = new OETLJob(cfg, server, new OETLListener() {
      @Override
      public void onEnd(OETLJob etlJob) {
        currentJob = null;
      }
    });

    job.validate();

    currentJob = job;
    pool.execute(job);

  }

  /**
   * Checks If the connection with given parameters is alive
   */
  public void checkConnection(ODocument args) throws Exception {

  }

  /**
   * Status of the Running Jobs
   *
   * @return ODocument
   */
  public ODocument status() {

    ODocument status = new ODocument();

    Collection<ODocument> jobs = new ArrayList<ODocument>();
    if (currentJob != null) {
      jobs.add(currentJob.status());
    }
    status.field("jobs", jobs);
    return status;
  }

  public void saveConfiguration(ODocument args, OServer server) throws Exception {

    final String outDBName = args.field("outDBName");
    final ODocument migrationConfig = new ODocument().fromJSON((String) args.field("migrationConfig"), "noMap");
    final String configName = args.field("configName");
    final String protocol = args.field("protocol");

    if (outDBName == null) {
      throw new IllegalArgumentException("target database name is null.");
    }

    // fetching the server orientdb home and updating coherently the target database URL if we have an orientdb loader
    String outOrientGraphUri = server.getDatabaseDirectory() + outDBName;
    String dbURL = protocol + ":" + outOrientGraphUri;
    ODocument loader = migrationConfig.field("loader");
    if (loader.field("orientdb") != null) {
      ((ODocument) loader.field("orientdb")).field("dbURL", dbURL);
    }

    if (migrationConfig == null) {
      throw new IllegalArgumentException("Migration config is null.");
    }

    OMigrationConfigManager.writeConfigurationInTargetDB(migrationConfig, outOrientGraphUri, configName);
  }

  public ODocument listConfigurations(OServer server) throws Exception {

    ODocument database2Configs = new ODocument();

    File serverDBHome = new File(server.getDatabaseDirectory());
    File[] dbDirectories = serverDBHome.listFiles();

    for (int i = 0; i < dbDirectories.length; i++) {

      String dbName = dbDirectories[i].getName();
      File currentETLConfigDir = new File(dbDirectories[i].getAbsolutePath() + "/etl-config/");

      if (currentETLConfigDir.exists()) {
        List<ODocument> configs = new LinkedList<ODocument>();
        File[] currentDBConfigs = currentETLConfigDir.listFiles();

        for (int k = 0; k < currentDBConfigs.length; k++) {

          String currentConfigName = currentDBConfigs[k].getName();

          if (currentConfigName.endsWith(".json")) {   // checking if the current file is a json configuration
            ODocument currentConfig = new ODocument();
            currentConfig.field("configName", currentConfigName);

            // reading the config as a text
            Scanner scanner = null;
            String configText = null;
            try {
              scanner = new Scanner(currentDBConfigs[k]);
              configText = scanner.useDelimiter("\\A").next();
            } catch (Exception e) {

            } finally {
              scanner.close();
            }

            currentConfig.field("config", configText);
            configs.add(currentConfig);
          }
        }
        database2Configs.field(dbName, configs);
      }
    }
    return database2Configs;
  }
}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */

package com.orientechnologies.orient.core.serialization.serializer.binary.impl;

import com.orientechnologies.common.serialization.types.OBinarySerializer;
import com.orientechnologies.common.serialization.types.OLongSerializer;
import com.orientechnologies.common.serialization.types.OShortSerializer;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.storage.impl.local.paginated.wal.OWALChanges;

import java.nio.ByteBuffer;

import static com.orientechnologies.orient.core.serialization.OBinaryProtocol.bytes2long;
import static com.orientechnologies.orient.core.serialization.OBinaryProtocol.bytes2short;
import static com.orientechnologies.orient.core.serialization.OBinaryProtocol.long2bytes;
import static com.orientechnologies.orient.core.serialization.OBinaryProtocol.short2bytes;

/**
 * Serializer for {@link com.orientechnologies.orient.core.metadata.schema.OType#LINK}
 *
 * @author Ilya Bershadskiy (ibersh20-at-gmail.com)
 * @since 07.02.12
 */
public class OLinkSerializer implements OBinarySerializer<OIdentifiable> {
  public static final  byte            ID               = 9;
  private static final int             CLUSTER_POS_SIZE = OLongSerializer.LONG_SIZE;
  public static final  int             RID_SIZE         = OShortSerializer.SHORT_SIZE + CLUSTER_POS_SIZE;
  public static final  OLinkSerializer INSTANCE         = new OLinkSerializer();

  public int getObjectSize(final OIdentifiable rid, Object... hints) {
    return RID_SIZE;
  }

  public void serialize(final OIdentifiable rid, final byte[] stream, final int startPosition, Object... hints) {
    final ORID r = rid.getIdentity();
    short2bytes((short) r.getClusterId(), stream, startPosition);
    long2bytes(r.getClusterPosition(), stream, startPosition + OShortSerializer.SHORT_SIZE);
  }

  public ORecordId deserialize(final byte[] stream, final int startPosition) {
    return new ORecordId(bytes2short(stream, startPosition), bytes2long(stream, startPosition + OShortSerializer.SHORT_SIZE));
  }

  public int getObjectSize(final byte[] stream, final int startPosition) {
    return RID_SIZE;
  }

  public byte getId() {
    return ID;
  }

  public int getObjectSizeNative(byte[] stream, int startPosition) {
    return RID_SIZE;
  }

  public void serializeNativeObject(OIdentifiable rid, byte[] stream, int startPosition, Object... hints) {
    final ORID r = rid.getIdentity();

    OShortSerializer.INSTANCE.serializeNative((short) r.getClusterId(), stream, startPosition);
    // Wrong implementation but needed for binary compatibility should be used serializeNative
    OLongSerializer.INSTANCE.serialize(r.getClusterPosition(), stream, startPosition + OShortSerializer.SHORT_SIZE);
  }

  public ORecordId deserializeNativeObject(byte[] stream, int startPosition) {
    final int clusterId = OShortSerializer.INSTANCE.deserializeNative(stream, startPosition);
    // Wrong implementation but needed for binary compatibility should be used deserializeNative
    final long clusterPosition = OLongSerializer.INSTANCE.deserialize(stream, startPosition + OShortSerializer.SHORT_SIZE);
    return new ORecordId(clusterId, clusterPosition);
  }

  public boolean isFixedLength() {
    return true;
  }

  public int getFixedLength() {
    return RID_SIZE;
  }

  @Override
  public OIdentifiable preprocess(OIdentifiable value, Object... hints) {
    if (value == null)
      return null;
    else
      return value.getIdentity();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void serializeInByteBufferObject(OIdentifiable object, ByteBuffer buffer, Object... hints) {
    final ORID r = object.getIdentity();

    buffer.putShort((short) r.getClusterId());
    // Wrong implementation but needed for binary compatibility
    byte[] stream = new byte[OLongSerializer.LONG_SIZE];
    OLongSerializer.INSTANCE.serialize(r.getClusterPosition(), stream, 0);
    buffer.put(stream);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public OIdentifiable deserializeFromByteBufferObject(ByteBuffer buffer) {
    final int clusterId = buffer.getShort();

    final byte[] stream = new byte[OLongSerializer.LONG_SIZE];
    buffer.get(stream);
    // Wrong implementation but needed for binary compatibility
    final long clusterPosition = OLongSerializer.INSTANCE.deserialize(stream, 0);

    return new ORecordId(clusterId, clusterPosition);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public int getObjectSizeInByteBuffer(ByteBuffer buffer) {
    return RID_SIZE;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public OIdentifiable deserializeFromByteBufferObject(ByteBuffer buffer, OWALChanges walChanges, int offset) {
    final int clusterId = walChanges.getShortValue(buffer, offset);

    // Wrong implementation but needed for binary compatibility
    final long clusterPosition = OLongSerializer.INSTANCE
        .deserialize(walChanges.getBinaryValue(buffer, offset + OShortSerializer.SHORT_SIZE, OLongSerializer.LONG_SIZE), 0);

    // final long clusterPosition = OLongSerializer.INSTANCE
    // .deserializeFromDirectMemory(pointer, offset + OShortSerializer.SHORT_SIZE);

    return new ORecordId(clusterId, clusterPosition);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public int getObjectSizeInByteBuffer(ByteBuffer buffer, OWALChanges walChanges, int offset) {
    return RID_SIZE;
  }
}


/*
  *
  *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
  *  *
  *  *  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.
  *  *
  *  * For more information: http://orientdb.com
  *
  */
package com.orientechnologies.common.comparator;

import com.orientechnologies.orient.core.config.OGlobalConfiguration;

import java.util.Comparator;

/**
 * Creates comparators for classes that does not implement {@link Comparable} but logically can be compared.
 *
 * @author Andrey Lomakin (a.lomakin-at-orientdb.com)
 * @since 03.07.12
 */
public class OComparatorFactory {
  public static final OComparatorFactory INSTANCE = new OComparatorFactory();

  private static final boolean           unsafeWasDetected;

  static {
    boolean unsafeDetected = false;

    try {
      Class<?> sunClass = Class.forName("sun.misc.Unsafe");
      unsafeDetected = sunClass != null;
    } catch (ClassNotFoundException ignore) {
      // Ignore
    }

    unsafeWasDetected = unsafeDetected;
  }

  /**
   * Returns {@link Comparator} instance if applicable one exist or null otherwise.
   *
   * @param clazz
   *          Class of object that is going to be compared.
   * @param <T>
   *          Class of object that is going to be compared.
   * @return {@link Comparator} instance if applicable one exist or null otherwise.
   */
  @SuppressWarnings("unchecked")
  public <T> Comparator<T> getComparator(Class<T> clazz) {
    boolean useUnsafe = OGlobalConfiguration.MEMORY_USE_UNSAFE.getValueAsBoolean();

    if (clazz.equals(byte[].class)) {
      if (useUnsafe && unsafeWasDetected)
        return (Comparator<T>) OUnsafeByteArrayComparator.INSTANCE;

      return (Comparator<T>) OByteArrayComparator.INSTANCE;
    }

    return null;
  }
}


package com.orientechnologies.orient.server.distributed.impl;

import com.hazelcast.core.HazelcastException;
import com.hazelcast.core.HazelcastInstanceNotActiveException;
import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.concur.OOfflineNodeException;
import com.orientechnologies.common.concur.lock.OInterruptedException;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.io.OFileUtils;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.compression.impl.OZIPCompressionUtil;
import com.orientechnologies.orient.core.db.*;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentEmbedded;
import com.orientechnologies.orient.core.db.record.OClassTrigger;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ORecordOperation;
import com.orientechnologies.orient.core.enterprise.OEnterpriseEndpoint;
import com.orientechnologies.orient.core.exception.*;
import com.orientechnologies.orient.core.hook.ORecordHook;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.index.OClassIndexManager;
import com.orientechnologies.orient.core.index.OIndex;
import com.orientechnologies.orient.core.metadata.OMetadataDefault;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OImmutableClass;
import com.orientechnologies.orient.core.metadata.schema.OImmutableSchema;
import com.orientechnologies.orient.core.metadata.schema.OView;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.metadata.security.ORule;
import com.orientechnologies.orient.core.metadata.sequence.OSequenceAction;
import com.orientechnologies.orient.core.metadata.sequence.OSequenceLibraryProxy;
import com.orientechnologies.orient.core.query.live.OLiveQueryHook;
import com.orientechnologies.orient.core.query.live.OLiveQueryHookV2;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ODocumentInternal;
import com.orientechnologies.orient.core.schedule.OScheduledEvent;
import com.orientechnologies.orient.core.sql.executor.OExecutionPlan;
import com.orientechnologies.orient.core.sql.executor.OResultSet;
import com.orientechnologies.orient.core.storage.ORecordDuplicatedException;
import com.orientechnologies.orient.core.storage.ORecordMetadata;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.storage.cluster.OPaginatedCluster;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;
import com.orientechnologies.orient.core.tx.OTransactionIndexChanges;
import com.orientechnologies.orient.core.tx.OTransactionIndexChangesPerKey;
import com.orientechnologies.orient.core.tx.OTransactionInternal;
import com.orientechnologies.orient.core.tx.OTransactionOptimistic;
import com.orientechnologies.orient.server.OServer;
import com.orientechnologies.orient.server.distributed.*;
import com.orientechnologies.orient.server.distributed.impl.metadata.OClassDistributed;
import com.orientechnologies.orient.server.distributed.impl.metadata.OSharedContextDistributed;
import com.orientechnologies.orient.server.distributed.impl.task.OCopyDatabaseChunkTask;
import com.orientechnologies.orient.server.distributed.impl.task.ORunQueryExecutionPlanTask;
import com.orientechnologies.orient.server.distributed.impl.task.OSyncClusterTask;
import com.orientechnologies.orient.server.distributed.task.ODistributedKeyLockedException;
import com.orientechnologies.orient.server.distributed.task.ODistributedRecordLockedException;
import com.orientechnologies.orient.server.distributed.task.ORemoteTask;
import com.orientechnologies.orient.server.hazelcast.OHazelcastPlugin;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutionException;

import static com.orientechnologies.orient.core.config.OGlobalConfiguration.*;
import static com.orientechnologies.orient.server.distributed.impl.ONewDistributedTxContextImpl.Status.*;

/**
 * Created by tglman on 30/03/17.
 */
public class ODatabaseDocumentDistributed extends ODatabaseDocumentEmbedded {

  private final OHazelcastPlugin distributedManager;

  public ODatabaseDocumentDistributed(OStorage storage, OHazelcastPlugin hazelcastPlugin) {
    super(storage);
    this.distributedManager = hazelcastPlugin;
  }

  public ODistributedStorage getStorageDistributed() {
    return (ODistributedStorage) super.getStorage();
  }

  /**
   * return the name of local node in the cluster
   *
   * @return the name of local node in the cluster
   */
  public String getLocalNodeName() {
    return distributedManager.getLocalNodeName();
  }

  /**
   * returns the cluster map for current deploy. The keys of the map are node names, the values contain names of clusters (data
   * files) available on the single node.
   *
   * @return the cluster map for current deploy
   */
  public Map<String, Set<String>> getActiveClusterMap() {
    if (distributedManager.isOffline() || !distributedManager.isNodeOnline(distributedManager.getLocalNodeName(), getName())
        || OScenarioThreadLocal.INSTANCE.isRunModeDistributed()) {
      return super.getActiveClusterMap();
    }
    Map<String, Set<String>> result = new HashMap<>();
    ODistributedConfiguration cfg = getDistributedConfiguration();

    for (String server : distributedManager.getActiveServers()) {
      if (getClustersOnServer(cfg, server).contains("*")) {
        //TODO check this!!!
        result.put(server, getStorage().getClusterNames());
      } else {
        result.put(server, getClustersOnServer(cfg, server));
      }
    }
    return result;
  }

  public Set<String> getClustersOnServer(ODistributedConfiguration cfg, String server) {
    Set<String> result = cfg.getClustersOnServer(server);
    if (result.contains("*")) {
      result.remove("*");
      HashSet<String> more = new HashSet<>();
      more.addAll(getStorage().getClusterNames());
      for (String s : cfg.getClusterNames()) {
        if (!cfg.getServers(s, null).contains(s)) {
          more.remove(s);
        }
      }
      result.addAll(more);
    }
    return result;
  }

  @Override
  protected void loadMetadata() {
    loadMetadata(this.getSharedContext());
  }

  @Override
  protected void loadMetadata(OSharedContext ctx) {
    metadata = new OMetadataDefault(this);
    sharedContext = ctx;
    metadata.init(sharedContext);
    sharedContext.load(this);
  }

  /**
   * returns the data center map for current deploy. The keys are data center names, the values are node names per data center
   *
   * @return data center map for current deploy
   */
  public Map<String, Set<String>> getActiveDataCenterMap() {
    Map<String, Set<String>> result = new HashMap<>();
    ODistributedConfiguration cfg = getDistributedConfiguration();
    Set<String> servers = cfg.getRegisteredServers();
    for (String server : servers) {
      String dc = cfg.getDataCenterOfServer(server);
      Set<String> dcConfig = result.get(dc);
      if (dcConfig == null) {
        dcConfig = new HashSet<>();
        result.put(dc, dcConfig);
      }
      dcConfig.add(server);
    }
    return result;
  }

  @Override
  public boolean isSharded() {
    Map<String, Set<String>> clusterMap = getActiveClusterMap();
    Iterator<Set<String>> iter = clusterMap.values().iterator();
    Set<String> firstClusterSet = null;
    if (iter.hasNext()) {
      firstClusterSet = iter.next();
    }
    while (iter.hasNext()) {
      if (!firstClusterSet.equals(iter.next())) {
        return true;
      }
    }
    return false;
  }

  @Override
  public boolean isDistributed() {
    return true;
  }

  @Override
  public ODatabaseDocumentInternal copy() {
    ODatabaseDocumentDistributed database = new ODatabaseDocumentDistributed(getStorage(), distributedManager);
    database.init(getConfig(), getSharedContext());
    String user;
    if (getUser() != null) {
      user = getUser().getName();
    } else {
      user = null;
    }
    database.internalOpen(user, null, false);
    database.callOnOpenListeners();
    this.activateOnCurrentThread();
    return database;
  }

  @Override
  public boolean sync(boolean forceDeployment, boolean tryWithDelta) {
    checkSecurity(ORule.ResourceGeneric.DATABASE, "sync", ORole.PERMISSION_UPDATE);
    final OStorage stg = getStorage();
    if (!(stg instanceof ODistributedStorage))
      throw new ODistributedException("SYNC DATABASE command cannot be executed against a non distributed server");

    final ODistributedStorage dStg = (ODistributedStorage) stg;

    final OHazelcastPlugin dManager = (OHazelcastPlugin) dStg.getDistributedManager();
    if (dManager == null || !dManager.isEnabled())
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");

    final String databaseName = getName();

    return dManager.installDatabase(true, databaseName, forceDeployment, tryWithDelta);
  }

  @Override
  public Map<String, Object> getHaStatus(boolean servers, boolean db, boolean latency, boolean messages) {
    checkSecurity(ORule.ResourceGeneric.SERVER, "status", ORole.PERMISSION_READ);

    if (distributedManager == null || !distributedManager.isEnabled())
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");

    final String databaseName = getName();

    final ODistributedConfiguration cfg = distributedManager.getDatabaseConfiguration(databaseName);

    Map<String, Object> row = new HashMap<>();
    final StringBuilder output = new StringBuilder();
    if (servers)
      row.put("servers", distributedManager.getClusterConfiguration());
    if (db)
      row.put("database", cfg.getDocument());
    if (latency)
      row.put("latency", ODistributedOutput.formatLatency(distributedManager, distributedManager.getClusterConfiguration()));
    if (messages)
      row.put("messages", ODistributedOutput.formatMessages(distributedManager, distributedManager.getClusterConfiguration()));

    return row;
  }

  @Override
  public boolean removeHaServer(String serverName) {
    checkSecurity(ORule.ResourceGeneric.SERVER, "remove", ORole.PERMISSION_EXECUTE);

    if (distributedManager == null || !distributedManager.isEnabled())
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");

    final String databaseName = getName();

    // The last parameter (true) indicates to set the node's database status to OFFLINE.
    // If this is changed to false, the node will be set to NOT_AVAILABLE, and then the auto-repairer will
    // re-synchronize the database on the node, and then set it to ONLINE.
    return distributedManager.removeNodeFromConfiguration(serverName, databaseName, false, true);
  }

  @Override
  public Map<String, Object> syncCluster(String clusterName) {
    checkSecurity(ORule.ResourceGeneric.CLUSTER, "sync", ORole.PERMISSION_UPDATE);

    if (distributedManager == null || !distributedManager.isEnabled())
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");

    final String databaseName = getName();

    final ODistributedConfiguration cfg = distributedManager.getDatabaseConfiguration(databaseName);
    OServer serverInstance = distributedManager.getServerInstance();
    final String dbPath = serverInstance.getDatabaseDirectory() + databaseName;

    final String nodeName = distributedManager.getLocalNodeName();

    final List<String> nodesWhereClusterIsCfg = cfg.getServers(clusterName, null);
    nodesWhereClusterIsCfg.remove(nodeName);

    if (nodesWhereClusterIsCfg.isEmpty())
      throw new OCommandExecutionException(
          "Cannot synchronize cluster '" + clusterName + "' because is not configured on any running nodes");

    final OSyncClusterTask task = new OSyncClusterTask(clusterName);
    final ODistributedResponse response = distributedManager
        .sendRequest(databaseName, null, nodesWhereClusterIsCfg, task, distributedManager.getNextMessageIdCounter(),
            ODistributedRequest.EXECUTION_MODE.RESPONSE, null, null, null);

    final Map<String, Object> results = (Map<String, Object>) response.getPayload();

    File tempFile = null;
    FileOutputStream out = null;
    try {
      tempFile = new File(Orient.getTempPath() + "/backup_" + databaseName + "_" + clusterName + "_toInstall.zip");
      if (tempFile.exists())
        tempFile.delete();
      else
        tempFile.getParentFile().mkdirs();
      tempFile.createNewFile();

      long fileSize = 0;
      out = new FileOutputStream(tempFile, false);
      for (Map.Entry<String, Object> r : results.entrySet()) {
        final Object value = r.getValue();

        if (value instanceof Boolean) {
          continue;
        } else if (value instanceof Throwable) {
          ODistributedServerLog
              .error(null, nodeName, r.getKey(), ODistributedServerLog.DIRECTION.IN, "error on installing cluster %s in %s",
                  (Exception) value, databaseName, dbPath);
        } else if (value instanceof ODistributedDatabaseChunk) {
          ODistributedDatabaseChunk chunk = (ODistributedDatabaseChunk) value;

          // DELETE ANY PREVIOUS .COMPLETED FILE
          final File completedFile = new File(tempFile.getAbsolutePath() + ".completed");
          if (completedFile.exists())
            completedFile.delete();

          fileSize = writeDatabaseChunk(nodeName, 1, chunk, out);
          for (int chunkNum = 2; !chunk.last; chunkNum++) {
            final Object result = distributedManager.sendRequest(databaseName, null, OMultiValue.getSingletonList(r.getKey()),
                new OCopyDatabaseChunkTask(chunk.filePath, chunkNum, chunk.offset + chunk.buffer.length, chunk.gzipCompressed),
                distributedManager.getNextMessageIdCounter(), ODistributedRequest.EXECUTION_MODE.RESPONSE, null, null, null);

            if (result instanceof Boolean)
              continue;
            else if (result instanceof Exception) {
              ODistributedServerLog.error(null, nodeName, r.getKey(), ODistributedServerLog.DIRECTION.IN,
                  "error on installing database %s in %s (chunk #%d)", (Exception) result, databaseName, dbPath, chunkNum);
            } else if (result instanceof ODistributedDatabaseChunk) {
              chunk = (ODistributedDatabaseChunk) result;

              fileSize += writeDatabaseChunk(nodeName, chunkNum, chunk, out);
            }
          }

          out.flush();

          // CREATE THE .COMPLETED FILE TO SIGNAL EOF
          new File(tempFile.getAbsolutePath() + ".completed").createNewFile();
        }
      }

      final String tempDirectoryPath = Orient.getTempPath() + "/backup_" + databaseName + "_" + clusterName + "_toInstall";
      final File tempDirectory = new File(tempDirectoryPath);
      tempDirectory.mkdirs();

      OZIPCompressionUtil.uncompressDirectory(new FileInputStream(tempFile), tempDirectory.getAbsolutePath(), null);

      ODatabaseDocumentInternal db = ODatabaseRecordThreadLocal.instance().getIfDefined();
      final boolean openDatabaseHere = db == null;
      if (db == null)
        db = serverInstance.openDatabase("plocal:" + dbPath, "", "", null, true);

      try {

        final OAbstractPaginatedStorage stg = (OAbstractPaginatedStorage) db.getStorage().getUnderlying();

        // TODO: FREEZE COULD IT NOT NEEDED
        stg.freeze(false);
        try {

          final OPaginatedCluster cluster = (OPaginatedCluster) stg.getClusterByName(clusterName);

          final File tempClusterFile = new File(tempDirectoryPath + "/" + clusterName + OPaginatedCluster.DEF_EXTENSION);

          cluster.replaceFile(tempClusterFile);

        } finally {
          stg.release();
        }

        db.getLocalCache().invalidate();

      } finally {
        if (openDatabaseHere)
          db.close();
      }

      Map<String, Object> result = new HashMap<>();
      result.put("fileSize", fileSize);
      result.put("message", "Cluster correctly replaced");
      result.put("result", true);
      return result;

    } catch (Exception e) {
      ODistributedServerLog
          .error(null, nodeName, null, ODistributedServerLog.DIRECTION.NONE, "error on transferring database '%s' to '%s'", e,
              databaseName, tempFile);
      throw OException.wrapException(new ODistributedException("Error on transferring database"), e);
    } finally {
      try {
        if (out != null) {
          out.flush();
          out.close();
        }
      } catch (IOException e) {
      }
    }

  }

  protected static long writeDatabaseChunk(final String iNodeName, final int iChunkId, final ODistributedDatabaseChunk chunk,
      final FileOutputStream out) throws IOException {

    ODistributedServerLog
        .warn(null, iNodeName, null, ODistributedServerLog.DIRECTION.NONE, "- writing chunk #%d offset=%d size=%s", iChunkId,
            chunk.offset, OFileUtils.getSizeAsString(chunk.buffer.length));
    out.write(chunk.buffer);

    return chunk.buffer.length;
  }

  @Override
  public OResultSet queryOnNode(String nodeName, OExecutionPlan executionPlan, Map<Object, Object> inputParameters) {
    ORunQueryExecutionPlanTask task = new ORunQueryExecutionPlanTask(executionPlan, inputParameters, nodeName);
    ODistributedResponse result = executeTaskOnNode(task, nodeName);
    return task.getResult(result, this);
  }

  public ODistributedResponse executeTaskOnNode(ORemoteTask task, String nodeName) {

    if (distributedManager == null || !distributedManager.isEnabled())
      throw new ODistributedException("OrientDB is not started in distributed mode");

    final String databaseName = getName();

    return distributedManager
        .sendRequest(databaseName, null, Collections.singletonList(nodeName), task, distributedManager.getNextMessageIdCounter(),
            ODistributedRequest.EXECUTION_MODE.RESPONSE, null, null, null);
  }

  @Override
  public void init(OrientDBConfig config, OSharedContext sharedContext) {
    OScenarioThreadLocal.executeAsDistributed(() -> {
      super.init(config, sharedContext);
      return null;
    });
  }

  protected void createMetadata(OSharedContext ctx) {
    // CREATE THE DEFAULT SCHEMA WITH DEFAULT USER
    OSharedContext shared = ctx;
    metadata.init(shared);
    ((OSharedContextDistributed) shared).create(this);
  }

  public int assignAndCheckCluster(ORecord record, String iClusterName) {
    ORecordId rid = (ORecordId) record.getIdentity();
    // if provided a cluster name use it.
    if (rid.getClusterId() <= ORID.CLUSTER_POS_INVALID && iClusterName != null) {
      rid.setClusterId(getClusterIdByName(iClusterName));
      if (rid.getClusterId() == -1)
        throw new IllegalArgumentException("Cluster name '" + iClusterName + "' is not configured");

    }
    OClass schemaClass = null;
    // if cluster id is not set yet try to find it out
    if (rid.getClusterId() <= ORID.CLUSTER_ID_INVALID && getStorage().isAssigningClusterIds()) {
      if (record instanceof ODocument) {
        //Immutable Schema Class not support distributed yet.
        schemaClass = ((ODocument) record).getSchemaClass();
        if (schemaClass != null) {
          if (schemaClass.isAbstract())
            throw new OSchemaException("Document belongs to abstract class " + schemaClass.getName() + " and cannot be saved");
          rid.setClusterId(((OClassDistributed) schemaClass).getClusterForNewInstance(this, (ODocument) record));
        } else
          throw new ODatabaseException("Cannot save (4) document " + record + ": no class or cluster defined");
      } else {
        throw new ODatabaseException("Cannot save (5) document " + record + ": no class or cluster defined");
      }
    } else if (record instanceof ODocument)
      schemaClass = ((ODocument) record).getSchemaClass();
    // If the cluster id was set check is validity
    if (rid.getClusterId() > ORID.CLUSTER_ID_INVALID) {
      if (schemaClass != null) {
        String messageClusterName = getClusterNameById(rid.getClusterId());
        checkRecordClass(schemaClass, messageClusterName, rid);
        if (!schemaClass.hasClusterId(rid.getClusterId())) {
          throw new IllegalArgumentException(
              "Cluster name '" + messageClusterName + "' (id=" + rid.getClusterId() + ") is not configured to store the class '"
                  + schemaClass.getName() + "', valid are " + Arrays.toString(schemaClass.getClusterIds()));
        }
      }
    }
    return rid.getClusterId();
  }

  @Override
  public void internalCommit(OTransactionInternal iTx) {
    int protocolVersion = DISTRIBUTED_REPLICATION_PROTOCOL_VERSION.getValueAsInteger();
    if (OScenarioThreadLocal.INSTANCE.isRunModeDistributed() || (iTx.isSequenceTransaction() && protocolVersion == 2)) {
      //Exclusive for handling schema manipulation, remove after refactor for distributed schema
      super.internalCommit(iTx);
    } else {
      switch (protocolVersion) {
      case 1:
        distributedCommitV1(iTx);
        break;
      default:
        throw new IllegalStateException(
            "Invalid distributed replicaiton protocol version: " + DISTRIBUTED_REPLICATION_PROTOCOL_VERSION.getValueAsInteger());
      }
    }
  }

  @Override
  public <T> T sendSequenceAction(OSequenceAction action) throws ExecutionException, InterruptedException {
    throw new UnsupportedOperationException();
  }

  public void distributedCommitV1(OTransactionInternal iTx) {
    //This is future may handle a retry
    try {
      for (ORecordOperation txEntry : iTx.getRecordOperations()) {
        if (txEntry.type == ORecordOperation.CREATED || txEntry.type == ORecordOperation.UPDATED) {
          final ORecord record = txEntry.getRecord();
          if (record instanceof ODocument)
            ((ODocument) record).validate();
        }
      }
      final ODistributedConfiguration dbCfg = getStorageDistributed().getDistributedConfiguration();
      ODistributedServerManager dManager = getStorageDistributed().getDistributedManager();
      final String localNodeName = dManager.getLocalNodeName();
      getStorageDistributed().checkNodeIsMaster(localNodeName, dbCfg, "Transaction Commit");
      ONewDistributedTransactionManager txManager = new ONewDistributedTransactionManager(getStorageDistributed(), dManager,
          getStorageDistributed().getLocalDistributedDatabase());
      int quorum = 0;
      for (String clusterName : txManager.getInvolvedClusters(iTx.getRecordOperations())) {
        final List<String> clusterServers = dbCfg.getServers(clusterName, null);
        final int writeQuorum = dbCfg.getWriteQuorum(clusterName, clusterServers.size(), localNodeName);
        quorum = Math.max(quorum, writeQuorum);
      }
      final int availableNodes = dManager.getAvailableNodes(getName());

      if (quorum > availableNodes) {
        List<String> online = dManager.getOnlineNodes(getName());
        throw new ODistributedException("No enough nodes online to execute the operation, online nodes: " + online);
      }

      txManager.commit(this, iTx);
      return;
    } catch (OValidationException e) {
      throw e;
    } catch (HazelcastInstanceNotActiveException e) {
      throw new OOfflineNodeException("Hazelcast instance is not available");

    } catch (HazelcastException e) {
      throw new OOfflineNodeException("Hazelcast instance is not available");
    } catch (Exception e) {
      getStorageDistributed().handleDistributedException("Cannot route TX operation against distributed node", e);
    }

  }

  public void acquireLocksForTx(OTransactionInternal tx, ODistributedTxContext txContext) {
    //Sort and lock transaction entry in distributed environment
    Set<ORID> rids = new TreeSet<>();
    for (ORecordOperation entry : tx.getRecordOperations()) {
      if (entry.getType() != ORecordOperation.CREATED) {
        rids.add(entry.getRID().copy());
      } else {
        rids.add(new ORecordId(entry.getRID().getClusterId(), -1));
      }
    }
    for (ORID rid : rids) {
      txContext.lock(rid);
    }

    //using OPair because there could be different types of values here, so falling back to lexicographic sorting
    Set<String> keys = new TreeSet<>();
    for (Map.Entry<String, OTransactionIndexChanges> change : tx.getIndexOperations().entrySet()) {
      OIndex<?> index = getMetadata().getIndexManager().getIndex(change.getKey());
      if (OClass.INDEX_TYPE.UNIQUE.name().equals(index.getType()) || OClass.INDEX_TYPE.UNIQUE_HASH_INDEX.name()
          .equals(index.getType()) || OClass.INDEX_TYPE.DICTIONARY.name().equals(index.getType())
          || OClass.INDEX_TYPE.DICTIONARY_HASH_INDEX.name().equals(index.getType())) {
        for (OTransactionIndexChangesPerKey changesPerKey : change.getValue().changesPerKey.values()) {
          keys.add(String.valueOf(changesPerKey.key));
        }
        if (!change.getValue().nullKeyChanges.entries.isEmpty()) {
          keys.add("null");
        }
      }
    }
    for (String key : keys) {
      txContext.lockIndexKey(key);
    }

  }

  public boolean beginDistributedTx(ODistributedRequestId requestId, OTransactionInternal tx, boolean local, int retryCount) {
    ODistributedDatabase localDistributedDatabase = getStorageDistributed().getLocalDistributedDatabase();
    ONewDistributedTxContextImpl txContext = new ONewDistributedTxContextImpl((ODistributedDatabaseImpl) localDistributedDatabase,
        requestId, tx);
    try {
      internalBegin2pc(txContext, local);
      txContext.setStatus(SUCCESS);
      localDistributedDatabase.registerTxContext(requestId, txContext);
    } catch (OConcurrentCreateException ex) {
      if (retryCount >= 0 && retryCount < getConfiguration().getValueAsInteger(DISTRIBUTED_CONCURRENT_TX_MAX_AUTORETRY)) {
        if (ex.getExpectedRid().getClusterPosition() > ex.getActualRid().getClusterPosition()) {
          OLogManager.instance()
              .debug(this, "Allocation of rid not match, expected:%s actual:%s waiting for re-enqueue request", ex.getExpectedRid(),
                  ex.getActualRid());
          txContext.unlock();
          return false;
        }
      }
      txContext.setStatus(FAILED);
      localDistributedDatabase.registerTxContext(requestId, txContext);
      throw ex;
    } catch (OConcurrentModificationException ex) {
      if (retryCount >= 0 && retryCount < getConfiguration().getValueAsInteger(DISTRIBUTED_CONCURRENT_TX_MAX_AUTORETRY)) {
        if (ex.getEnhancedRecordVersion() > ex.getEnhancedDatabaseVersion()) {
          OLogManager.instance()
              .info(this, "Persistent version not match, record:%s expected:%s actual:%s waiting for re-enqueue request",
                  ex.getRid(), ex.getEnhancedRecordVersion(), ex.getEnhancedDatabaseVersion());
          txContext.unlock();
          return false;
        }
      }
      txContext.setStatus(FAILED);
      localDistributedDatabase.registerTxContext(requestId, txContext);
      throw ex;
    } catch (ORecordNotFoundException e) {
      // This error can happen only in deserialization before locks happen, no need to unlock
      if (retryCount >= 0 && retryCount < getConfiguration().getValueAsInteger(DISTRIBUTED_CONCURRENT_TX_MAX_AUTORETRY)) {
        return false;
      }
      txContext.setStatus(FAILED);
      localDistributedDatabase.registerTxContext(requestId, txContext);
      throw e;
    } catch (ODistributedRecordLockedException | ODistributedKeyLockedException ex) {
      /// ?? do i've to save this state as well ?
      txContext.setStatus(TIMEDOUT);
      localDistributedDatabase.registerTxContext(requestId, txContext);
      throw ex;
    } catch (ORecordDuplicatedException ex) {
      txContext.setStatus(FAILED);
      localDistributedDatabase.registerTxContext(requestId, txContext);
      throw ex;
    } catch (OLowDiskSpaceException ex) {
      distributedManager.setDatabaseStatus(getLocalNodeName(), getName(), ODistributedServerManager.DB_STATUS.OFFLINE);
      throw ex;
    }
    return true;
  }

  /**
   * The Local commit is different from a remote commit due to local rid pre-allocation
   *
   * @param transactionId
   */
  public void commit2pcLocal(ODistributedRequestId transactionId) {
    commit2pc(transactionId, true);
  }

  /**
   * @param transactionId
   *
   * @return null returned means that commit failed
   */
  public boolean commit2pc(ODistributedRequestId transactionId, boolean local) {
    getStorageDistributed().resetLastValidBackup();
    ODistributedDatabase localDistributedDatabase = getStorageDistributed().getLocalDistributedDatabase();

    ONewDistributedTxContextImpl txContext = (ONewDistributedTxContextImpl) localDistributedDatabase.getTxContext(transactionId);

    if (txContext != null) {
      if (SUCCESS.equals(txContext.getStatus())) {
        try {
          txContext.commit(this);
          localDistributedDatabase.popTxContext(transactionId);
          OLiveQueryHook.notifyForTxChanges(this);
          OLiveQueryHookV2.notifyForTxChanges(this);
        } finally {
          OLiveQueryHook.removePendingDatabaseOps(this);
          OLiveQueryHookV2.removePendingDatabaseOps(this);
        }
        return true;
      } else if (TIMEDOUT.equals(txContext.getStatus())) {
        int nretry = getConfiguration().getValueAsInteger(DISTRIBUTED_CONCURRENT_TX_MAX_AUTORETRY);
        int delay = getConfiguration().getValueAsInteger(DISTRIBUTED_CONCURRENT_TX_AUTORETRY_DELAY);

        for (int i = 0; i < nretry; i++) {
          try {
            if (i > 0) {
              try {
                Thread.sleep(new Random().nextInt(delay));
              } catch (InterruptedException e) {
                OException.wrapException(new OInterruptedException(e.getMessage()), e);
              }
            }
            internalBegin2pc(txContext, local);
            txContext.setStatus(SUCCESS);
            break;
          } catch (ODistributedRecordLockedException | ODistributedKeyLockedException ex) {
            // Just retry
          } catch (Exception ex) {
            OLogManager.instance()
                .warn(ODatabaseDocumentDistributed.this, "Error beginning timed out transaction: %s ", ex, transactionId);
            break;
          }
        }
        if (!SUCCESS.equals(txContext.getStatus())) {
          txContext.destroy();
          localDistributedDatabase.popTxContext(transactionId);
          Orient.instance().submit(() -> {
            OLogManager.instance()
                .warn(ODatabaseDocumentDistributed.this, "Reached limit of retry for commit tx:%s forcing database re-install",
                    transactionId);
            distributedManager.installDatabase(false, ODatabaseDocumentDistributed.this.getName(), true, true);
          });
          return true;
        }
        try {
          txContext.commit(this);
          localDistributedDatabase.popTxContext(transactionId);
          OLiveQueryHook.notifyForTxChanges(this);
          OLiveQueryHookV2.notifyForTxChanges(this);
          return true;
        } finally {
          OLiveQueryHook.removePendingDatabaseOps(this);
          OLiveQueryHookV2.removePendingDatabaseOps(this);
        }

      } else {
        txContext.destroy();
        localDistributedDatabase.popTxContext(transactionId);
        Orient.instance().submit(() -> {
          OLogManager.instance()
              .warn(ODatabaseDocumentDistributed.this, "Reached limit of retry for commit tx:%s forcing database re-install",
                  transactionId);
          distributedManager.installDatabase(false, ODatabaseDocumentDistributed.this.getName(), true, true);
        });
        return true;
      }
    }
    return false;
  }

  public boolean rollback2pc(ODistributedRequestId transactionId) {
    ODistributedDatabase localDistributedDatabase = getStorageDistributed().getLocalDistributedDatabase();
    ODistributedTxContext txContext = localDistributedDatabase.popTxContext(transactionId);
    if (txContext != null) {
      txContext.destroy();
      OLiveQueryHook.removePendingDatabaseOps(this);
      OLiveQueryHookV2.removePendingDatabaseOps(this);
      return true;
    }
    return false;
  }

  public void internalCommit2pc(ONewDistributedTxContextImpl txContext) {
    try {
      OTransactionInternal tx = txContext.getTransaction();
      ((OAbstractPaginatedStorage) this.getStorage().getUnderlying()).commitPreAllocated(tx);
    } catch (OLowDiskSpaceException ex) {
      distributedManager.setDatabaseStatus(getLocalNodeName(), getName(), ODistributedServerManager.DB_STATUS.OFFLINE);
      throw ex;
    } finally {
      txContext.destroy();
    }

  }

  public void internalBegin2pc(ONewDistributedTxContextImpl txContext, boolean local) {
    getStorageDistributed().resetLastValidBackup();
    OTransactionInternal transaction = txContext.getTransaction();
    //This is moved before checks because also the coordinator first node allocate before checks
    if (!local) {
      ((OTransactionOptimisticDistributed) transaction).setDatabase(this);
      ((OTransactionOptimistic) transaction).begin();
    }

    acquireLocksForTx(transaction, txContext);

    firstPhaseDataChecks(local, transaction);

  }

  private void firstPhaseDataChecks(boolean local, OTransactionInternal transaction) {
    ((OAbstractPaginatedStorage) getStorage().getUnderlying()).preallocateRids(transaction);

    for (Map.Entry<String, OTransactionIndexChanges> change : transaction.getIndexOperations().entrySet()) {
      OIndex<?> index = getSharedContext().getIndexManager().getRawIndex(change.getKey());
      if (OClass.INDEX_TYPE.UNIQUE.name().equals(index.getType()) || OClass.INDEX_TYPE.UNIQUE_HASH_INDEX.name()
          .equals(index.getType())) {
        OTransactionIndexChangesPerKey nullKeyChanges = change.getValue().nullKeyChanges;
        if (!nullKeyChanges.entries.isEmpty()) {
          OIdentifiable old = (OIdentifiable) index.get(null);
          Object newValue = nullKeyChanges.entries.get(nullKeyChanges.entries.size() - 1).value;
          if (old != null && !old.equals(newValue)) {
            boolean oldValueRemoved = false;
            for (OTransactionIndexChangesPerKey.OTransactionIndexEntry entry : nullKeyChanges.entries) {
              if (entry.value != null && entry.value.equals(old) && entry.operation == OTransactionIndexChanges.OPERATION.REMOVE) {
                oldValueRemoved = true;
                break;
              }
            }
            if (!oldValueRemoved) {
              throw new ORecordDuplicatedException(String
                  .format("Cannot index record %s: found duplicated key '%s' in index '%s' previously assigned to the record %s",
                      newValue, null, getName(), old.getIdentity()), getName(), old.getIdentity(), null);
            }
          }
        }

        for (OTransactionIndexChangesPerKey changesPerKey : change.getValue().changesPerKey.values()) {
          OIdentifiable old = (OIdentifiable) index.get(changesPerKey.key);
          if (!changesPerKey.entries.isEmpty()) {
            Object newValue = changesPerKey.entries.get(changesPerKey.entries.size() - 1).value;
            if (old != null && !old.equals(newValue)) {
              boolean oldValueRemoved = false;
              for (OTransactionIndexChangesPerKey.OTransactionIndexEntry entry : changesPerKey.entries) {
                if (entry.value != null && entry.value.equals(old)
                    && entry.operation == OTransactionIndexChanges.OPERATION.REMOVE) {
                  oldValueRemoved = true;
                  break;
                }
              }
              if (!oldValueRemoved) {
                throw new ORecordDuplicatedException(String
                    .format("Cannot index record %s: found duplicated key '%s' in index '%s' previously assigned to the record %s",
                        newValue, changesPerKey.key, getName(), old.getIdentity()), getName(), old.getIdentity(),
                    changesPerKey.key);
              }
            }
          }
        }
      }
    }

    for (ORecordOperation entry : transaction.getRecordOperations()) {
      if (entry.getType() != ORecordOperation.CREATED) {
        int changeVersion = entry.getRecord().getVersion();
        ORecordMetadata metadata = getStorage().getRecordMetadata(entry.getRID());
        if (metadata == null) {
          if (((OAbstractPaginatedStorage) getStorage().getUnderlying()).isDeleted(entry.getRID())) {
            throw new OConcurrentModificationException(entry.getRID(), changeVersion, changeVersion, entry.getType());
          } else {
            //don't exist i get -1, -1 rid that put the operation in queue for retry.
            throw new OConcurrentCreateException(new ORecordId(-1, -1), entry.getRID());
          }
        }
        int persistentVersion = metadata.getVersion();
        if (changeVersion != persistentVersion) {
          throw new OConcurrentModificationException(entry.getRID(), persistentVersion, changeVersion, entry.getType());
        }
      }
    }
  }

  public void afterCreateOperations(final OIdentifiable id) {
    if (id instanceof ODocument) {
      ODocument doc = (ODocument) id;
      OImmutableClass clazz = ODocumentInternal.getImmutableSchemaClass(this, doc);
      if (clazz != null) {
        OClassIndexManager.checkIndexesAfterCreate(doc, this);
        if (clazz.isFunction()) {
          this.getSharedContext().getFunctionLibrary().createdFunction(doc);
          Orient.instance().getScriptManager().close(this.getName());
        }
        if (clazz.isOuser() || clazz.isOrole()) {
          getMetadata().getSecurity().incrementVersion();
        }
        if (clazz.isSequence()) {
          ((OSequenceLibraryProxy) getMetadata().getSequenceLibrary()).getDelegate().onSequenceCreated(this, doc);
        }
        if (clazz.isScheduler()) {
          getMetadata().getScheduler().scheduleEvent(new OScheduledEvent(doc));
        }
        if (clazz.isTriggered()) {
          OClassTrigger.onRecordAfterCreate(doc, this);
        }
        OLiveQueryHook.addOp(doc, ORecordOperation.CREATED, this);
        OLiveQueryHookV2.addOp(doc, ORecordOperation.CREATED, this);
      }
    }
    callbackHooks(ORecordHook.TYPE.AFTER_CREATE, id);
  }

  public void afterUpdateOperations(final OIdentifiable id) {
    if (id instanceof ODocument) {
      ODocument doc = (ODocument) id;
      OImmutableClass clazz = ODocumentInternal.getImmutableSchemaClass(this, doc);
      if (clazz != null) {
        OClassIndexManager.checkIndexesAfterUpdate((ODocument) id, this);
        if (clazz.isFunction()) {
          this.getSharedContext().getFunctionLibrary().updatedFunction(doc);
          Orient.instance().getScriptManager().close(this.getName());
        }
        if (clazz.isOuser() || clazz.isOrole()) {
          getMetadata().getSecurity().incrementVersion();
        }
        if (clazz.isSequence()) {
          ((OSequenceLibraryProxy) getMetadata().getSequenceLibrary()).getDelegate().onSequenceUpdated(this, doc);
        }
        if (clazz.isTriggered()) {
          OClassTrigger.onRecordAfterUpdate(doc, this);
        }
        OLiveQueryHook.addOp(doc, ORecordOperation.UPDATED, this);
        OLiveQueryHookV2.addOp(doc, ORecordOperation.UPDATED, this);
      }
    }
    callbackHooks(ORecordHook.TYPE.AFTER_UPDATE, id);
  }

  public void afterDeleteOperations(final OIdentifiable id) {
    if (id instanceof ODocument) {
      ODocument doc = (ODocument) id;
      OImmutableClass clazz = ODocumentInternal.getImmutableSchemaClass(this, doc);
      if (clazz != null) {
        OClassIndexManager.checkIndexesAfterDelete(doc, this);
        if (clazz.isFunction()) {
          this.getSharedContext().getFunctionLibrary().droppedFunction(doc);
          Orient.instance().getScriptManager().close(this.getName());
        }
        if (clazz.isOuser() || clazz.isOrole()) {
          getMetadata().getSecurity().incrementVersion();
        }
        if (clazz.isSequence()) {
          ((OSequenceLibraryProxy) getMetadata().getSequenceLibrary()).getDelegate().onSequenceDropped(this, doc);
        }
        if (clazz.isScheduler()) {
          final String eventName = doc.field(OScheduledEvent.PROP_NAME);
          getSharedContext().getScheduler().removeEventInternal(eventName);
        }
        if (clazz.isTriggered()) {
          OClassTrigger.onRecordAfterDelete(doc, this);
        }
      }
      OLiveQueryHook.addOp(doc, ORecordOperation.DELETED, this);
      OLiveQueryHookV2.addOp(doc, ORecordOperation.DELETED, this);
    }
    callbackHooks(ORecordHook.TYPE.AFTER_DELETE, id);
  }

  @Override
  public OView getViewFromCluster(int cluster) {
    OImmutableSchema schema = getMetadata().getImmutableSchemaSnapshot();
    OView view = schema.getViewByClusterId(cluster);
    if (view == null) {
      String viewName = ((OSharedContextDistributed) getSharedContext()).getViewManager().getViewFromOldCluster(cluster);
      if (viewName != null) {
        view = schema.getView(viewName);
      }
    }
    return view;
  }

  public OEnterpriseEndpoint getEnterpriseEndpoint() {
    OServer server = distributedManager.getServerInstance();
    return server.getPlugins().stream().filter(OEnterpriseEndpoint.class::isInstance).findFirst()
        .map(OEnterpriseEndpoint.class::cast).orElse(null);
  }

  public ODistributedServerManager getDistributedManager() {
    return distributedManager;
  }

  public ODistributedConfiguration getDistributedConfiguration() {
    return getStorageDistributed().getDistributedConfiguration();
  }

}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.index;

import com.orientechnologies.common.serialization.types.OBinarySerializer;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.exception.OConfigurationException;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.binary.OBinarySerializerFactory;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Index definition that use the serializer specified at run-time not based on type. This is useful to have custom type keys for
 * indexes.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class ORuntimeKeyIndexDefinition<T> extends OAbstractIndexDefinition {
  private static final long                 serialVersionUID = -8855918974071833818L;
  private transient    OBinarySerializer<T> serializer;

  @SuppressWarnings("unchecked")
  public ORuntimeKeyIndexDefinition(final byte iId) {
    super();

    serializer = (OBinarySerializer<T>) OBinarySerializerFactory.getInstance().getObjectSerializer(iId);
    if (serializer == null)
      throw new OConfigurationException("Runtime index definition cannot find binary serializer with id=" + iId
          + ". Assure to plug custom serializer into the server.");
  }

  public ORuntimeKeyIndexDefinition() {
  }

  public List<String> getFields() {
    return Collections.emptyList();
  }

  public List<String> getFieldsToIndex() {
    return Collections.emptyList();
  }

  public String getClassName() {
    return null;
  }

  public Comparable<?> createValue(final List<?> params) {
    return (Comparable<?>) params.get(0);
  }

  public Comparable<?> createValue(final Object... params) {
    return createValue(Arrays.asList(params));
  }

  public int getParamCount() {
    return 1;
  }

  public OType[] getTypes() {
    return new OType[0];
  }

  @Override
  public ODocument toStream() {
    document.setInternalStatus(ORecordElement.STATUS.UNMARSHALLING);
    try {
      serializeToStream();
      return document;
    } finally {
      document.setInternalStatus(ORecordElement.STATUS.LOADED);
    }
  }

  @Override
  protected void serializeToStream() {
    super.serializeToStream();

    document.field("keySerializerId", serializer.getId());
    document.field("collate", collate.getName());
    document.field("nullValuesIgnored", isNullValuesIgnored());
  }

  @SuppressWarnings("unchecked")
  @Override
  protected void fromStream() {
    serializeFromStream();
  }

  @Override
  protected void serializeFromStream() {
    super.serializeFromStream();

    final byte keySerializerId = ((Number) document.field("keySerializerId")).byteValue();
    //noinspection unchecked
    serializer = (OBinarySerializer<T>) OBinarySerializerFactory.getInstance().getObjectSerializer(keySerializerId);
    if (serializer == null)
      throw new OConfigurationException("Runtime index definition cannot find binary serializer with id=" + keySerializerId
          + ". Assure to plug custom serializer into the server.");

    setNullValuesIgnored(!Boolean.FALSE.equals(document.<Boolean>field("nullValuesIgnored")));
  }

  public Object getDocumentValueToIndex(final ODocument iDocument) {
    throw new OIndexException("This method is not supported in given index definition.");
  }

  @Override
  public boolean equals(final Object o) {
    if (this == o)
      return true;
    if (o == null || getClass() != o.getClass())
      return false;

    final ORuntimeKeyIndexDefinition<?> that = (ORuntimeKeyIndexDefinition<?>) o;
    return serializer.equals(that.serializer);
  }

  @Override
  public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + serializer.getId();
    return result;
  }

  @Override
  public String toString() {
    return "ORuntimeKeyIndexDefinition{" + "serializer=" + serializer.getId() + '}';
  }

  /**
   * {@inheritDoc}
   */
  public String toCreateIndexDDL(final String indexName, final String indexType, String engine) {
    return "create index `" + indexName + "` " + indexType + ' ' + "runtime " + serializer.getId();
  }

  public OBinarySerializer<T> getSerializer() {
    return serializer;
  }

  @Override
  public boolean isAutomatic() {
    return getClassName() != null;
  }
}