/*
 *
 *  *  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.sql;

import com.orientechnologies.common.util.OPair;
import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.metadata.OMetadataInternal;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.record.OVertex;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.functions.OSQLFunctionRuntime;

import java.util.*;

/**
 * SQL CREATE VERTEX command.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class OCommandExecutorSQLCreateVertex extends OCommandExecutorSQLSetAware implements OCommandDistributedReplicateRequest {
  public static final String NAME = "CREATE VERTEX";
  private OClass                      clazz;
  private String                      clusterName;
  private List<OPair<String, Object>> fields;

  @SuppressWarnings("unchecked")
  public OCommandExecutorSQLCreateVertex parse(final OCommandRequest iRequest) {

    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;

    String queryText = textRequest.getText();
    String originalQuery = queryText;
    try {
      queryText = preParse(queryText, iRequest);
      textRequest.setText(queryText);

      final ODatabaseDocument database = getDatabase();

      init((OCommandRequestText) iRequest);

      String className = null;

      parserRequiredKeyword("CREATE");
      parserRequiredKeyword("VERTEX");

      String temp = parseOptionalWord(true);

      while (temp != null) {
        if (temp.equals("CLUSTER")) {
          clusterName = parserRequiredWord(false);

        } else if (temp.equals(KEYWORD_SET)) {
          fields = new ArrayList<OPair<String, Object>>();
          parseSetFields(clazz, fields);

        } else if (temp.equals(KEYWORD_CONTENT)) {
          parseContent();

        } else if (className == null && temp.length() > 0) {
          className = temp;
          if (className == null)
            // ASSIGN DEFAULT CLASS
            className = "V";

          // GET/CHECK CLASS NAME
          clazz = ((OMetadataInternal) database.getMetadata()).getImmutableSchemaSnapshot().getClass(className);
          if (clazz == null)
            throw new OCommandSQLParsingException("Class '" + className + "' was not found");
        }

        temp = parserOptionalWord(true);
        if (parserIsEnded())
          break;
      }

      if (className == null) {
        // ASSIGN DEFAULT CLASS
        className = "V";

        // GET/CHECK CLASS NAME
        clazz = ((OMetadataInternal) database.getMetadata()).getImmutableSchemaSnapshot().getClass(className);
        if (clazz == null)
          throw new OCommandSQLParsingException("Class '" + className + "' was not found");
      }
    } finally {
      textRequest.setText(originalQuery);
    }
    return this;
  }

  /**
   * Execute the command and return the ODocument object created.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    if (clazz == null)
      throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet");

    // CREATE VERTEX DOES NOT HAVE TO BE IN TX
    final OVertex vertex = getDatabase().newVertex(clazz);

    if (fields != null)
      // EVALUATE FIELDS
      for (final OPair<String, Object> f : fields) {
        if (f.getValue() instanceof OSQLFunctionRuntime)
          f.setValue(((OSQLFunctionRuntime) f.getValue()).getValue(vertex.getRecord(), null, context));
      }

    OSQLHelper.bindParameters(vertex.getRecord(), fields, new OCommandParameters(iArgs), context);

    if (content != null)
      ((ODocument)vertex.getRecord()).merge(content, true, false);

    if (clusterName != null)
      vertex.save(clusterName);
    else
      vertex.save();

    return vertex.getRecord();

  }

  @Override
  public DISTRIBUTED_EXECUTION_MODE getDistributedExecutionMode() {
    return DISTRIBUTED_EXECUTION_MODE.LOCAL;
  }

  @Override
  public QUORUM_TYPE getQuorumType() {
    return QUORUM_TYPE.WRITE;
  }

  @Override
  public Set<String> getInvolvedClusters() {
    if (clazz != null)
      return Collections.singleton(getDatabase().getClusterNameById(clazz.getClusterSelection().getCluster(clazz, null)));
    else if (clusterName != null)
      return getInvolvedClustersOfClusters(Collections.singleton(clusterName));

    return Collections.EMPTY_SET;
  }

  @Override
  public String getSyntax() {
    return "CREATE VERTEX [<class>] [CLUSTER <cluster>] [SET <field> = <expression>[,]*]";
  }
}


/*
 *
 *  *  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.comparator.ODefaultComparator;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.ODocumentSerializable;

import java.io.Serializable;
import java.util.*;

/**
 * Container for the list of heterogeneous values that are going to be stored in in index as composite keys.
 *
 * @author Andrey Lomakin (a.lomakin-at-orientdb.com), Artem Orobets
 */
public class OCompositeKey implements Comparable<OCompositeKey>, Serializable, ODocumentSerializable {
  private static final long         serialVersionUID = 1L;
  /**
   */
  private final        List<Object> keys;

  private final transient Comparator<Object> comparator;

  public OCompositeKey(final List<?> keys) {
    this.keys = new ArrayList<Object>(keys.size());
    this.comparator = ODefaultComparator.INSTANCE;

    for (final Object key : keys)
      addKey(key);
  }

  public OCompositeKey(final Object... keys) {
    this.keys = new ArrayList<Object>(keys.length);
    this.comparator = ODefaultComparator.INSTANCE;

    for (final Object key : keys)
      addKey(key);
  }

  public OCompositeKey() {
    this.keys = new ArrayList<Object>();
    this.comparator = ODefaultComparator.INSTANCE;
  }

  /**
   * Clears the keys array for reuse of the object
   */
  public void reset() {
    if (this.keys != null)
      this.keys.clear();
  }

  /**
   */
  public List<Object> getKeys() {
    return Collections.unmodifiableList(keys);
  }

  /**
   * Add new key value to the list of already registered values.
   *

   * If passed in value is {@link OCompositeKey} itself then its values will be copied in current index. But key itself will not be
   * added.
   *
   * @param key Key to add.
   */
  public void addKey(final Object key) {
    if (key instanceof OCompositeKey) {
      final OCompositeKey compositeKey = (OCompositeKey) key;
      for (final Object inKey : compositeKey.keys) {
        addKey(inKey);
      }
    } else {
      keys.add(key);
    }
  }

  /**
   * Performs partial comparison of two composite keys.
   *

   * Two objects will be equal if the common subset of their keys is equal. For example if first object contains two keys and second
   * contains four keys then only first two keys will be compared.
   *
   * @param otherKey Key to compare.
   *
   * @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified
   * object.
   */
  public int compareTo(final OCompositeKey otherKey) {
    final Iterator<Object> inIter = keys.iterator();
    final Iterator<Object> outIter = otherKey.keys.iterator();

    while (inIter.hasNext() && outIter.hasNext()) {
      final Object inKey = inIter.next();
      final Object outKey = outIter.next();

      if (outKey instanceof OAlwaysGreaterKey)
        return -1;

      if (outKey instanceof OAlwaysLessKey)
        return 1;

      if (inKey instanceof OAlwaysGreaterKey)
        return 1;

      if (inKey instanceof OAlwaysLessKey)
        return -1;

      final int result = comparator.compare(inKey, outKey);
      if (result != 0)
        return result;
    }

    return 0;
  }

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

    final OCompositeKey that = (OCompositeKey) o;

    return keys.equals(that.keys);
  }

  /**
   * {@inheritDoc }
   */
  @Override
  public int hashCode() {
    return keys.hashCode();
  }

  /**
   * {@inheritDoc }
   */
  @Override
  public String toString() {
    return "OCompositeKey{" + "keys=" + keys + '}';
  }

  @Override
  public ODocument toDocument() {
    final ODocument document = new ODocument();
    for (int i = 0; i < keys.size(); i++) {
      document.field("key" + i, keys.get(i));
    }

    return document;
  }

  @Override
  public void fromDocument(ODocument document) {
    document.setLazyLoad(false);

    final String[] fieldNames = document.fieldNames();

    final SortedMap<Integer, Object> keyMap = new TreeMap<Integer, Object>();

    for (String fieldName : fieldNames) {
      if (fieldName.startsWith("key")) {
        final String keyIndex = fieldName.substring(3);
        keyMap.put(Integer.valueOf(keyIndex), document.field(fieldName));
      }
    }

    keys.clear();
    for (Object value : keyMap.values())
      keys.add(value);
  }
}


/*
 *
 *  *  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.exception.OException;
import com.orientechnologies.common.listener.OProgressListener;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.util.OMultiKey;
import com.orientechnologies.common.util.OUncaughtExceptionHandler;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.ODatabase;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.OSharedContext;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentEmbedded;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.db.record.OTrackedSet;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.index.engine.OBaseIndexEngine;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OClassImpl;
import com.orientechnologies.orient.core.metadata.schema.OSchemaShared;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;

import java.nio.channels.UnsupportedAddressTypeException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * Manages indexes at database level. A single instance is shared among multiple databases. Contentions are managed by r/w locks.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 * @author Artem Orobets added composite index managemement
 */
public class OIndexManagerShared extends OIndexManagerAbstract {
  private volatile transient Thread   recreateIndexesThread = null;
  private volatile           boolean  rebuildCompleted      = false;
  private final              OStorage storage;

  public OIndexManagerShared(OStorage storage) {
    super();
    this.storage = storage;
  }

  /**
   * Create a new index with default algorithm.
   *
   * @param iName             - name of index
   * @param iType             - index type. Specified by plugged index factories.
   * @param indexDefinition   metadata that describes index structure
   * @param clusterIdsToIndex ids of clusters that index should track for changes.
   * @param progressListener  listener to track task progress.
   * @param metadata          document with additional properties that can be used by index engine.
   *
   * @return a newly created index instance
   */
  public OIndex<?> createIndex(ODatabaseDocumentInternal database, final String iName, final String iType,
      final OIndexDefinition indexDefinition, final int[] clusterIdsToIndex, OProgressListener progressListener,
      ODocument metadata) {
    return createIndex(database, iName, iType, indexDefinition, clusterIdsToIndex, progressListener, metadata, null);
  }

  /**
   * Create a new index.
   * <p>
   * May require quite a long time if big amount of data should be indexed.
   *
   * @param iName             name of index
   * @param type              index type. Specified by plugged index factories.
   * @param indexDefinition   metadata that describes index structure
   * @param clusterIdsToIndex ids of clusters that index should track for changes.
   * @param progressListener  listener to track task progress.
   * @param metadata          document with additional properties that can be used by index engine.
   * @param algorithm         tip to an index factory what algorithm to use
   *
   * @return a newly created index instance
   */
  public OIndex<?> createIndex(ODatabaseDocumentInternal database, final String iName, String type,
      final OIndexDefinition indexDefinition, final int[] clusterIdsToIndex, OProgressListener progressListener, ODocument metadata,
      String algorithm) {
    if (database.getTransaction().isActive())
      throw new IllegalStateException("Cannot create a new index inside a transaction");

    final Character c = OSchemaShared.checkFieldNameIfValid(iName);
    if (c != null)
      throw new IllegalArgumentException("Invalid index name '" + iName + "'. Character '" + c + "' is invalid");

    if (indexDefinition == null) {
      throw new IllegalArgumentException("Index definition cannot be null");
    }

    final Locale locale = getServerLocale();
    type = type.toUpperCase(locale);
    if (algorithm == null) {
      algorithm = OIndexes.chooseDefaultIndexAlgorithm(type);
    }

    final String valueContainerAlgorithm = chooseContainerAlgorithm(type);

    final OIndexInternal<?> index;
    acquireExclusiveLock();
    try {

      if (indexes.containsKey(iName))
        throw new OIndexException("Index with name " + iName + " already exists.");

      // manual indexes are always durable
      if (clusterIdsToIndex == null || clusterIdsToIndex.length == 0) {
        if (metadata == null)
          metadata = new ODocument().setTrackingChanges(false);

        final Object durable = metadata.field("durableInNonTxMode");
        if (!(durable instanceof Boolean))
          metadata.field("durableInNonTxMode", true);
        if (metadata.field("trackMode") == null)
          metadata.field("trackMode", "FULL");
      }

      index = OIndexes.createIndex(getStorage(), iName, type, algorithm, valueContainerAlgorithm, metadata, -1);
      if (progressListener == null)
        // ASSIGN DEFAULT PROGRESS LISTENER
        progressListener = new OIndexRebuildOutputListener(index);

      final Set<String> clustersToIndex = findClustersByIds(clusterIdsToIndex, database);
      Object ignoreNullValues = metadata == null ? null : metadata.field("ignoreNullValues");
      if (Boolean.TRUE.equals(ignoreNullValues)) {
        indexDefinition.setNullValuesIgnored(true);
      } else if (Boolean.FALSE.equals(ignoreNullValues)) {
        indexDefinition.setNullValuesIgnored(false);
      } else {
        indexDefinition.setNullValuesIgnored(
            database.getConfiguration().getValueAsBoolean(OGlobalConfiguration.INDEX_IGNORE_NULL_VALUES_DEFAULT));
      }

      // decide which cluster to use ("index" - for automatic and "manindex" for manual)
      final String clusterName = indexDefinition.getClassName() != null ? defaultClusterName : manualClusterName;

      index.create(iName, indexDefinition, clusterName, clustersToIndex, true, progressListener);

      addIndexInternal(index);

      if (metadata != null) {
        final ODocument config = index.getConfiguration();
        config.field("metadata", metadata, OType.EMBEDDED);
      }

      setDirty();
      save();
    } finally {
      releaseExclusiveLock();
    }

    notifyInvolvedClasses(database, clusterIdsToIndex);

    return preProcessBeforeReturn(database, index);
  }

  protected void notifyInvolvedClasses(ODatabaseDocumentInternal database, int[] clusterIdsToIndex) {
    if (clusterIdsToIndex == null || clusterIdsToIndex.length == 0)
      return;

    // UPDATE INVOLVED CLASSES
    final Set<String> classes = new HashSet<>();
    for (int clusterId : clusterIdsToIndex) {
      final OClass cls = database.getMetadata().getSchema().getClassByClusterId(clusterId);
      if (cls instanceof OClassImpl && !classes.contains(cls.getName())) {
        ((OClassImpl) cls).onPostIndexManagement();
        classes.add(cls.getName());
      }
    }
  }

  private static Set<String> findClustersByIds(int[] clusterIdsToIndex, ODatabase database) {
    Set<String> clustersToIndex = new HashSet<>();
    if (clusterIdsToIndex != null) {
      for (int clusterId : clusterIdsToIndex) {
        final String clusterNameToIndex = database.getClusterNameById(clusterId);
        if (clusterNameToIndex == null)
          throw new OIndexException("Cluster with id " + clusterId + " does not exist.");

        clustersToIndex.add(clusterNameToIndex);
      }
    }
    return clustersToIndex;
  }

  private static String chooseContainerAlgorithm(String type) {
    final String valueContainerAlgorithm;
    if (OClass.INDEX_TYPE.NOTUNIQUE.toString().equals(type) || OClass.INDEX_TYPE.NOTUNIQUE_HASH_INDEX.toString().equals(type)
        || OClass.INDEX_TYPE.FULLTEXT_HASH_INDEX.toString().equals(type) || OClass.INDEX_TYPE.FULLTEXT.toString().equals(type)) {
      valueContainerAlgorithm = ODefaultIndexFactory.SBTREE_BONSAI_VALUE_CONTAINER;
    } else {
      valueContainerAlgorithm = ODefaultIndexFactory.NONE_VALUE_CONTAINER;
    }
    return valueContainerAlgorithm;
  }

  public void dropIndex(ODatabaseDocumentInternal database, final String iIndexName) {
    if (database.getTransaction().isActive())
      throw new IllegalStateException("Cannot drop an index inside a transaction");

    int[] clusterIdsToIndex = null;

    acquireExclusiveLock();

    OIndex<?> idx = null;
    try {
      idx = indexes.remove(iIndexName);
      if (idx != null) {
        final Set<String> clusters = idx.getClusters();
        if (clusters != null && !clusters.isEmpty()) {
          final ODatabaseDocumentInternal db = database;
          clusterIdsToIndex = new int[clusters.size()];
          int i = 0;
          for (String cl : clusters) {
            clusterIdsToIndex[i++] = db.getClusterIdByName(cl);
          }
        }

        removeClassPropertyIndex(idx);

        idx.delete();
        setDirty();
        save();

        notifyInvolvedClasses(database, clusterIdsToIndex);
      }
    } catch (OException e) {
      indexes.put(iIndexName, idx);
      reload();
      throw e;
    } finally {
      releaseExclusiveLock();
    }

  }

  /**
   * Binds POJO to ODocument.
   */
  @Override
  public ODocument toStream() {
    internalAcquireExclusiveLock();
    try {
      document.setInternalStatus(ORecordElement.STATUS.UNMARSHALLING);

      try {
        final OTrackedSet<ODocument> indexes = new OTrackedSet<>(document);

        for (final OIndex<?> i : this.indexes.values()) {
          indexes.add(((OIndexInternal<?>) i).updateConfiguration());
        }
        document.field(CONFIG_INDEXES, indexes, OType.EMBEDDEDSET);

      } finally {
        document.setInternalStatus(ORecordElement.STATUS.LOADED);
      }
      document.setDirty();

      return document;
    } finally {
      internalReleaseExclusiveLock();
    }
  }

  @Override
  public void recreateIndexes(ODatabaseDocumentInternal database) {
    acquireExclusiveLock();
    try {
      if (recreateIndexesThread != null && recreateIndexesThread.isAlive())
        // BUILDING ALREADY IN PROGRESS
        return;

      document = database.load(new ORecordId(database.getStorage().getConfiguration().getIndexMgrRecordId()));

      Runnable recreateIndexesTask = new RecreateIndexesTask(database.getSharedContext());
      recreateIndexesThread = new Thread(recreateIndexesTask, "OrientDB rebuild indexes");
      recreateIndexesThread.setUncaughtExceptionHandler(new OUncaughtExceptionHandler());
      recreateIndexesThread.start();
    } finally {
      releaseExclusiveLock();
    }

    if (database.getConfiguration().getValueAsBoolean(OGlobalConfiguration.INDEX_SYNCHRONOUS_AUTO_REBUILD)) {
      waitTillIndexRestore();

      database.getMetadata().reload();
    }

  }

  public void recreateIndexes() {
    throw new UnsupportedAddressTypeException();
  }

  public void waitTillIndexRestore() {
    if (recreateIndexesThread != null && recreateIndexesThread.isAlive()) {
      if (Thread.currentThread().equals(recreateIndexesThread))
        return;

      OLogManager.instance().info(this, "Wait till indexes restore after crash was finished.");
      while (recreateIndexesThread.isAlive())
        try {
          recreateIndexesThread.join();
          OLogManager.instance().info(this, "Indexes restore after crash was finished.");
        } catch (InterruptedException e) {
          OLogManager.instance().info(this, "Index rebuild task was interrupted.", e);
        }
    }
  }

  public boolean autoRecreateIndexesAfterCrash(ODatabaseDocumentInternal database) {
    if (rebuildCompleted)
      return false;

    final OStorage storage = database.getStorage();
    if (storage instanceof OAbstractPaginatedStorage) {
      OAbstractPaginatedStorage paginatedStorage = (OAbstractPaginatedStorage) storage;
      return paginatedStorage.wereDataRestoredAfterOpen() && paginatedStorage.wereNonTxOperationsPerformedInPreviousOpen();
    }

    return false;
  }

  @Override
  protected void fromStream() {
    internalAcquireExclusiveLock();
    try {
      final Map<String, OIndex<?>> oldIndexes = new HashMap<>(indexes);

      clearMetadata();
      final Collection<ODocument> indexDocuments = document.field(CONFIG_INDEXES);

      if (indexDocuments != null) {
        OIndexInternal<?> index;
        boolean configUpdated = false;
        Iterator<ODocument> indexConfigurationIterator = indexDocuments.iterator();
        while (indexConfigurationIterator.hasNext()) {
          final ODocument d = indexConfigurationIterator.next();
          try {
            final int indexVersion =
                d.field(OIndexInternal.INDEX_VERSION) == null ? 1 : (Integer) d.field(OIndexInternal.INDEX_VERSION);

            final OIndexMetadata newIndexMetadata = OIndexAbstract
                .loadMetadataInternal(d, d.field(OIndexInternal.CONFIG_TYPE), d.field(OIndexInternal.ALGORITHM),
                    d.field(OIndexInternal.VALUE_CONTAINER_ALGORITHM));

            index = OIndexes
                .createIndex(getStorage(), newIndexMetadata.getName(), newIndexMetadata.getType(), newIndexMetadata.getAlgorithm(),
                    newIndexMetadata.getValueContainerAlgorithm(), d.field(OIndexInternal.METADATA), indexVersion);

            final String normalizedName = newIndexMetadata.getName();

            OIndex<?> oldIndex = oldIndexes.remove(normalizedName);
            if (oldIndex != null) {
              OIndexMetadata oldIndexMetadata = oldIndex.getInternal().loadMetadata(oldIndex.getConfiguration());

              if (!(oldIndexMetadata.equals(newIndexMetadata) || newIndexMetadata.getIndexDefinition() == null)) {
                oldIndex.delete();
              }

              if (index.loadFromConfiguration(d)) {
                addIndexInternal(index);
              } else {
                indexConfigurationIterator.remove();
                configUpdated = true;
              }
            } else {
              if (index.loadFromConfiguration(d)) {
                addIndexInternal(index);
              } else {
                indexConfigurationIterator.remove();
                configUpdated = true;
              }
            }
          } catch (RuntimeException e) {
            indexConfigurationIterator.remove();
            configUpdated = true;
            OLogManager.instance().error(this, "Error on loading index by configuration: %s", e, d);
          }
        }

        for (OIndex<?> oldIndex : oldIndexes.values())
          try {
            OLogManager.instance().warn(this, "Index '%s' was not found after reload and will be removed", oldIndex.getName());

            oldIndex.delete();
          } catch (Exception e) {
            OLogManager.instance().error(this, "Error on deletion of index '%s'", e, oldIndex.getName());
          }

        if (configUpdated) {
          document.field(CONFIG_INDEXES, indexDocuments);
          save();
        }

      }
    } finally {
      internalReleaseExclusiveLock();
    }
  }

  public void removeClassPropertyIndex(final OIndex<?> idx) {
    acquireExclusiveLock();
    try {
      final OIndexDefinition indexDefinition = idx.getDefinition();
      if (indexDefinition == null || indexDefinition.getClassName() == null)
        return;

      final Locale locale = getServerLocale();
      Map<OMultiKey, Set<OIndex<?>>> map = classPropertyIndex.get(indexDefinition.getClassName().toLowerCase(locale));

      if (map == null) {
        return;
      }

      map = new HashMap<>(map);

      final int paramCount = indexDefinition.getParamCount();

      for (int i = 1; i <= paramCount; i++) {
        final List<String> fields = indexDefinition.getFields().subList(0, i);
        final OMultiKey multiKey = new OMultiKey(fields);

        Set<OIndex<?>> indexSet = map.get(multiKey);
        if (indexSet == null)
          continue;

        indexSet = new HashSet<>(indexSet);
        indexSet.remove(idx);

        if (indexSet.isEmpty()) {
          map.remove(multiKey);
        } else {
          map.put(multiKey, indexSet);
        }
      }

      if (map.isEmpty())
        classPropertyIndex.remove(indexDefinition.getClassName().toLowerCase(locale));
      else
        classPropertyIndex.put(indexDefinition.getClassName().toLowerCase(locale), copyPropertyMap(map));

    } finally {
      releaseExclusiveLock();
    }
  }

  public ODocument toNetworkStream() {
    ODocument document = new ODocument();
    internalAcquireExclusiveLock();
    try {
      document.setInternalStatus(ORecordElement.STATUS.UNMARSHALLING);

      try {
        final OTrackedSet<ODocument> indexes = new OTrackedSet<>(document);

        for (final OIndex<?> i : this.indexes.values()) {
          indexes.add(((OIndexInternal<?>) i).updateConfiguration().copy());
        }
        document.field(CONFIG_INDEXES, indexes, OType.EMBEDDEDSET);

      } finally {
        document.setInternalStatus(ORecordElement.STATUS.LOADED);
      }
      document.setDirty();

      return document;
    } finally {
      internalReleaseExclusiveLock();
    }

  }

  private class RecreateIndexesTask implements Runnable {
    private final OSharedContext ctx;
    private       int            ok;
    private       int            errors;

    public RecreateIndexesTask(OSharedContext ctx) {
      this.ctx = ctx;
    }

    @Override
    public void run() {
      try {
        final ODatabaseDocumentEmbedded newDb = new ODatabaseDocumentEmbedded(ctx.getStorage());
        newDb.activateOnCurrentThread();
        newDb.init(null, ctx);
        newDb.internalOpen("admin", "nopass", false);

        final Collection<ODocument> indexesToRebuild;
        acquireExclusiveLock();
        try {
          final Collection<ODocument> knownIndexes = document.field(CONFIG_INDEXES);
          if (knownIndexes == null) {
            OLogManager.instance().warn(this, "List of indexes is empty");
            indexesToRebuild = Collections.emptyList();
          } else {
            indexesToRebuild = new ArrayList<>();
            for (ODocument index : knownIndexes)
              indexesToRebuild.add(index.copy()); // make copies to safely iterate them later
          }
        } finally {
          releaseExclusiveLock();
        }

        try {
          recreateIndexes(indexesToRebuild, newDb);
        } finally {
          if (storage instanceof OAbstractPaginatedStorage) {
            final OAbstractPaginatedStorage abstractPaginatedStorage = (OAbstractPaginatedStorage) storage;
            abstractPaginatedStorage.synch();
          }
          newDb.close();
        }

      } catch (Exception e) {
        OLogManager.instance().error(this, "Error when attempt to restore indexes after crash was performed", e);
      }
    }

    private void recreateIndexes(Collection<ODocument> indexesToRebuild, ODatabaseDocumentEmbedded db) {
      ok = 0;
      errors = 0;
      for (ODocument index : indexesToRebuild) {
        try {
          recreateIndex(index, db);
        } catch (RuntimeException e) {
          OLogManager.instance().error(this, "Error during addition of index '%s'", e, index);
          errors++;
        }
      }

      db.getMetadata().getIndexManager().save();

      rebuildCompleted = true;

      OLogManager.instance().info(this, "%d indexes were restored successfully, %d errors", ok, errors);
    }

    private void recreateIndex(ODocument indexDocument, ODatabaseDocumentEmbedded db) {
      final OIndexInternal<?> index = createIndex(indexDocument);
      final OIndexMetadata indexMetadata = index.loadMetadata(indexDocument);
      final OIndexDefinition indexDefinition = indexMetadata.getIndexDefinition();

      final boolean automatic = indexDefinition != null && indexDefinition.isAutomatic();
      // XXX: At this moment Lucene-based indexes are not durable, so we still need to rebuild them.
      final boolean durable = !"LUCENE".equalsIgnoreCase(indexMetadata.getAlgorithm());

      // The database and its index manager are in a special half-open state now, the index manager is created, but not populated
      // with the index metadata, we have to rebuild the whole index list manually and insert it into the index manager.

      if (automatic) {
        if (durable) {
          OLogManager.instance().info(this, "Index '%s' is a durable automatic index and will be added as is without rebuilding",
              indexMetadata.getName());
          addIndexAsIs(indexDocument, index, db);
        } else {
          OLogManager.instance()
              .info(this, "Index '%s' is a non-durable automatic index and must be rebuilt", indexMetadata.getName());
          rebuildNonDurableAutomaticIndex(indexDocument, index, indexMetadata, indexDefinition, db);
        }
      } else {
        if (durable) {
          OLogManager.instance()
              .info(this, "Index '%s' is a durable non-automatic index and will be added as is without rebuilding",
                  indexMetadata.getName());
          addIndexAsIs(indexDocument, index, db);
        } else {
          OLogManager.instance()
              .info(this, "Index '%s' is a non-durable non-automatic index and will be added as is without rebuilding",
                  indexMetadata.getName());
          addIndexAsIs(indexDocument, index, db);
        }
      }
    }

    private void rebuildNonDurableAutomaticIndex(ODocument indexDocument, OIndexInternal<?> index, OIndexMetadata indexMetadata,
        OIndexDefinition indexDefinition, ODatabaseDocumentEmbedded database) {
      try {
        index.loadFromConfiguration(indexDocument);
        index.delete();
      } catch (Exception e) {
        OLogManager.instance()
            .error(this, "Error on removing index '%s' on rebuilding. Trying to remove index files.", e, index.getName());

        // TRY DELETING ALL THE FILES RELATIVE TO THE INDEX
        for (Iterator<OIndexFactory> it = OIndexes.getAllFactories(); it.hasNext(); ) {
          try {
            final OIndexFactory indexFactory = it.next();
            final OBaseIndexEngine engine = indexFactory
                .createIndexEngine(index.getAlgorithm(), index.getName(), false, storage, 0, 1,
                    indexDefinition.getTypes().length > 1, null);

            engine.deleteWithoutLoad(index.getName());
          } catch (Exception e2) {
            OLogManager.instance().error(this, "Error during deletion of index engine %s", e2, index.getName());
          }
        }
      }

      final String indexName = indexMetadata.getName();
      final Set<String> clusters = indexMetadata.getClustersToIndex();
      final String type = indexMetadata.getType();

      if (indexName != null && clusters != null && !clusters.isEmpty() && type != null) {
        OLogManager.instance().info(this, "Start creation of index '%s'", indexName);
        index.create(indexName, indexDefinition, defaultClusterName, clusters, false, new OIndexRebuildOutputListener(index));

        index.setRebuildingFlag();
        addIndexInternal(index);

        OLogManager.instance().info(this, "Index '%s' was successfully created and rebuild is going to be started", indexName);

        index.rebuild(new OIndexRebuildOutputListener(index));
        index.flush();

        setDirty();

        ok++;

        OLogManager.instance().info(this, "Rebuild of '%s index was successfully finished", indexName);
      } else {
        errors++;
        OLogManager.instance().error(this, "Information about index was restored incorrectly, following data were loaded : "
            + "index name '%s', index definition '%s', clusters %s, type %s", null, indexName, indexDefinition, clusters, type);
      }
    }

    private void addIndexAsIs(ODocument indexDocument, OIndexInternal<?> index, ODatabaseDocumentEmbedded database) {
      if (index.loadFromConfiguration(indexDocument)) {
        addIndexInternal(index);
        setDirty();

        ok++;
        OLogManager.instance().info(this, "Index '%s' was added in DB index list", index.getName());
      } else {
        try {
          OLogManager.instance().error(this, "Index '%s' can't be restored and will be deleted", null, index.getName());
          index.delete();
        } catch (Exception e) {
          OLogManager.instance().error(this, "Error while deleting index '%s'", e, index.getName());
        }
        errors++;
      }
    }

    private OIndexInternal<?> createIndex(ODocument idx) {
      final String indexName = idx.field(OIndexInternal.CONFIG_NAME);
      final String indexType = idx.field(OIndexInternal.CONFIG_TYPE);
      String algorithm = idx.field(OIndexInternal.ALGORITHM);
      String valueContainerAlgorithm = idx.field(OIndexInternal.VALUE_CONTAINER_ALGORITHM);

      ODocument metadata = idx.field(OIndexInternal.METADATA);
      if (indexType == null) {
        OLogManager.instance().error(this, "Index type is null, will process other record", null);
        throw new OIndexException("Index type is null, will process other record. Index configuration: " + idx.toString());
      }

      return OIndexes.createIndex(storage, indexName, indexType, algorithm, valueContainerAlgorithm, metadata, -1);
    }
  }

  public OIndex<?> preProcessBeforeReturn(ODatabaseDocumentInternal database, final OIndex<?> index) {
    if (index instanceof OIndexMultiValues)
      //noinspection unchecked
      return new OIndexTxAwareMultiValue(database, (OIndex<Collection<OIdentifiable>>) index);
    else if (index instanceof OIndexDictionary)
      //noinspection unchecked
      return new OIndexTxAwareDictionary(database, (OIndex<OIdentifiable>) index);
    else if (index instanceof OIndexOneValue)
      //noinspection unchecked
      return new OIndexTxAwareOneValue(database, (OIndex<OIdentifiable>) index);

    return index;
  }

  protected OStorage getStorage() {
    return storage;
  }
}


/*
 *
 *  *  Copyright 2016 Orient Technologies LTD (info(at)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://www.orientdb.com
 *
 */
package com.orientechnologies.orient.server.distributed;

import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ODocumentInternal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

/**
 * Modifiable Distributed configuration. It's created starting from a ODistributedConfiguration object. Every changes increment the
 * field "version".
 *
 * @author Luca Garulli (l.garulli--at--orientdb.com)
 */
public class OModifiableDistributedConfiguration extends ODistributedConfiguration {
  public OModifiableDistributedConfiguration(final ODocument iConfiguration) {
    super(iConfiguration);
  }

  public OModifiableDistributedConfiguration modify() {
    return this;
  }

  /**
   * Sets the server role between MASTER (default) and REPLICA.
   */
  public void setServerRole(final String iServerName, final ROLES role) {
    synchronized (configuration) {
      ODocument servers = configuration.field(SERVERS);
      if (servers == null) {
        servers = new ODocument();
        configuration.field(SERVERS, servers, OType.EMBEDDED);
      }

      servers.field(iServerName, role);
      incrementVersion();
    }
  }

  /**
   * Adds a server in the configuration. It replaces all the tags <NEW_NODE> with the new server name

   * NOTE: It must be executed in distributed database lock.
   *
   * @param iNode
   *          Server name
   * @return the list of changed partitions or null if no changes have applied
   */
  public List<String> addNewNodeInServerList(final String iNode) {
    synchronized (configuration) {
      final List<String> changedPartitions = new ArrayList<String>();
      // ADD THE NODE IN CONFIGURATION. LOOK FOR $newNode TAG
      for (String clusterName : getClusterNames()) {
        final List<String> partitions = getClusterConfiguration(clusterName).field(SERVERS);
        if (partitions != null) {
          final int newNodePos = partitions.indexOf(OModifiableDistributedConfiguration.NEW_NODE_TAG);
          if (newNodePos > -1 && !partitions.contains(iNode)) {
            partitions.add(newNodePos, iNode);
            changedPartitions.add(clusterName);
          }
        }
      }

      if (!changedPartitions.isEmpty()) {
        // INCREMENT VERSION
        incrementVersion();

        if (!getRegisteredServers().contains(iNode)) {
          if (getNewNodeStrategy() == NEW_NODE_STRATEGIES.STATIC) {
            // REGISTER THE SERVER AS STATIC AND INCREMENT VERSION
            setServerRole(iNode, getServerRole("*"));
          }
        }
        return changedPartitions;
      }
    }
    return null;
  }

  /**
   * Sets the server as owner for the given cluster. The owner server is the first in server list.

   * NOTE: It must be executed in distributed database lock.
   *
   * @param iClusterName
   *          Cluster name or *. Does not accept null.
   */
  public void setServerOwner(final String iClusterName, final String iServerName) {
    if (iClusterName == null)
      throw new IllegalArgumentException("cluster name cannot be null");

    synchronized (configuration) {
      final ODocument clusters = configuration.field(CLUSTERS);
      ODocument cluster = clusters.field(iClusterName);

      if (cluster == null)
        // CREATE IT
        cluster = createCluster(iClusterName);
      else {
        // CHECK IF THE OWNER IS ALREADY CONFIGURED
        final String owner = cluster.field(OWNER);
        if (owner != null && !iServerName.equalsIgnoreCase(owner))
          throw new ODistributedException("Cannot overwrite ownership of cluster '" + iClusterName + "' to the server '"
              + iServerName + "', because server '" + owner + "' was already configured as owner");
      }

      List<String> serverList = getClusterConfiguration(iClusterName).field(SERVERS);
      if (serverList == null) {
        serverList = initClusterServers(cluster);
      }

      if (!serverList.isEmpty() && serverList.get(0).equals(iServerName))
        // ALREADY OWNER
        return;

      // REMOVE THE NODE IF ANY
      boolean removed = false;
      for (Iterator<String> it = serverList.iterator(); it.hasNext();) {
        if (it.next().equals(iServerName)) {
          it.remove();
          removed = true;
          break;
        }
      }

      if( !removed )
        throw new ODistributedException("Cannot set ownership of cluster '" + iClusterName + "' to the server '"
            + iServerName + "', because the server has no that cluster (sharding)");

      // ADD THE NODE AS FIRST OF THE LIST = MASTER
      serverList.add(0, iServerName);

      incrementVersion();
    }
  }

  /**
   * Removes a server from the list.

   * NOTE: It must be executed in distributed database lock.
   *
   * @param iNode
   *          Server name
   * @return
   */
  public List<String> removeServer(final String iNode) {
    synchronized (configuration) {
      final List<String> changedPartitions = new ArrayList<String>();

      for (String clusterName : getClusterNames()) {
        final Collection<String> nodes = getClusterConfiguration(clusterName).field(SERVERS);
        if (nodes != null) {
          for (String node : nodes) {
            if (node.equals(iNode)) {
              // FOUND: REMOVE IT
              nodes.remove(node);
              changedPartitions.add(clusterName);
              break;
            }
          }
        }
      }

      if (!changedPartitions.isEmpty()) {
        incrementVersion();
        return changedPartitions;
      }
    }
    return null;
  }

  /**
   * Set a server offline. It assures the offline server is never on top of the list.

   * NOTE: It must be executed in distributed database lock.
   *
   * @param iNode
   *          Server name
   * @param newLockManagerServer
   *          New Lock Manager server name
   * @return
   */
  public List<String> setServerOffline(final String iNode, final String newLockManagerServer) {
    final List<String> changedPartitions = new ArrayList<String>();

    final String[] clusters = getClusterNames();
    synchronized (configuration) {
      for (String clusterName : clusters) {
        final List<String> nodes = getClusterConfiguration(clusterName).field(SERVERS);
        if (nodes != null && nodes.size() > 1) {
          for (String node : nodes) {
            if (node.equals(iNode)) {
              // FOUND: PUT THE NODE AT THE END (BEFORE ANY TAG <NEW_NODE>)
              nodes.remove(node);

              final boolean newNodeRemoved = nodes.remove(NEW_NODE_TAG);

              nodes.add(node);

              if (newNodeRemoved)
                // REINSERT NEW NODE TAG AT THE END
                nodes.add(NEW_NODE_TAG);

              if (newLockManagerServer != null) {
                // ASSURE THE NEW LOCK MANAGER IS THE FIRST IN THE LIST
                if (nodes.remove(newLockManagerServer))
                  nodes.add(0, newLockManagerServer);
              }

              changedPartitions.add(clusterName);
              break;
            }
          }
        }
      }

      if (!changedPartitions.isEmpty()) {
        incrementVersion();
        return changedPartitions;
      }
    }
    return null;
  }

  private void incrementVersion() {
    // INCREMENT VERSION
    Integer oldVersion = configuration.field(VERSION);
    if (oldVersion == null)
      oldVersion = 0;
    configuration.field(VERSION, oldVersion.intValue() + 1);
  }

  private List<String> initClusterServers(final ODocument cluster) {
    final ODocument any = getClusterConfiguration(ALL_WILDCARD);

    // COPY THE SERVER LIST FROM ALL_WILDCARD
    final List<String> anyServers = any.field(SERVERS);
    final List<String> servers = new ArrayList<String>(anyServers);
    cluster.field(SERVERS, servers);

    return servers;
  }

  private ODocument createCluster(final String iClusterName) {
    // CREATE IT
    final ODocument clusters = configuration.field(CLUSTERS);

    ODocument cluster = clusters.field(iClusterName);
    if (cluster != null)
      // ALREADY EXISTS
      return clusters;

    cluster = new ODocument();
    ODocumentInternal.addOwner(cluster, clusters);
    clusters.field(iClusterName, cluster, OType.EMBEDDED);

    final List<String> servers = initClusterServers(cluster);

    return cluster;
  }

  public void override(final ODocument newCfg) {
    configuration.fromStream(newCfg.toStream());
    incrementVersion();
  }
}


/*
 *
 *  *  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.client.remote;

import com.orientechnologies.orient.core.config.OStorageClusterConfiguration;
import com.orientechnologies.orient.core.conflict.ORecordConflictStrategy;
import com.orientechnologies.orient.core.exception.ORecordNotFoundException;
import com.orientechnologies.orient.core.storage.OCluster;
import com.orientechnologies.orient.core.storage.OPhysicalPosition;
import com.orientechnologies.orient.core.storage.ORawBuffer;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.storage.impl.local.OClusterBrowsePage;

import java.io.IOException;

/**
 * Remote cluster implementation
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class OClusterRemote implements OCluster {
  private String name;
  private int    id;

  /*
   * (non-Javadoc)
   *
   * @see com.orientechnologies.orient.core.storage.OCluster#configure(com.orientechnologies.orient.core.storage.OStorage, int,
   * java.lang.String, java.lang.String, int, java.lang.Object[])
   */
  public void configure(OStorage iStorage, int iId, String iClusterName, Object... iParameters) {
    id = iId;
    name = iClusterName;
  }

  /*
   * (non-Javadoc)
   *
   * @see com.orientechnologies.orient.core.storage.OCluster#configure(com.orientechnologies.orient.core.storage.OStorage,
   * com.orientechnologies.orient.core.config.OStorageClusterConfiguration)
   */
  public void configure(OStorage iStorage, OStorageClusterConfiguration iConfig) throws IOException {
    id = iConfig.getId();
    name = iConfig.getName();
  }

  /*
   * (non-Javadoc)
   *
   * @see com.orientechnologies.orient.core.storage.OCluster#create(int)
   */
  public void create(int iStartSize) throws IOException {

  }

  /*
   * (non-Javadoc)
   *
   * @see com.orientechnologies.orient.core.storage.OCluster#open()
   */
  public void open() throws IOException {
  }

  public void close() throws IOException {
  }

  @Override
  public void close(boolean flush) throws IOException {
  }

  @Override
  public OPhysicalPosition allocatePosition(byte recordType) throws IOException {
    throw new UnsupportedOperationException("allocatePosition");
  }

  @Override
  public OPhysicalPosition createRecord(byte[] content, int recordVersion, byte recordType, OPhysicalPosition allocatedPosition)
      throws IOException {
    throw new UnsupportedOperationException("createRecord");
  }

  @Override
  public boolean deleteRecord(long clusterPosition) throws IOException {
    throw new UnsupportedOperationException("deleteRecord");
  }

  @Override
  public void updateRecord(long clusterPosition, byte[] content, int recordVersion, byte recordType) throws IOException {
    throw new UnsupportedOperationException("updateRecord");
  }

  @Override
  public void recycleRecord(long clusterPosition, byte[] content, int recordVersion, byte recordType) throws IOException {
    throw new UnsupportedOperationException("recyclePosition");
  }

  @Override
  public ORawBuffer readRecord(long clusterPosition, boolean prefetchRecords) throws IOException {
    throw new UnsupportedOperationException("readRecord");
  }

  @Override
  public ORawBuffer readRecordIfVersionIsNotLatest(long clusterPosition, int recordVersion)
      throws IOException, ORecordNotFoundException {
    throw new UnsupportedOperationException("readRecordIfVersionIsNotLatest");
  }

  @Override
  public boolean exists() {
    throw new UnsupportedOperationException("exists");
  }

  public void delete() throws IOException {
  }

  public Object set(ATTRIBUTES iAttribute, Object iValue) throws IOException {
    return null;
  }

  @Override
  public String encryption() {
    throw new UnsupportedOperationException("encryption");
  }

  public void truncate() throws IOException {
  }

  public OPhysicalPosition getPhysicalPosition(OPhysicalPosition iPPosition) throws IOException {
    return null;
  }

  public long getEntries() {
    return 0;
  }

  @Override
  public long getTombstonesCount() {
    throw new UnsupportedOperationException("getTombstonesCount()");
  }

  @Override
  public long getFirstPosition() {
    return 0;
  }

  @Override
  public long getLastPosition() {
    return 0;
  }

  @Override
  public long getNextPosition() throws IOException {
    return 0;
  }

  @Override
  public String getFileName() {
    throw new UnsupportedOperationException("getFileName()");
  }

  public int getId() {
    return id;
  }

  public void synch() throws IOException {
  }

  public String getName() {
    return name;
  }

  public long getRecordsSize() {
    throw new UnsupportedOperationException("getRecordsSize()");
  }

  @Override
  public boolean isSystemCluster() {
    return false;
  }

  @Override
  public OPhysicalPosition[] higherPositions(OPhysicalPosition position) {
    throw new UnsupportedOperationException("higherPositions()");
  }

  @Override
  public OPhysicalPosition[] lowerPositions(OPhysicalPosition position) {
    throw new UnsupportedOperationException("lowerPositions()");
  }

  @Override
  public OPhysicalPosition[] ceilingPositions(OPhysicalPosition position) throws IOException {
    throw new UnsupportedOperationException("ceilingPositions()");
  }

  @Override
  public OPhysicalPosition[] floorPositions(OPhysicalPosition position) throws IOException {
    throw new UnsupportedOperationException("floorPositions()");
  }

  @Override
  public boolean isDeleted(OPhysicalPosition iPPosition) throws IOException {
    throw new UnsupportedOperationException("isDeleted()");
  }

  @Override
  public float recordGrowFactor() {
    throw new UnsupportedOperationException("recordGrowFactor()");
  }

  @Override
  public float recordOverflowGrowFactor() {
    throw new UnsupportedOperationException("recordOverflowGrowFactor()");
  }

  @Override
  public String compression() {
    throw new UnsupportedOperationException("compression()");
  }

  @Override
  public boolean hideRecord(long position) {
    throw new UnsupportedOperationException("Operation is not supported for given cluster implementation");
  }

  @Override
  public ORecordConflictStrategy getRecordConflictStrategy() {
    return null;
  }

  @Override
  public void acquireAtomicExclusiveLock() {
    throw new UnsupportedOperationException("remote cluster doesn't support atomic locking");
  }

  @Override
  public OClusterBrowsePage nextPage(long lastPosition) {
    throw new UnsupportedOperationException();
  }

  @Override
  public int getBinaryVersion() {
    throw new UnsupportedOperationException("Operation is not supported for given cluster implementation");
  }
}


/*
 *
 *  *  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.record;

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.io.OIOUtils;
import com.orientechnologies.orient.core.db.ODatabase;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.exception.ORecordNotFoundException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.record.impl.ODirtyManager;
import com.orientechnologies.orient.core.serialization.serializer.record.ORecordSerializer;
import com.orientechnologies.orient.core.serialization.serializer.record.string.ORecordSerializerJSON;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.storage.cluster.OOfflineClusterException;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;

@SuppressWarnings({ "unchecked", "serial" })
public abstract class ORecordAbstract implements ORecord {
  protected ORecordId _recordId;
  protected int _recordVersion = 0;

  protected byte[] _source;
  protected int    _size;

  protected transient ORecordSerializer _recordFormat;
  protected           boolean               _dirty          = true;
  protected           boolean               _contentChanged = true;
  protected           ORecordElement.STATUS _status         = ORecordElement.STATUS.LOADED;
  protected transient Set<ORecordListener>  _listeners      = null;

  private transient Set<OIdentityChangeListener> newIdentityChangeListeners = null;
  protected ODirtyManager _dirtyManager;

  public ORecordAbstract() {
  }

  public ORecordAbstract(final byte[] iSource) {
    _source = iSource;
    _size = iSource.length;
    unsetDirty();
  }

  public ORID getIdentity() {
    return _recordId;
  }

  protected ORecordAbstract setIdentity(final ORecordId iIdentity) {
    _recordId = iIdentity;
    return this;
  }

  @Override
  public ORecordElement getOwner() {
    return null;
  }

  public ORecord getRecord() {
    return this;
  }

  public boolean detach() {
    return true;
  }

  public ORecordAbstract clear() {
    setDirty();
    invokeListenerEvent(ORecordListener.EVENT.CLEAR);
    return this;
  }

  public ORecordAbstract reset() {
    _status = ORecordElement.STATUS.LOADED;
    _recordVersion = 0;
    _size = 0;

    _source = null;
    setDirty();
    if (_recordId != null)
      _recordId.reset();

    invokeListenerEvent(ORecordListener.EVENT.RESET);

    return this;
  }

  public byte[] toStream() {
    if (_source == null)
      _source = _recordFormat.toStream(this, false);

    invokeListenerEvent(ORecordListener.EVENT.MARSHALL);

    return _source;
  }

  public ORecordAbstract fromStream(final byte[] iRecordBuffer) {
    _dirty = false;
    _contentChanged = false;
    _dirtyManager = null;
    _source = iRecordBuffer;
    _size = iRecordBuffer != null ? iRecordBuffer.length : 0;
    _status = ORecordElement.STATUS.LOADED;

    invokeListenerEvent(ORecordListener.EVENT.UNMARSHALL);

    return this;
  }

  public ORecordAbstract setDirty() {
    if (!_dirty && _status != STATUS.UNMARSHALLING) {
      _dirty = true;
      _source = null;
    }

    _contentChanged = true;
    return this;
  }

  @Override
  public void setDirtyNoChanged() {
    if (!_dirty && _status != STATUS.UNMARSHALLING) {
      _dirty = true;
      _source = null;
    }
  }

  public boolean isDirty() {
    return _dirty;
  }

  public <RET extends ORecord> RET fromJSON(final String iSource, final String iOptions) {
    // ORecordSerializerJSON.INSTANCE.fromString(iSource, this, null, iOptions);
    ORecordSerializerJSON.INSTANCE.fromString(iSource, this, null, iOptions, false); // Add new parameter to accommodate new API,
    // nothing change
    return (RET) this;
  }

  public <RET extends ORecord> RET fromJSON(final String iSource) {
    ORecordSerializerJSON.INSTANCE.fromString(iSource, this, null);
    return (RET) this;
  }

  // Add New API to load record if rid exist
  public <RET extends ORecord> RET fromJSON(final String iSource, boolean needReload) {
    return (RET) ORecordSerializerJSON.INSTANCE.fromString(iSource, this, null, needReload);
  }

  public <RET extends ORecord> RET fromJSON(final InputStream iContentResult) throws IOException {
    final ByteArrayOutputStream out = new ByteArrayOutputStream();
    OIOUtils.copyStream(iContentResult, out, -1);
    ORecordSerializerJSON.INSTANCE.fromString(out.toString(), this, null);
    return (RET) this;
  }

  public String toJSON() {
    return toJSON("rid,version,class,type,attribSameRow,keepTypes,alwaysFetchEmbedded,fetchPlan:*:0");
  }

  public String toJSON(final String iFormat) {
    return ORecordSerializerJSON.INSTANCE.toString(this, new StringBuilder(1024), iFormat == null ? "" : iFormat).toString();
  }

  public void toJSON(final String iFormat, final OutputStream stream) throws IOException {
    stream.write(toJSON(iFormat).toString().getBytes());
  }

  public void toJSON(final OutputStream stream) throws IOException {
    stream.write(toJSON().toString().getBytes());
  }

  @Override
  public String toString() {
    return (_recordId.isValid() ? _recordId : "") + (_source != null ? Arrays.toString(_source) : "[]") + " v" + _recordVersion;
  }

  public int getVersion() {
    // checkForLoading();
    return _recordVersion;
  }

  protected void setVersion(final int iVersion) {
    _recordVersion = iVersion;
  }

  public ORecordAbstract unload() {
    _status = ORecordElement.STATUS.NOT_LOADED;
    _source = null;
    unsetDirty();
    invokeListenerEvent(ORecordListener.EVENT.UNLOAD);
    return this;
  }

  public ORecord load() {
    if (!getIdentity().isValid())
      throw new ORecordNotFoundException(getIdentity(), "The record has no id, probably it's new or transient yet ");

    final ORecord result = getDatabase().load(this);

    if (result == null)
      throw new ORecordNotFoundException(getIdentity());

    return result;
  }

  public ODatabaseDocumentInternal getDatabase() {
    return ODatabaseRecordThreadLocal.instance().get();
  }

  public ODatabaseDocumentInternal getDatabaseIfDefined() {
    return ODatabaseRecordThreadLocal.instance().getIfDefined();
  }

  public ORecord reload() {
    return reload(null, true, true);
  }

  public ORecord reload(final String fetchPlan) {
    return reload(fetchPlan, true);
  }

  public ORecord reload(final String fetchPlan, final boolean ignoreCache) {
    return reload(fetchPlan, ignoreCache, true);
  }

  @Override
  public ORecord reload(String fetchPlan, boolean ignoreCache, boolean force) throws ORecordNotFoundException {
    if (!getIdentity().isValid())
      throw new ORecordNotFoundException(getIdentity(), "The record has no id. It is probably new or still transient");

    try {
      getDatabase().reload(this, fetchPlan, ignoreCache, force);

      return this;

    } catch (OOfflineClusterException e) {
      throw e;
    } catch (ORecordNotFoundException e) {
      throw e;
    } catch (Exception e) {
      throw OException.wrapException(new ORecordNotFoundException(getIdentity()), e);
    }
  }

  public ORecordAbstract save() {
    return save(false);
  }

  public ORecordAbstract save(final String iClusterName) {
    return save(iClusterName, false);
  }

  public ORecordAbstract save(boolean forceCreate) {
    getDatabase().save(this, ODatabase.OPERATION_MODE.SYNCHRONOUS, forceCreate, null, null);
    return this;
  }

  public ORecordAbstract save(String iClusterName, boolean forceCreate) {
    return getDatabase().save(this, iClusterName, ODatabase.OPERATION_MODE.SYNCHRONOUS, forceCreate, null, null);
  }

  public ORecordAbstract delete() {
    getDatabase().delete(this);
    return this;
  }

  public int getSize() {
    return _size;
  }

  @Override
  public void lock(final boolean iExclusive) {
    ODatabaseRecordThreadLocal.instance().get().getTransaction()
        .lockRecord(this, iExclusive ? OStorage.LOCKING_STRATEGY.EXCLUSIVE_LOCK : OStorage.LOCKING_STRATEGY.SHARED_LOCK);
  }

  @Override
  public boolean isLocked() {
    return ODatabaseRecordThreadLocal.instance().get().getTransaction().isLockedRecord(this);
  }

  @Override
  public OStorage.LOCKING_STRATEGY lockingStrategy() {
    return ODatabaseRecordThreadLocal.instance().get().getTransaction().lockingStrategy(this);
  }

  @Override
  public void unlock() {
    ODatabaseRecordThreadLocal.instance().get().getTransaction().unlockRecord(this);
  }

  @Override
  public int hashCode() {
    return _recordId != null ? _recordId.hashCode() : 0;
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;

    if (obj instanceof OIdentifiable)
      return _recordId.equals(((OIdentifiable) obj).getIdentity());

    return false;
  }

  public int compare(final OIdentifiable iFirst, final OIdentifiable iSecond) {
    if (iFirst == null || iSecond == null)
      return -1;
    return iFirst.compareTo(iSecond);
  }

  public int compareTo(final OIdentifiable iOther) {
    if (iOther == null)
      return 1;

    if (_recordId == null)
      return iOther.getIdentity() == null ? 0 : 1;

    return _recordId.compareTo(iOther.getIdentity());
  }

  public ORecordElement.STATUS getInternalStatus() {
    return _status;
  }

  public void setInternalStatus(final ORecordElement.STATUS iStatus) {
    this._status = iStatus;
  }

  public ORecordAbstract copyTo(final ORecordAbstract cloned) {
    cloned._source = _source;
    cloned._size = _size;
    cloned._recordId = _recordId.copy();
    cloned._recordVersion = _recordVersion;
    cloned._status = _status;
    cloned._recordFormat = _recordFormat;
    cloned._listeners = null;
    cloned._dirty = false;
    cloned._contentChanged = false;
    cloned._dirtyManager = null;
    return cloned;
  }

  protected ORecordAbstract fill(final ORID iRid, final int iVersion, final byte[] iBuffer, boolean iDirty) {
    _recordId.setClusterId(iRid.getClusterId());
    _recordId.setClusterPosition(iRid.getClusterPosition());
    _recordVersion = iVersion;
    _status = ORecordElement.STATUS.LOADED;
    _source = iBuffer;
    _size = iBuffer != null ? iBuffer.length : 0;
    if (_source != null && _source.length > 0) {
      _dirty = iDirty;
      _contentChanged = iDirty;
      if (!iDirty && _dirtyManager != null)
        _dirtyManager.removePointed(this);
    }

    return this;
  }

  protected ORecordAbstract fill(final ORID iRid, final int iVersion, final byte[] iBuffer, boolean iDirty, ODatabaseDocumentInternal db) {
    _recordId.setClusterId(iRid.getClusterId());
    _recordId.setClusterPosition(iRid.getClusterPosition());
    _recordVersion = iVersion;
    _status = ORecordElement.STATUS.LOADED;
    _source = iBuffer;
    _size = iBuffer != null ? iBuffer.length : 0;
    if (_source != null && _source.length > 0) {
      _dirty = iDirty;
      _contentChanged = iDirty;
      if (!iDirty && _dirtyManager != null)
        _dirtyManager.removePointed(this);
    }

    return this;
  }

  protected ORecordAbstract setIdentity(final int iClusterId, final long iClusterPosition) {
    if (_recordId == null || _recordId == ORecordId.EMPTY_RECORD_ID)
      _recordId = new ORecordId(iClusterId, iClusterPosition);
    else {
      _recordId.setClusterId(iClusterId);
      _recordId.setClusterPosition(iClusterPosition);
    }
    return this;
  }

  protected void unsetDirty() {
    _contentChanged = false;
    _dirty = false;
    if (_dirtyManager != null)
      _dirtyManager.removePointed(this);
  }

  protected abstract byte getRecordType();

  protected void onBeforeIdentityChanged(final ORecord iRecord) {
    if (newIdentityChangeListeners != null) {
      for (OIdentityChangeListener changeListener : newIdentityChangeListeners)
        changeListener.onBeforeIdentityChange(this);
    }
  }

  protected void onAfterIdentityChanged(final ORecord iRecord) {
    invokeListenerEvent(ORecordListener.EVENT.IDENTITY_CHANGED);

    if (newIdentityChangeListeners != null) {
      for (OIdentityChangeListener changeListener : newIdentityChangeListeners)
        changeListener.onAfterIdentityChange(this);
    }

  }

  protected ODatabaseDocumentInternal getDatabaseInternal() {
    return ODatabaseRecordThreadLocal.instance().get();
  }

  protected ODatabaseDocumentInternal getDatabaseIfDefinedInternal() {
    return ODatabaseRecordThreadLocal.instance().getIfDefined();
  }

  /**
   * Add a listener to the current document to catch all the supported events.
   *
   * @param iListener ODocumentListener implementation
   *
   * @see ORecordListener ju
   */
  protected void addListener(final ORecordListener iListener) {
    if (_listeners == null)
      _listeners = Collections.newSetFromMap(new WeakHashMap<ORecordListener, Boolean>());

    _listeners.add(iListener);
  }

  /**
   * Remove the current event listener.
   *
   * @see ORecordListener
   */
  protected void removeListener(final ORecordListener listener) {
    if (_listeners != null) {
      _listeners.remove(listener);
      if (_listeners.isEmpty())
        _listeners = null;
    }
  }

  protected <RET extends ORecord> RET flatCopy() {
    return (RET) copy();
  }

  protected void addIdentityChangeListener(OIdentityChangeListener identityChangeListener) {
    if (newIdentityChangeListeners == null)
      newIdentityChangeListeners = Collections.newSetFromMap(new WeakHashMap<OIdentityChangeListener, Boolean>());
    newIdentityChangeListeners.add(identityChangeListener);
  }

  protected void removeIdentityChangeListener(OIdentityChangeListener identityChangeListener) {
    if (newIdentityChangeListeners != null)
      newIdentityChangeListeners.remove(identityChangeListener);
  }

  protected void setup() {
    if (_recordId == null)
      _recordId = new ORecordId();
  }

  protected void invokeListenerEvent(final ORecordListener.EVENT iEvent) {
    if (_listeners != null)
      for (final ORecordListener listener : _listeners)
        if (listener != null)
          listener.onEvent(this, iEvent);
  }

  protected void checkForLoading() {
    if (_status == ORecordElement.STATUS.NOT_LOADED && ODatabaseRecordThreadLocal.instance().isDefined())
      reload(null, true);
  }

  protected boolean isContentChanged() {
    return _contentChanged;
  }

  protected void setContentChanged(boolean contentChanged) {
    _contentChanged = contentChanged;
  }

  protected void clearSource() {
    this._source = null;
  }

  protected ODirtyManager getDirtyManager() {
    if (this._dirtyManager == null) {
      this._dirtyManager = new ODirtyManager();
      if (this.getIdentity().isNew() && getOwner() == null)
        this._dirtyManager.setDirty(this);
    }
    return this._dirtyManager;
  }

  protected void setDirtyManager(ODirtyManager dirtyManager) {
    if (this._dirtyManager != null && dirtyManager != null) {
      dirtyManager.merge(this._dirtyManager);
    }
    this._dirtyManager = dirtyManager;
    if (this.getIdentity().isNew() && getOwner() == null && this._dirtyManager != null)
      this._dirtyManager.setDirty(this);
  }

  protected void track(OIdentifiable id) {
    this.getDirtyManager().track(this, id);
  }

  protected void unTrack(OIdentifiable id) {
    this.getDirtyManager().unTrack(this, id);
  }

}


/*
 *
 *  *  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.server.distributed.impl;

import com.orientechnologies.common.profiler.OProfilerEntry;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.server.OSystemDatabase;
import com.orientechnologies.orient.server.distributed.*;
import com.orientechnologies.orient.server.distributed.ODistributedServerLog.DIRECTION;
import com.orientechnologies.orient.server.hazelcast.OHazelcastPlugin;

import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Hazelcast implementation of distributed peer. There is one instance per database. Each node creates own instance to talk with
 * each others.
 *
 * @author Luca Garulli (l.garulli--at--orientdb.com)
 */
public class ODistributedMessageServiceImpl implements ODistributedMessageService {

  private final    OHazelcastPlugin                                     manager;
  private final    ConcurrentHashMap<Long, ODistributedResponseManager> responsesByRequestIds;
  private final    TimerTask                                            asynchMessageManager;
  final            ConcurrentHashMap<String, ODistributedDatabaseImpl>  databases           = new ConcurrentHashMap<String, ODistributedDatabaseImpl>();
  private          Thread                                               responseThread;
  private          long[]                                               responseTimeMetrics = new long[10];
  private volatile boolean                                              running             = true;
  private final    Map<String, OProfilerEntry>                          latencies           = new HashMap<String, OProfilerEntry>();
  private final    Map<String, AtomicLong>                              messagesStats       = new HashMap<String, AtomicLong>();

  public ODistributedMessageServiceImpl(final OHazelcastPlugin manager) {
    this.manager = manager;
    this.responsesByRequestIds = new ConcurrentHashMap<Long, ODistributedResponseManager>();

    // RESET ALL THE METRICS
    for (int i = 0; i < responseTimeMetrics.length; ++i)
      responseTimeMetrics[i] = -1;

    // CREATE TASK THAT CHECK ASYNCHRONOUS MESSAGE RECEIVED
    asynchMessageManager = new TimerTask() {
      @Override
      public void run() {
        purgePendingMessages();
      }
    };
  }

  public ODistributedDatabaseImpl getDatabase(final String iDatabaseName) {
    if (databases != null)
      return databases.get(iDatabaseName);

    // NOT INITIALIZED YET
    return null;
  }

  public void shutdown() {
    running = false;

    if (responseThread != null) {
      responseThread.interrupt();
      responseThread = null;
    }

    // SET ALL DATABASES TO NOT_AVAILABLE
    for (Entry<String, ODistributedDatabaseImpl> m : databases.entrySet()) {
      if (OSystemDatabase.SYSTEM_DB_NAME.equals(m.getKey()))
        continue;

      try {
        manager.setDatabaseStatus(manager.getLocalNodeName(), m.getKey(), ODistributedServerManager.DB_STATUS.NOT_AVAILABLE);
      } catch (Exception t) {
        // IGNORE IT
      }
      m.getValue().shutdown();
    }
    databases.clear();

    asynchMessageManager.cancel();

    // CANCEL ALL THE PENDING REQUESTS
    for (ODistributedResponseManager req : responsesByRequestIds.values())
      req.cancel();

    responsesByRequestIds.clear();

    latencies.clear();
    messagesStats.clear();
  }

  @Override
  public ODistributedResponseManager getResponseManager(final ODistributedRequestId reqId) {
    return responsesByRequestIds.get(reqId.getMessageId());
  }

  public void registerRequest(final long id, final ODistributedResponseManager currentResponseMgr) {
    responsesByRequestIds.put(id, currentResponseMgr);
  }

  public void handleUnreachableNode(final String nodeName) {
    // WAKE UP ALL THE WAITING RESPONSES
    for (ODistributedResponseManager r : responsesByRequestIds.values())
      r.removeServerBecauseUnreachable(nodeName);
  }

  public long getAverageResponseTime() {
    long total = 0;
    int involved = 0;
    for (long metric : responseTimeMetrics) {
      if (metric > -1) {
        total += metric;
        involved++;
      }
    }
    return total > 0 ? total / involved : 0;
  }

  /**
   * Creates a distributed database instance if not defined yet.
   */
  public ODistributedDatabaseImpl registerDatabase(final String iDatabaseName, ODistributedConfiguration cfg) {
    final ODistributedDatabaseImpl ddb = databases.get(iDatabaseName);
    if (ddb != null)
      return ddb;

    return new ODistributedDatabaseImpl(manager, this, iDatabaseName, cfg, manager.getServerInstance());
  }

  public ODistributedDatabaseImpl unregisterDatabase(final String iDatabaseName) {
    try {
      manager.setDatabaseStatus(manager.getLocalNodeName(), iDatabaseName, ODistributedServerManager.DB_STATUS.OFFLINE);
    } catch (Exception t) {
      ODistributedServerLog.warn(this, manager.getLocalNodeName(), null, null, "error un-registering database", t);
      // IGNORE IT
    }

    final ODistributedDatabaseImpl db = databases.remove(iDatabaseName);
    if (db != null) {
      db.shutdown();
    }
    return db;
  }

  @Override
  public Set<String> getDatabases() {
    // We assign the ConcurrentHashMap (databases) to the Map interface for this reason:
    // ConcurrentHashMap.keySet() in Java 8 returns a ConcurrentHashMap.KeySetView.
    // ConcurrentHashMap.keySet() in Java 7 returns a Set.
    // If this code is compiled with Java 8 yet is run on Java 7, you'll receive a NoSuchMethodError:
    // java.util.concurrent.ConcurrentHashMap.keySet()Ljava/util/concurrent/ConcurrentHashMap$KeySetView.
    // By assigning the ConcurrentHashMap variable to a Map, the call to keySet() will return a Set
    // and not the Java 8 type, KeySetView.
    Map<String, ODistributedDatabaseImpl> map = databases;

    final Set<String> result = new HashSet<String>(map.keySet());
    result.remove(OSystemDatabase.SYSTEM_DB_NAME);
    return result;
  }

  /**
   * Not synchronized, it's called when a message arrives
   */
  public void dispatchResponseToThread(final ODistributedResponse response) {
    try {
      final long msgId = response.getRequestId().getMessageId();

      // GET ASYNCHRONOUS MSG MANAGER IF ANY
      final ODistributedResponseManager asynchMgr = responsesByRequestIds.get(msgId);
      if (asynchMgr == null) {
        if (ODistributedServerLog.isDebugEnabled())
          ODistributedServerLog.debug(this, manager.getLocalNodeName(), response.getExecutorNodeName(), DIRECTION.IN,
              "received response for message %d after the timeout (%dms)", msgId,
              OGlobalConfiguration.DISTRIBUTED_ASYNCH_RESPONSES_TIMEOUT.getValueAsLong());
      } else if (asynchMgr.collectResponse(response)) {
        // ALL RESPONSE RECEIVED, REMOVE THE RESPONSE MANAGER WITHOUT WAITING THE PURGE THREAD REMOVE THEM FOR TIMEOUT
        responsesByRequestIds.remove(msgId);
      }
    } finally {
      Orient.instance().getProfiler()
          .updateCounter("distributed.node.msgReceived", "Number of replication messages received in current node", +1,
              "distributed.node.msgReceived");

      Orient.instance().getProfiler().updateCounter("distributed.node." + response.getExecutorNodeName() + ".msgReceived",
          "Number of replication messages received in current node from a node", +1, "distributed.node.*.msgReceived");
    }
  }

  /**
   * Removes a response manager because in timeout.
   */
  public void timeoutRequest(final long msgId) {
    final ODistributedResponseManager asynchMgr = responsesByRequestIds.remove(msgId);
    if (asynchMgr != null)
      asynchMgr.timeout();
  }

  @Override
  public ODocument getLatencies() {
    final ODocument doc = new ODocument();

    synchronized (latencies) {
      for (Entry<String, OProfilerEntry> entry : latencies.entrySet())
        doc.field(entry.getKey(), entry.getValue().toDocument(), OType.EMBEDDED);
    }

    return doc;
  }

  @Override
  public long getCurrentLatency(final String server) {
    synchronized (latencies) {
      final OProfilerEntry l = latencies.get(server);
      if (l != null)
        return (long) (l.average / 1000000);
    }
    // NOT FOUND
    return 0;
  }

  @Override
  public void updateLatency(final String server, final long sentOn) {
    // MANAGE THIS ASYNCHRONOUSLY
    synchronized (latencies) {
      OProfilerEntry latency = latencies.get(server);
      if (latency == null) {
        latency = new OProfilerEntry();
        latencies.put(server, latency);
      } else
        latency.updateLastExecution();

      latency.entries++;

      if (latency.lastExecution - latency.lastReset > 30000) {
        // RESET STATS EVERY 30 SECONDS
        latency.last = 0;
        latency.total = 0;
        latency.average = 0;
        latency.min = 0;
        latency.max = 0;
        latency.lastResetEntries = 0;
        latency.lastReset = latency.lastExecution;
      }

      latency.lastResetEntries++;
      latency.last = System.nanoTime() - sentOn;
      latency.total += latency.last;
      latency.average = latency.total / latency.lastResetEntries;
      if (latency.last < latency.min)
        latency.min = latency.last;
      if (latency.last > latency.max)
        latency.max = latency.last;
    }
  }

  protected void purgePendingMessages() {
    final long now = System.nanoTime();

    final long timeout = OGlobalConfiguration.DISTRIBUTED_ASYNCH_RESPONSES_TIMEOUT.getValueAsLong();

    for (Iterator<Entry<Long, ODistributedResponseManager>> it = responsesByRequestIds.entrySet().iterator(); it.hasNext(); ) {
      final Entry<Long, ODistributedResponseManager> item = it.next();

      final ODistributedResponseManager resp = item.getValue();

      final long timeElapsed = (now - resp.getSentOn()) / 1000000;

      if (timeElapsed > timeout) {
        // EXPIRED REQUEST, FREE IT!
        final List<String> missingNodes = resp.getMissingNodes();

        ODistributedServerLog.warn(this, manager.getLocalNodeName(), missingNodes.toString(), DIRECTION.IN,
            "%d missed response(s) for message %d by nodes %s after %dms when timeout is %dms", missingNodes.size(),
            resp.getMessageId(), missingNodes, timeElapsed, timeout);

        Orient.instance().getProfiler()
            .updateCounter("distributed.db." + resp.getDatabaseName() + ".timeouts", "Number of messages in timeouts", +1,
                "distributed.db.*.timeouts");

        Orient.instance().getProfiler()
            .updateCounter("distributed.node.timeouts", "Number of messages in timeouts", +1, "distributed.node.timeouts");

        resp.timeout();
        it.remove();
      } else if (resp.isFinished()) {
        it.remove();
      }
    }
  }

  @Override
  public ODocument getMessageStats() {
    final ODocument doc = new ODocument();

    synchronized (messagesStats) {
      for (Map.Entry<String, AtomicLong> entry : messagesStats.entrySet())
        doc.field(entry.getKey(), entry.getValue().longValue());
    }

    return doc;
  }

  @Override
  public void updateMessageStats(final String message) {
    // MANAGE THIS ASYNCHRONOUSLY
    synchronized (messagesStats) {
      AtomicLong counter = messagesStats.get(message);
      if (counter == null) {
        counter = new AtomicLong();
        messagesStats.put(message, counter);
      }
      counter.incrementAndGet();
    }
  }

  @Override
  public long getReceivedRequests() {
    long total = 0;
    for (ODistributedDatabaseImpl db : databases.values()) {
      total += db.getReceivedRequests();
    }

    return total;
  }

  @Override
  public long getProcessedRequests() {
    long total = 0;
    for (ODistributedDatabaseImpl db : databases.values()) {
      total += db.getProcessedRequests();
    }

    return total;
  }
}