/*
 *
 *  *  Copyright 2010-2017 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.storage.impl.local;

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.orient.core.cache.OLocalRecordCache;
import com.orientechnologies.orient.core.db.ODatabase;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ORecordOperation;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.exception.OStorageException;
import com.orientechnologies.orient.core.exception.OTransactionException;
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.*;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.metadata.security.ORule;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordInternal;
import com.orientechnologies.orient.core.record.impl.ODirtyManager;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ODocumentInternal;
import com.orientechnologies.orient.core.storage.OBasicTransaction;
import com.orientechnologies.orient.core.storage.ORecordCallback;
import com.orientechnologies.orient.core.tx.*;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The special micro transaction used to wrap non-transactional operations into implicit transactions. Such transactions are not
 * visible on the database level.
 *
 * @author Sergey Sitnikov
 */
public final class OMicroTransaction implements OBasicTransaction, OTransactionInternal {

  private static final AtomicInteger transactionSerial = new AtomicInteger(0);

  private ODatabaseDocumentInternal database;
  private final OAbstractPaginatedStorage storage;

  private final int id;

  /**
   * All the record/cluster operations known to this micro-transactions, mapped by {@link ORID}.
   */
  private final Map<ORID, ORecordOperation> recordOperations = new LinkedHashMap<>();

  /**
   * All the index operations known to this micro-transaction, mapped by index name.
   */
  private final Map<String, OTransactionIndexChanges> indexOperations = new LinkedHashMap<>();

  /**
   * All the index operations known to this micro-transaction affecting index values containing records with certain {@link ORID}s,
   * mapped by the record {@link ORID}.
   */
  private final Map<ORID, List<OTransactionRecordIndexOperation>> recordIndexOperations = new HashMap<>();

  private final Map<ORID, ORID> updatedRids      = new HashMap<>();
  private final Set<ODocument>  changedDocuments = new HashSet<>();

  private final Map<String, Object> customData = new HashMap<>();

  private boolean active       = false;
  private int     level        = 0;
  private int     recordSerial = -2;

  private Map<ORID, OTransactionAbstract.LockedRecordMetadata> noTxLocks;

  /**
   * Instantiates a new micro-transaction.
   *
   * @param storage  the micro-transaction's storage.
   * @param database the micro-transaction's database.
   */
  public OMicroTransaction(OAbstractPaginatedStorage storage, ODatabaseDocumentInternal database) {
    this.storage = storage;
    this.database = database;

    this.id = transactionSerial.incrementAndGet();
  }

  /**
   * @return the unique id of this micro-transaction.
   */
  public int getId() {
    return id;
  }

  @Override
  public int getClientTransactionId() {
    return -1;
  }

  @Override
  public void updateIdentityAfterCommit(ORID oldRID, ORID rid) {
    updateIdentityAfterRecordCommit(oldRID, rid);
  }

  /**
   * @return the micro-transaction's database.
   */
  public ODatabaseDocumentInternal getDatabase() {
    return database;
  }

  /**
   * @return the record operations done in the context of this micro-transaction.
   */
  public Collection<ORecordOperation> getRecordOperations() { // ordered by operation time
    return recordOperations.values();
  }

  /**
   * @return the index operations done in the context of this micro-transaction.
   */
  public Map<String, OTransactionIndexChanges> getIndexOperations() {
    return indexOperations;
  }

  /**
   * Begins the micro-transaction. Micro-transactions may be nested.
   */
  public void begin() {
    if (level < 0)
      throw error("Unbalanced micro-transaction, level = " + level);

    ++level;
    active = true;
  }

  /**
   * Commits the micro-transaction if it's a top-level micro-transaction.
   */
  public void commit() {
    if (!active)
      throw error("Inactive micro-transaction on commit");
    if (level < 1)
      throw error("Unbalanced micro-transaction, level = " + level);

    --level;

    if (level == 0) {
      active = false;
      doCommit();
    }
  }

  /**
   * Rollbacks the micro-transaction if it's a top-level micro-transaction.
   */
  public void rollback() {
    if (!active)
      throw error("Inactive micro-transaction on rollback");
    if (level < 1)
      throw error("Unbalanced micro-transaction, level = " + level);

    --level;

    if (level == 0) {
      active = false;
      doRollback();
    }
  }

  /**
   * Rollbacks the micro-transaction after failed commit attempt.
   */
  public void rollbackAfterFailedCommit() {
    if (active)
      throw error("Active micro-transaction on rollback after failed commit");
    if (level != 0)
      throw error("Unbalanced micro-transaction, level = " + level);

    doRollback();
  }

  /**
   * Updates the record identity after its successful commit.
   */
  public void updateIdentityAfterRecordCommit(final ORID oldRid, final ORID newRid) {
    if (oldRid.equals(newRid))
      return; // no change, ignore

    // XXX: Identity update may mutate the index keys, so we have to identify and reinsert potentially affected index keys to keep
    // the OTransactionIndexChanges.changesPerKey in a consistent state.

    final List<KeyChangesUpdateRecord> keyRecordsToReinsert = new ArrayList<>();
    final OIndexManager indexManager = getDatabase().getMetadata().getIndexManager();
    for (Map.Entry<String, OTransactionIndexChanges> entry : indexOperations.entrySet()) {
      final OIndex<?> index = indexManager.getIndex(entry.getKey());
      if (index == null)
        throw new OTransactionException("Cannot find index '" + entry.getValue() + "' while committing transaction");

      final Dependency[] fieldRidDependencies = getIndexFieldRidDependencies(index);
      if (!isIndexMayDependOnRids(fieldRidDependencies))
        continue;

      final OTransactionIndexChanges indexChanges = entry.getValue();
      for (final Iterator<OTransactionIndexChangesPerKey> iterator = indexChanges.changesPerKey.values().iterator(); iterator
          .hasNext(); ) {
        final OTransactionIndexChangesPerKey keyChanges = iterator.next();
        if (isIndexKeyMayDependOnRid(keyChanges.key, oldRid, fieldRidDependencies)) {
          keyRecordsToReinsert.add(new KeyChangesUpdateRecord(keyChanges, indexChanges));
          iterator.remove();
        }
      }
    }

    // Update the identity.

    final ORecordOperation rec = resolveRecordOperation(oldRid);
    if (rec != null) {
      updatedRids.put(newRid.copy(), oldRid.copy());

      if (!rec.getRecord().getIdentity().equals(newRid)) {
        ORecordInternal.onBeforeIdentityChanged(rec.getRecord());

        final ORecordId recordId = (ORecordId) rec.getRecord().getIdentity();
        if (recordId == null) {
          ORecordInternal.setIdentity(rec.getRecord(), new ORecordId(newRid));
        } else {
          recordId.setClusterPosition(newRid.getClusterPosition());
          recordId.setClusterId(newRid.getClusterId());
        }

        ORecordInternal.onAfterIdentityChanged(rec.getRecord());
      }
    }

    // Reinsert the potentially affected index keys.

    for (KeyChangesUpdateRecord record : keyRecordsToReinsert)
      record.indexChanges.changesPerKey.put(record.keyChanges.key, record.keyChanges);

    // Update the indexes.

    final List<OTransactionRecordIndexOperation> transactionIndexOperations = recordIndexOperations.get(translateRid(oldRid));
    if (transactionIndexOperations != null) {
      for (final OTransactionRecordIndexOperation indexOperation : transactionIndexOperations) {
        OTransactionIndexChanges indexEntryChanges = indexOperations.get(indexOperation.index);
        if (indexEntryChanges == null)
          continue;
        final OTransactionIndexChangesPerKey keyChanges;
        if (indexOperation.key == null) {
          keyChanges = indexEntryChanges.nullKeyChanges;
        } else {
          keyChanges = indexEntryChanges.changesPerKey.get(indexOperation.key);
        }
        if (keyChanges != null)
          updateChangesIdentity(oldRid, newRid, keyChanges);
      }
    }
  }

  /**
   * Updates the record cache after unsuccessful micro-transaction commit.
   */
  public void updateRecordCacheAfterRollback() {
    final OLocalRecordCache databaseLocalCache = database.getLocalCache();

    for (ORecordOperation recordOperation : recordOperations.values())
      databaseLocalCache.deleteRecord(recordOperation.getRecord().getIdentity());
  }

  @Override
  public boolean isActive() {
    return active;
  }

  @Override
  public ORecord saveRecord(ORecord record, String clusterName, ODatabase.OPERATION_MODE operationMode, boolean forceCreation,
      ORecordCallback<? extends Number> createdCallback, ORecordCallback<Integer> updatedCallback) {
    if (!active)
      throw error("Inactive micro-transaction on record save");

    if (record == null)
      return null;
    if (!record.isDirty())
      return record;

    final ORecordOperation recordOperation;
    if (forceCreation || !record.getIdentity().isValid())
      recordOperation = addRecordOperation(record, ORecordOperation.CREATED, clusterName);
    else
      recordOperation = addRecordOperation(record, ORecordOperation.UPDATED, clusterName);

    if (recordOperation != null) {
      if (createdCallback != null)
        //noinspection unchecked
        recordOperation.createdCallback = (ORecordCallback<Long>) createdCallback;
      if (updatedCallback != null)
        recordOperation.updatedCallback = updatedCallback;
    }

    return record;
  }

  @Override
  public void deleteRecord(ORecord record, ODatabase.OPERATION_MODE mode) {
    if (!record.getIdentity().isValid())
      return;

    addRecordOperation(record, ORecordOperation.DELETED, null);
  }

  @Override
  public ORecord getRecord(ORID rid) {
    final ORecordOperation recordOperation = resolveRecordOperation(rid);
    if (recordOperation == null)
      return null;

    return recordOperation.type == ORecordOperation.DELETED ? DELETED_RECORD : recordOperation.record.getRecord();
  }

  @Override
  public OTransactionIndexChanges getIndexChanges(String indexName) {
    return indexOperations.get(indexName);
  }

  @Override
  public OTransactionIndexChanges getIndexChangesInternal(String indexName) {
    return getIndexChanges(indexName);
  }

  @Override
  public Object getCustomData(String name) {
    return customData.get(name);
  }

  @Override
  public void setCustomData(String name, Object value) {
    customData.put(name, value);
  }

  private OStorageException error(String message) {
    return new OStorageException(message);
  }

  private void doCommit() {
    if (!recordOperations.isEmpty() || !indexOperations.isEmpty())
      getDatabase().internalCommit(this);

    invokeCallbacks();

    reset();
  }

  private void doRollback() {
    storage.rollback(this);

    database.getLocalCache().clear();

    for (ORecordOperation recordOperation : recordOperations.values()) {
      final ORecord record = recordOperation.record.getRecord();

      if (record.isDirty())
        if (record instanceof ODocument) {
          final ODocument document = (ODocument) record;

          if (document.isTrackingChanges())
            document.undo();
        } else
          record.unload();
    }

    reset();
  }

  private void invokeCallbacks() {
    for (ORecordOperation recordOperation : recordOperations.values()) {
      final ORecord record = recordOperation.getRecord();
      final ORID identity = record.getIdentity();

      if (recordOperation.type == ORecordOperation.CREATED && recordOperation.createdCallback != null)
        recordOperation.createdCallback.call(new ORecordId(identity), identity.getClusterPosition());
      else if (recordOperation.type == ORecordOperation.UPDATED && recordOperation.updatedCallback != null)
        recordOperation.updatedCallback.call(new ORecordId(identity), record.getVersion());
    }
  }

  private void reset() {
    for (ORecordOperation recordOperation : recordOperations.values()) {
      final ORecord record = recordOperation.record.getRecord();

      if (record instanceof ODocument) {
        final ODocument document = (ODocument) record;

        if (document.isDirty())
          document.undo();

        changedDocuments.remove(document);
      }
    }

    for (ODocument changedDocument : changedDocuments)
      changedDocument.undo();

    changedDocuments.clear();
    updatedRids.clear();
    recordOperations.clear();
    indexOperations.clear();
    recordIndexOperations.clear();
    recordSerial = -2;

    customData.clear();
  }

  private ORecordOperation resolveRecordOperation(ORID rid) {
    return recordOperations.get(translateRid(rid));
  }

  private ORID translateRid(ORID rid) {
    while (true) {
      final ORID translatedRid = updatedRids.get(rid);
      if (translatedRid == null)
        break;

      rid = translatedRid;
    }

    return rid;
  }

  private ORecordOperation addRecordOperation(ORecord record, byte type, String clusterName) {

    if (clusterName == null)
      clusterName = database.getClusterNameById(record.getIdentity().getClusterId());

    //noinspection SuspiciousMethodCalls
    changedDocuments.remove(record);

    try {
      switch (type) {
      case ORecordOperation.CREATED: {
        OIdentifiable newRec = database.beforeCreateOperations(record, clusterName);
        if (newRec != null) {
          record = (ORecord) newRec;
          reSave(record);
        }
      }
      break;

      case ORecordOperation.UPDATED: {
        OIdentifiable newRec = database.beforeUpdateOperations(record, clusterName);
        if (newRec != null) {
          record = (ORecord) newRec;
          reSave(record);
        }
      }
      break;

      case ORecordOperation.DELETED:
        database.beforeDeleteOperations(record, clusterName);
        break;
      }

      try {
        ORecordOperation recordOperation = internalAddRecord(record, type, clusterName);

        switch (type) {
        case ORecordOperation.CREATED:
          database.afterCreateOperations(record);
          break;
        case ORecordOperation.UPDATED:
          database.afterUpdateOperations(record);
          break;
        case ORecordOperation.DELETED:
          database.afterDeleteOperations(record);
          break;
        }

        if (record instanceof ODocument && ((ODocument) record).isTrackingChanges())
          ODocumentInternal.clearTrackData(((ODocument) record));

        return recordOperation;

      } catch (Exception e) {
        switch (type) {
        case ORecordOperation.CREATED:
          database.callbackHooks(ORecordHook.TYPE.CREATE_FAILED, record);
          break;
        case ORecordOperation.UPDATED:
          database.callbackHooks(ORecordHook.TYPE.UPDATE_FAILED, record);
          break;
        case ORecordOperation.DELETED:
          database.callbackHooks(ORecordHook.TYPE.DELETE_FAILED, record);
          break;
        }

        throw OException.wrapException(new ODatabaseException("Error on saving record " + record.getIdentity()), e);
      }
    } finally {
      switch (type) {
      case ORecordOperation.CREATED:
        database.callbackHooks(ORecordHook.TYPE.FINALIZE_CREATION, record);
        break;
      case ORecordOperation.UPDATED:
        database.callbackHooks(ORecordHook.TYPE.FINALIZE_UPDATE, record);
        break;
      case ORecordOperation.DELETED:
        database.callbackHooks(ORecordHook.TYPE.FINALIZE_DELETION, record);
        break;
      }
    }
  }

  public ORecordOperation internalAddRecord(ORecord record, byte type, String clusterName) {
    final ORecordId recordId = (ORecordId) record.getIdentity();

    if (!recordId.isValid()) {
      ORecordInternal.onBeforeIdentityChanged(record);
      database.assignAndCheckCluster(record, clusterName);

      recordId.setClusterPosition(recordSerial--);

      ORecordInternal.onAfterIdentityChanged(record);
    }

    ORecordOperation recordOperation = resolveRecordOperation(recordId);

    if (recordOperation == null) {
      if (!(recordId.isTemporary() && type != ORecordOperation.CREATED)) {
        recordOperation = new ORecordOperation(record, type);
        recordOperations.put(recordId.copy(), recordOperation);
      }
    } else {
      recordOperation.record = record;

      switch (recordOperation.type) {
      case ORecordOperation.CREATED:
        if (type == ORecordOperation.DELETED)
          recordOperations.remove(recordId);
        break;
      case ORecordOperation.UPDATED:
        if (type == ORecordOperation.DELETED)
          recordOperation.type = ORecordOperation.DELETED;
        break;
      case ORecordOperation.DELETED:
        break; // do nothing
      }
    }
    return recordOperation;
  }

  @Override
  public void addIndexEntry(OIndex<?> index, String indexName, OTransactionIndexChanges.OPERATION type, Object key,
      OIdentifiable value) {
    final OTransactionIndexChanges indexOperation = indexOperations.computeIfAbsent(indexName, k -> new OTransactionIndexChanges());

    if (type == OTransactionIndexChanges.OPERATION.CLEAR)
      indexOperation.setCleared();
    else {
      final OTransactionIndexChangesPerKey changesPerKey = indexOperation.getChangesPerKey(key);
      changesPerKey.clientTrackOnly = false;
      changesPerKey.add(value, type);

      if (value == null)
        return;

      List<OTransactionRecordIndexOperation> transactionIndexOperations = recordIndexOperations.get(value.getIdentity());
      if (transactionIndexOperations == null) {
        transactionIndexOperations = new ArrayList<>();
        recordIndexOperations.put(value.getIdentity().copy(), transactionIndexOperations);
      }

      transactionIndexOperations.add(new OTransactionRecordIndexOperation(indexName, key, type));
    }
  }

  @Override
  public void addChangedDocument(ODocument document) {
    if (getRecord(document.getIdentity()) == null)
      changedDocuments.add(document);
  }

  private void reSave(ORecord record) {
    final ODirtyManager manager = ORecordInternal.getDirtyManager(record);
    final Set<ORecord> newRecords = manager.getNewRecords();
    final Set<ORecord> updatedRecords = manager.getUpdateRecords();
    manager.clearForSave();

    if (newRecords != null) {
      for (ORecord newRecord : newRecords) {
        if (newRecord != record)
          saveRecord(newRecord, null, ODatabase.OPERATION_MODE.SYNCHRONOUS, false, null, null);
      }
    }

    if (updatedRecords != null) {
      for (ORecord updatedRecord : updatedRecords) {
        if (updatedRecord != record)
          saveRecord(updatedRecord, null, ODatabase.OPERATION_MODE.SYNCHRONOUS, false, null, null);
      }
    }
  }

  private void updateChangesIdentity(ORID oldRid, ORID newRid, OTransactionIndexChangesPerKey changesPerKey) {
    if (changesPerKey == null)
      return;

    for (final OTransactionIndexChangesPerKey.OTransactionIndexEntry indexEntry : changesPerKey.entries)
      if (indexEntry.value.getIdentity().equals(oldRid))
        indexEntry.value = newRid;
  }

  private static Dependency[] getIndexFieldRidDependencies(OIndex<?> index) {
    final OIndexDefinition definition = index.getDefinition();

    if (definition == null) // type for untyped index is still not resolved
      return null;

    final OType[] types = definition.getTypes();
    final Dependency[] dependencies = new Dependency[types.length];

    for (int i = 0; i < types.length; ++i)
      dependencies[i] = getTypeRidDependency(types[i]);

    return dependencies;
  }

  private static Dependency getTypeRidDependency(OType type) {
    switch (type) {
    case CUSTOM:
    case ANY:
      return Dependency.Unknown;

    case EMBEDDED:
    case LINK:
      return Dependency.Yes;

    case LINKLIST:
    case LINKSET:
    case LINKMAP:
    case LINKBAG:
    case EMBEDDEDLIST:
    case EMBEDDEDSET:
    case EMBEDDEDMAP:
      assert false; // under normal conditions, collection field type is already resolved to its component type
      return Dependency.Unknown; // fallback to the safest variant, just in case

    default: // all other primitive types which doesn't depend on rids
      return Dependency.No;
    }
  }

  private static boolean isIndexMayDependOnRids(Dependency[] fieldDependencies) {
    if (fieldDependencies == null)
      return true;

    for (Dependency dependency : fieldDependencies)
      switch (dependency) {
      case Unknown:
        return true;
      case Yes:
        return true;
      case No:
        break; // do nothing
      }

    return false;
  }

  private static boolean isIndexKeyMayDependOnRid(Object key, ORID rid, Dependency[] keyDependencies) {
    if (key instanceof OCompositeKey) {
      final List<Object> subKeys = ((OCompositeKey) key).getKeys();
      for (int i = 0; i < subKeys.size(); ++i)
        if (isIndexKeyMayDependOnRid(subKeys.get(i), rid, keyDependencies == null ? null : keyDependencies[i]))
          return true;
      return false;
    }

    return isIndexKeyMayDependOnRid(key, rid, keyDependencies == null ? null : keyDependencies[0]);
  }

  private static boolean isIndexKeyMayDependOnRid(Object key, ORID rid, Dependency dependency) {
    if (dependency == Dependency.No)
      return false;

    if (key instanceof OIdentifiable)
      return key.equals(rid);

    return dependency == Dependency.Unknown || dependency == null;
  }

  @Override
  public void setStatus(OTransaction.TXSTATUS iStatus) {
    //IGNORE
  }

  private enum Dependency {
    Unknown, Yes, No
  }

  private static class KeyChangesUpdateRecord {
    final OTransactionIndexChangesPerKey keyChanges;
    final OTransactionIndexChanges       indexChanges;

    public KeyChangesUpdateRecord(OTransactionIndexChangesPerKey keyChanges, OTransactionIndexChanges indexChanges) {
      this.keyChanges = keyChanges;
      this.indexChanges = indexChanges;
    }
  }

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

  @Override
  public ORecordOperation getRecordEntry(ORID currentRid) {
    return recordOperations.get(currentRid);
  }

  public void setNoTxLocks(Map<ORID, OTransactionAbstract.LockedRecordMetadata> noTxLocks) {
    this.noTxLocks = noTxLocks;
  }

  public Set<ORID> getLockedRecords() {
    if (noTxLocks != null) {
      return noTxLocks.keySet();
    }
    return null;
  }

  @Override
  public void setDatabase(ODatabaseDocumentInternal database) {
    this.database = database;
  }

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


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

import com.orientechnologies.common.log.OLogManager;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Locale;

public class OFileUtils {
  public static final int  KILOBYTE = 1024;
  public static final int  MEGABYTE = 1048576;
  public static final int  GIGABYTE = 1073741824;
  public static final long TERABYTE = 1099511627776L;

  private static final boolean useOldFileAPI;

  static {
    boolean oldAPI = false;

    try {
      Class.forName("java.nio.file.FileSystemException");
    } catch (ClassNotFoundException ignore) {
      oldAPI = true;
    }

    useOldFileAPI = oldAPI;
  }

  public static long getSizeAsNumber(final Object iSize) {
    if (iSize == null)
      throw new IllegalArgumentException("Size is null");

    if (iSize instanceof Number)
      return ((Number) iSize).longValue();

    String size = iSize.toString();

    boolean number = true;
    for (int i = size.length() - 1; i >= 0; --i) {
      final char c = size.charAt(i);
      if (!Character.isDigit(c)) {
        if (i > 0 || (c != '-' && c != '+'))
          number = false;
        break;
      }
    }

    if (number)
      return string2number(size).longValue();
    else {
      size = size.toUpperCase(Locale.ENGLISH);
      int pos = size.indexOf("KB");
      if (pos > -1)
        return (long) (string2number(size.substring(0, pos)).floatValue() * KILOBYTE);

      pos = size.indexOf("MB");
      if (pos > -1)
        return (long) (string2number(size.substring(0, pos)).floatValue() * MEGABYTE);

      pos = size.indexOf("GB");
      if (pos > -1)
        return (long) (string2number(size.substring(0, pos)).floatValue() * GIGABYTE);

      pos = size.indexOf("TB");
      if (pos > -1)
        return (long) (string2number(size.substring(0, pos)).floatValue() * TERABYTE);

      pos = size.indexOf('B');
      if (pos > -1)
        return (long) string2number(size.substring(0, pos)).floatValue();

      pos = size.indexOf('%');
      if (pos > -1)
        return (long) (-1 * string2number(size.substring(0, pos)).floatValue());

      // RE-THROW THE EXCEPTION
      throw new IllegalArgumentException("Size " + size + " has a unrecognizable format");
    }
  }

  public static Number string2number(final String iText) {
    if (iText.indexOf('.') > -1)
      return Double.parseDouble(iText);
    else
      return Long.parseLong(iText);
  }

  public static String getSizeAsString(final long iSize) {
    if (iSize > TERABYTE)
      return String.format("%2.2fTB", (float) iSize / TERABYTE);
    if (iSize > GIGABYTE)
      return String.format("%2.2fGB", (float) iSize / GIGABYTE);
    if (iSize > MEGABYTE)
      return String.format("%2.2fMB", (float) iSize / MEGABYTE);
    if (iSize > KILOBYTE)
      return String.format("%2.2fKB", (float) iSize / KILOBYTE);

    return String.valueOf(iSize) + "b";
  }

  public static String getDirectory(String iPath) {
    iPath = getPath(iPath);
    int pos = iPath.lastIndexOf("/");
    if (pos == -1)
      return "";

    return iPath.substring(0, pos);
  }

  public static void createDirectoryTree(final String iFileName) {
    final String[] fileDirectories = iFileName.split("/");
    for (int i = 0; i < fileDirectories.length - 1; ++i)
      new File(fileDirectories[i]).mkdir();
  }

  public static String getPath(final String iPath) {
    if (iPath == null)
      return null;
    return iPath.replace('\\', '/');
  }

  public static void checkValidName(final String iFileName) throws IOException {
    if (iFileName.contains("..") || iFileName.contains("/") || iFileName.contains("\\"))
      throw new IOException("Invalid file name '" + iFileName + "'");
  }

  public static void deleteRecursively(final File rootFile) {
    deleteRecursively(rootFile, false);
  }

  public static void deleteRecursively(final File rootFile, boolean onlyDirs) {
    if (!rootFile.exists())
      return;

    try {
      Path rootPath = Paths.get(rootFile.getCanonicalPath());
      Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
          if (!onlyDirs) {
            if (file != null && file.toFile() != null && file.toFile().exists()) {
              file.toFile().delete();
            }
          }
          return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
          if (dir != null && dir.toFile() != null && dir.toFile().exists()) {
            dir.toFile().delete();
          }
          return FileVisitResult.CONTINUE;
        }
      });
    } catch (IOException e) {
      throw new IllegalStateException(e);
    }
  }

  public static void deleteFolderIfEmpty(final File dir) {
    if (dir != null && dir.listFiles() != null && dir.listFiles().length == 0) {
      deleteRecursively(dir);
    }
  }

  @SuppressWarnings("resource")
  public static final void copyFile(final File source, final File destination) throws IOException {
    FileChannel sourceChannel = new FileInputStream(source).getChannel();
    FileChannel targetChannel = new FileOutputStream(destination).getChannel();
    sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
    sourceChannel.close();
    targetChannel.close();
  }

  public static final void copyDirectory(final File source, final File destination) throws IOException {
    if (!destination.exists())
      destination.mkdirs();

    for (File f : source.listFiles()) {
      final File target = new File(destination.getAbsolutePath() + "/" + f.getName());
      if (f.isFile())
        copyFile(f, target);
      else
        copyDirectory(f, target);
    }
  }

  public static boolean renameFile(File from, File to) throws IOException {
    if (useOldFileAPI)
      return from.renameTo(to);

    final FileSystem fileSystem = FileSystems.getDefault();

    final Path fromPath = fileSystem.getPath(from.getAbsolutePath());
    final Path toPath = fileSystem.getPath(to.getAbsolutePath());
    Files.move(fromPath, toPath);

    return true;
  }

  public static boolean delete(File file) throws IOException {
    if (!file.exists())
      return true;

    if (useOldFileAPI)
      return file.delete();

    return OFileUtilsJava7.delete(file);
  }

  /**
   * Prepares the path for a file creation or replacement. If the file pointed by the path already exists, it will be deleted, a
   * warning will be emitted to the log in this case. All absent directories along the path will be created.
   *
   * @param path      the file path.
   * @param requester the requester of an operation being performed to produce user-friendly log messages.
   * @param operation the description of an operation being performed to produce user-friendly log messages. Use descriptions like
   *                  "exporting", "backing up", etc.
   */
  public static void prepareForFileCreationOrReplacement(Path path, Object requester, String operation) throws IOException {
    if (Files.deleteIfExists(path))
      OLogManager.instance().warn(requester, "'%s' deleted while %s", path, operation);

    final Path parent = path.getParent();
    if (parent != null)
      Files.createDirectories(parent);
  }

  /**
   * Tries to move a file from the source to the target atomically. If atomic move is not possible, falls back to regular move.
   *
   * @param source    the source to move the file from.
   * @param target    the target to move the file to.
   * @param requester the requester of the move being performed to produce user-friendly log messages.
   *
   * @see Files#move(Path, Path, CopyOption...)
   * @see StandardCopyOption#ATOMIC_MOVE
   */
  public static void atomicMoveWithFallback(Path source, Path target, Object requester) throws IOException {
    try {
      Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
    } catch (AtomicMoveNotSupportedException ignore) {
      OLogManager.instance()
          .warn(requester, "atomic file move is not possible, falling back to regular move (moving '%s' to '%s')", source, target);
      Files.move(source, target);
    }
  }

}


package com.orientechnologies.orient.core.sql.parser;

import com.orientechnologies.orient.core.sql.OCommandSQLParsingException;
import com.orientechnologies.orient.core.sql.executor.PatternEdge;
import com.orientechnologies.orient.core.sql.executor.PatternNode;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by luigidellaquila on 28/07/15.
 */
public class Pattern {
  public Map<String, PatternNode> aliasToNode = new LinkedHashMap<String, PatternNode>();
  public int                      numOfEdges  = 0;

  public void addExpression(OMatchExpression expression) {
    PatternNode originNode = getOrCreateNode(expression.origin);

    for (OMatchPathItem item : expression.items) {
      String nextAlias = item.filter.getAlias();
      PatternNode nextNode = getOrCreateNode(item.filter);

      numOfEdges += originNode.addEdge(item, nextNode);
      originNode = nextNode;
    }
  }

  private PatternNode getOrCreateNode(OMatchFilter origin) {
    PatternNode originNode = get(origin.getAlias());
    if (originNode == null) {
      originNode = new PatternNode();
      originNode.alias = origin.getAlias();
      aliasToNode.put(originNode.alias, originNode);
    }
    if (origin.isOptional()) {
      originNode.optional = true;
    }
    return originNode;
  }

  public PatternNode get(String alias) {
    return aliasToNode.get(alias);
  }

  public int getNumOfEdges() {
    return numOfEdges;
  }

  public void validate() {
    for (PatternNode node : this.aliasToNode.values()) {
      if (node.isOptionalNode()) {
        if (node.out.size() > 0) {
          throw new OCommandSQLParsingException(
              "In current MATCH version, optional nodes are allowed only on right terminal nodes, eg. {} --> {optional:true} is allowed, {optional:true} <-- {} is not. ");
        }
        if (node.in.size() == 0) {
          throw new OCommandSQLParsingException(
              "In current MATCH version, optional nodes must have at least one incoming pattern edge");
        }
        //        if (node.in.size() != 1) {
        //          throw new OCommandSQLParsingException("In current MATCH version, optional nodes are allowed only as single terminal nodes. ");
        //        }
      }
    }
  }

  /**
   * splits this pattern into multiple
   *
   * @return
   */
  public List<Pattern> getDisjointPatterns() {
    Map<PatternNode, String> reverseMap = new IdentityHashMap<>();
    reverseMap.putAll(this.aliasToNode.entrySet().stream().collect(Collectors.toMap(x -> x.getValue(), x -> x.getKey())));

    List<Pattern> result = new ArrayList<>();
    while (!reverseMap.isEmpty()) {
      Pattern pattern = new Pattern();
      result.add(pattern);
      Map.Entry<PatternNode, String> nextNode = reverseMap.entrySet().iterator().next();
      Set<PatternNode> toVisit = new HashSet<>();
      toVisit.add(nextNode.getKey());
      while (toVisit.size() > 0) {
        PatternNode currentNode = toVisit.iterator().next();
        toVisit.remove(currentNode);
        if (reverseMap.containsKey(currentNode)) {
          pattern.aliasToNode.put(reverseMap.get(currentNode), currentNode);
          reverseMap.remove(currentNode);
          for (PatternEdge x : currentNode.out) {
            toVisit.add(x.in);
          }
          for (PatternEdge x : currentNode.in) {
            toVisit.add(x.out);
          }
        }
      }
      pattern.recalculateNumOfEdges();
    }
    return result;
  }

  private void recalculateNumOfEdges() {
    Map<PatternEdge, PatternEdge> edges = new IdentityHashMap<>();
    for (PatternNode node : this.aliasToNode.values()) {
      for (PatternEdge edge : node.out) {
        edges.put(edge, edge);
      }
      for (PatternEdge edge : node.in) {
        edges.put(edge, edge);
      }
    }
    this.numOfEdges = edges.size();
  }

  public Map<String, PatternNode> getAliasToNode() {
    return aliasToNode;
  }

  public void setAliasToNode(Map<String, PatternNode> aliasToNode) {
    this.aliasToNode = aliasToNode;
  }

  public void setNumOfEdges(int numOfEdges) {
    this.numOfEdges = numOfEdges;
  }
}


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

import com.orientechnologies.common.concur.ONeedRetryException;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.io.OIOException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.util.OCallable;
import com.orientechnologies.orient.core.command.*;
import com.orientechnologies.orient.core.command.script.OCommandScript;
import com.orientechnologies.orient.core.config.OContextConfiguration;
import com.orientechnologies.orient.core.config.OStorageConfiguration;
import com.orientechnologies.orient.core.conflict.ORecordConflictStrategy;
import com.orientechnologies.orient.core.db.OScenarioThreadLocal;
import com.orientechnologies.orient.core.db.OScenarioThreadLocal.RUN_MODE;
import com.orientechnologies.orient.core.db.record.OCurrentStorageComponentsFactory;
import com.orientechnologies.orient.core.db.record.ORecordOperation;
import com.orientechnologies.orient.core.exception.OConcurrentModificationException;
import com.orientechnologies.orient.core.exception.ORecordNotFoundException;
import com.orientechnologies.orient.core.exception.OStorageException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.OCommandExecutorSQLDelegate;
import com.orientechnologies.orient.core.sql.OCommandExecutorSQLSelect;
import com.orientechnologies.orient.core.sql.OCommandSQL;
import com.orientechnologies.orient.core.sql.functions.OSQLFunctionRuntime;
import com.orientechnologies.orient.core.storage.*;
import com.orientechnologies.orient.core.storage.disk.OLocalPaginatedStorage;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;
import com.orientechnologies.orient.core.storage.impl.local.OFreezableStorageComponent;
import com.orientechnologies.orient.core.storage.ridbag.sbtree.OSBTreeCollectionManager;
import com.orientechnologies.orient.core.tx.OTransactionInternal;
import com.orientechnologies.orient.distributed.impl.task.*;
import com.orientechnologies.orient.server.OServer;
import com.orientechnologies.orient.server.distributed.*;
import com.orientechnologies.orient.server.distributed.ODistributedRequest.EXECUTION_MODE;
import com.orientechnologies.orient.server.distributed.task.OAbstractCommandTask;
import com.orientechnologies.orient.server.distributed.task.OAbstractRemoteTask;
import com.orientechnologies.orient.server.distributed.task.ODistributedOperationException;

import java.io.*;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Distributed storage implementation that routes to the owner node the request.
 *
 * @author Luca Garulli (l.garulli--at--orientdb.com)
 */
public class ODistributedStorage implements OStorage, OFreezableStorageComponent, OAutoshardedStorage {
  private final    String                    name;
  private final    OServer                   serverInstance;
  private final    ODistributedServerManager dManager;
  private volatile OAbstractPaginatedStorage wrapped;

  private ODistributedServerManager.DB_STATUS prevStatus;
  private ODistributedDatabase                localDistributedDatabase;
  private ODistributedStorageEventListener    eventListener;

  private volatile ODistributedConfiguration distributedConfiguration;
  private volatile OBackgroundBackup         lastValidBackup = null;

  public ODistributedStorage(final OServer iServer, final String dbName) {
    this.serverInstance = iServer;
    this.dManager = iServer.getDistributedManager();
    this.name = dbName;
  }

  /**
   * Supported only in embedded storage. Use SELECT FROM metadata:storage instead.
   */
  @Override
  public String getCreatedAtVersion() {
    throw new UnsupportedOperationException("Supported only in embedded storage. Use 'SELECT FROM metadata:storage' instead.");
  }

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

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

  public Object command(final OCommandRequestText iCommand) {
    if (isLocalEnv())
      // ALREADY DISTRIBUTED
      return wrapped.command(iCommand);

    List<String> servers = (List<String>) iCommand.getContext().getVariable("servers");
    if (servers == null) {
      servers = new ArrayList<String>();
      iCommand.getContext().setVariable("servers", servers);
    }
    final String localNodeName = dManager.getLocalNodeName();

    servers.add(localNodeName);

    final ODistributedConfiguration dbCfg = distributedConfiguration;
    if (!dbCfg.isReplicationActive(null, localNodeName))
      // DON'T REPLICATE
      return wrapped.command(iCommand);

    final OCommandExecutor executor = OCommandManager.instance().getExecutor(iCommand);

    executor.setProgressListener(iCommand.getProgressListener());
    executor.parse(iCommand);

    final OCommandExecutor exec = executor instanceof OCommandExecutorSQLDelegate ?
        ((OCommandExecutorSQLDelegate) executor).getDelegate() :
        executor;

    if (!exec.isIdempotent()) {
      resetLastValidBackup();
    }

    if (exec.isIdempotent() && !dManager.isNodeAvailable(dManager.getLocalNodeName(), getName())) {
      // SPECIAL CASE: NODE IS OFFLINE AND THE COMMAND IS IDEMPOTENT, EXECUTE IT LOCALLY ONLY
      ODistributedServerLog.warn(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
          "Node '%s' is %s, the command '%s' against database '%s' will be executed only on local server with the possibility to have partial result",
          dManager.getLocalNodeName(), dManager.getDatabaseStatus(dManager.getLocalNodeName(), getName()), iCommand,
          wrapped.getName());

      return wrapped.command(iCommand);
    }

    if (!exec.isIdempotent())
      checkNodeIsMaster(localNodeName, dbCfg, "Command '" + iCommand + "'");

    try {
      Object result = null;
      OCommandDistributedReplicateRequest.DISTRIBUTED_EXECUTION_MODE executionMode = OCommandDistributedReplicateRequest.DISTRIBUTED_EXECUTION_MODE.LOCAL;
      OCommandDistributedReplicateRequest.DISTRIBUTED_RESULT_MGMT resultMgmt = OCommandDistributedReplicateRequest.DISTRIBUTED_RESULT_MGMT.CHECK_FOR_EQUALS;
      boolean executeOnLocalNodeFirst = true;

      if (OScenarioThreadLocal.INSTANCE.getRunMode() != RUN_MODE.RUNNING_DISTRIBUTED) {
        if (exec instanceof OCommandDistributedReplicateRequest) {
          executionMode = ((OCommandDistributedReplicateRequest) exec).getDistributedExecutionMode();
          resultMgmt = ((OCommandDistributedReplicateRequest) exec).getDistributedResultManagement();
          executeOnLocalNodeFirst = ((OCommandDistributedReplicateRequest) exec).isDistributedExecutingOnLocalNodeFirst();
        }
      }

      switch (executionMode) {
      case LOCAL:
        // CALL IN DEFAULT MODE TO LET OWN COMMAND TO REDISTRIBUTE CHANGES (LIKE INSERT)
        return wrapped.command(iCommand);

      case REPLICATE:
        // REPLICATE IT, GET ALL THE INVOLVED NODES
        final Collection<String> involvedClusters = exec.getInvolvedClusters();

        if (resultMgmt == OCommandDistributedReplicateRequest.DISTRIBUTED_RESULT_MGMT.MERGE) {
          if (!exec.isIdempotent() && dbCfg.isSharded())
            throw new ODistributedException("Cannot distribute the command '" + iCommand.getText()
                + "' because it is not idempotent and a map-reduce has been requested");

          final Map<String, Collection<String>> nodeClusterMap = dbCfg
              .getServerClusterMap(involvedClusters, localNodeName, exec.isIdempotent());

          final Map<String, Object> results;

          if (exec.isIdempotent() && nodeClusterMap.size() == 1 && nodeClusterMap.keySet().iterator().next()
              .equals(localNodeName)) {
            // LOCAL NODE, AVOID TO DISTRIBUTE IT
            // CALL IN DEFAULT MODE TO LET OWN COMMAND TO REDISTRIBUTE CHANGES (LIKE INSERT)
            result = wrapped.command(iCommand);

            results = new HashMap<String, Object>(1);
            results.put(localNodeName, result);

          } else {
            // SELECT: SPLIT CLASSES/CLUSTER IF ANY
            results = executeOnServers(iCommand, exec, involvedClusters, nodeClusterMap);
          }

          final OCommandExecutorSQLSelect select = exec instanceof OCommandExecutorSQLSelect ?
              (OCommandExecutorSQLSelect) exec :
              null;

          if (select != null && select.isAnyFunctionAggregates() && !select.hasGroupBy()) {
            result = mergeResultByAggregation(select, results);
          } else {
            // MIX & FILTER RESULT SET AVOIDING DUPLICATES
            // TODO: ONCE OPTIMIZED (SEE ABOVE) AVOID TO FILTER HERE

            result = exec.mergeResults(results);
          }

          if (result instanceof Throwable && results.containsKey(localNodeName))
            undoCommandOnLocalServer(iCommand);

        } else {
          final OAbstractCommandTask task = iCommand instanceof OCommandScript ?
              new OScriptTask(iCommand) :
              new OSQLCommandTask(iCommand, new HashSet<String>());
          task.setResultStrategy(OAbstractRemoteTask.RESULT_STRATEGY.ANY);

          final Set<String> nodes = dbCfg.getServers(involvedClusters);

          if (iCommand instanceof ODistributedCommand)
            nodes.removeAll(((ODistributedCommand) iCommand).nodesToExclude());

          if (executeOnlyLocally(localNodeName, dbCfg, exec, involvedClusters, nodes))
            // LOCAL NODE, AVOID TO DISTRIBUTE IT
            // CALL IN DEFAULT MODE TO LET OWN COMMAND TO REDISTRIBUTE CHANGES (LIKE INSERT)
            return wrapped.command(iCommand);

          final Object localResult;

          final boolean executedLocally = executeOnLocalNodeFirst && nodes.contains(localNodeName);

          if (exec.involveSchema())
            // EXECUTE THE COMMAND IN LOCK
            result = dManager
                .executeInDistributedDatabaseLock(getName(), 20000, dManager.getDatabaseConfiguration(getName()).modify(),
                    new OCallable<Object, OModifiableDistributedConfiguration>() {
                      @Override
                      public Object call(OModifiableDistributedConfiguration iArgument) {
                        return executeCommand(iCommand, localNodeName, involvedClusters, task, nodes, executedLocally);
                      }
                    });
          else
            result = executeCommand(iCommand, localNodeName, involvedClusters, task, nodes, executedLocally);
        }

        break;
      }

      if (result instanceof ONeedRetryException)
        throw (ONeedRetryException) result;
      else if (result instanceof RuntimeException)
        throw (RuntimeException) result;
      else if (result instanceof Exception)
        throw OException.wrapException(new ODistributedException("Error on execution distributed COMMAND"), (Exception) result);

      return result;

    } catch (OConcurrentModificationException e) {
      localDistributedDatabase.getDatabaseRepairer().enqueueRepairRecord((ORecordId) e.getRid());
      throw e;
    } catch (ONeedRetryException e) {
      // PASS THROUGH
      throw e;
    }catch (Exception e) {
      handleDistributedException("Cannot route COMMAND operation to the distributed node", e);
      // UNREACHABLE
      return null;
    }
  }

  public void acquireDistributedExclusiveLock(final long timeout) {
    dManager.getLockManagerRequester().acquireExclusiveLock(getName(), dManager.getLocalNodeName(), timeout);
  }

  public void releaseDistributedExclusiveLock() {
    dManager.getLockManagerRequester().releaseExclusiveLock(getName(), dManager.getLocalNodeName());
  }

  protected Object executeCommand(final OCommandRequestText iCommand, String localNodeName, Collection<String> involvedClusters,
      OAbstractCommandTask task, Set<String> nodes, boolean executedLocally) {
    Object localResult;
    Object result;
    if (executedLocally) {
      // EXECUTE ON LOCAL NODE FIRST
      try {

        localResult = OScenarioThreadLocal.executeAsDistributed(new Callable() {
          @Override
          public Object call() throws Exception {
            return wrapped.command(iCommand);
          }
        });

      } catch (RuntimeException e) {
        throw e;
      } catch (Exception e) {
        throw OException.wrapException(new ODistributedException("Cannot execute command " + iCommand), e);
      }
      nodes.remove(localNodeName);
    } else
      localResult = null;

    if (!nodes.isEmpty()) {
      if (ODistributedServerLog.isDebugEnabled())
        ODistributedServerLog.debug(this, dManager.getLocalNodeName(), nodes.toString(), ODistributedServerLog.DIRECTION.OUT,
            "Sending command '%s' database '%s'", iCommand, wrapped.getName());

      final ODistributedResponse dResponse = dManager
          .sendRequest(getName(), involvedClusters, nodes, task, dManager.getNextMessageIdCounter(), EXECUTION_MODE.RESPONSE,
              localResult, null, null);

      result = dResponse.getPayload();

      if (executedLocally && result instanceof Throwable)
        undoCommandOnLocalServer(iCommand);

    } else
      result = localResult;
    return result;
  }

  protected void undoCommandOnLocalServer(final OCommandRequestText iCommand) {
    // UNDO LOCALLY
    OScenarioThreadLocal.executeAsDistributed(new Callable() {
      @Override
      public Object call() throws Exception {
        final OCommandExecutor executor = OCommandManager.instance().getExecutor(iCommand);

        // COPY THE CONTEXT FROM THE REQUEST
        executor.setContext(iCommand.getContext());
        executor.setProgressListener(iCommand.getProgressListener());
        executor.parse(iCommand);

        final String undoCommand = ((OCommandDistributedReplicateRequest) executor).getUndoCommand();
        if (undoCommand != null) {
          wrapped.command(new OCommandSQL(undoCommand));
        }
        return null;
      }
    });
  }

  protected Map<String, Object> executeOnServers(final OCommandRequestText iCommand, final OCommandExecutor exec,
      final Collection<String> involvedClusters, final Map<String, Collection<String>> nodeClusterMap) {

    final Map<String, Object> results = new HashMap<String, Object>(nodeClusterMap.size());

    // EXECUTE DIFFERENT TASK ON EACH SERVER
    final List<String> nodes = new ArrayList<String>(1);
    for (Map.Entry<String, Collection<String>> c : nodeClusterMap.entrySet()) {
      final String nodeName = c.getKey();

      if (!dManager.isNodeAvailable(nodeName, getName())) {

        ODistributedServerLog.debug(this, dManager.getLocalNodeName(), nodeName, ODistributedServerLog.DIRECTION.OUT,
            "Node '%s' is involved in the command '%s' against database '%s', but the node is not active. Excluding it", nodeName,
            iCommand, wrapped.getName());

      } else {

        final OAbstractCommandTask task = iCommand instanceof OCommandScript ?
            new OScriptTask(iCommand) :
            new OSQLCommandTask(iCommand, c.getValue());
        task.setResultStrategy(OAbstractRemoteTask.RESULT_STRATEGY.ANY);

        nodes.clear();
        nodes.add(nodeName);

        try {
          final ODistributedResponse response = dManager
              .sendRequest(getName(), involvedClusters, nodes, task, dManager.getNextMessageIdCounter(), EXECUTION_MODE.RESPONSE,
                  null, null, null);

          if (response != null) {
            if (!(response.getPayload() instanceof ODistributedOperationException))
              // IGNORE ODistributedOperationException EXCEPTION ON SINGLE NODE
              results.put(nodeName, response.getPayload());
          }

        } catch (Exception e) {
          ODistributedServerLog.debug(this, dManager.getLocalNodeName(), nodeName, ODistributedServerLog.DIRECTION.OUT,
              "Error on execution of command '%s' against server '%s', database '%s'", iCommand, nodeName, wrapped.getName());
        }
      }
    }

    if (results.isEmpty())
      throw new ODistributedException("No active nodes found to execute command: " + iCommand);

    return results;
  }

  protected Object mergeResultByAggregation(final OCommandExecutorSQLSelect select, final Map<String, Object> iResults) {
    List<Object> list = null;
    ODocument doc = null;
    boolean hasNonAggregates = false;
    final Map<String, Object> proj = select.getProjections();
    for (Map.Entry<String, Object> p : proj.entrySet()) {
      if (!(p.getValue() instanceof OSQLFunctionRuntime)) {
        hasNonAggregates = true;
        break;
      }
    }

    if (hasNonAggregates) {
      // MERGE NON AGGREGATED FIELDS
      for (Map.Entry<String, Object> entry : iResults.entrySet()) {
        final List<Object> resultSet = (List<Object>) entry.getValue();
        if (resultSet != null) {
          if (list == null) {
            list = new ArrayList<Object>();
            doc = new ODocument();
            list.add(doc);
          }
          for (Object r : resultSet) {
            if (r instanceof ODocument) {
              final ODocument d = (ODocument) r;

              for (Map.Entry<String, Object> p : proj.entrySet()) {
                // WRITE THE FIELD AS IS
                if (!(p.getValue() instanceof OSQLFunctionRuntime))
                  doc.field(p.getKey(), (Object) d.field(p.getKey()));
              }
            }
          }
        }
      }
    }

    final List<Object> toMerge = new ArrayList<Object>();

    // MERGE AGGREGATED FIELDS
    for (Map.Entry<String, Object> p : proj.entrySet()) {
      if (p.getValue() instanceof OSQLFunctionRuntime) {
        // MERGE RESULTS
        final OSQLFunctionRuntime f = (OSQLFunctionRuntime) p.getValue();

        toMerge.clear();
        for (Map.Entry<String, Object> entry : iResults.entrySet()) {
          final List<Object> resultSet = (List<Object>) entry.getValue();
          if (resultSet != null) {
            if (list == null) {
              list = new ArrayList<Object>();
              doc = new ODocument();
              list.add(doc);
            }

            for (Object r : resultSet) {
              if (r instanceof ODocument) {
                final ODocument d = (ODocument) r;
                toMerge.add(d.rawField(p.getKey()));
              }
            }
          }
        }
        if (doc != null) {
          // WRITE THE FINAL MERGED RESULT
          doc.field(p.getKey(), f.getFunction().mergeDistributedResult(toMerge));
        }
      }
    }

    return list;
  }

  /**
   * Only idempotent commands that don't involve any other node can be executed locally.
   */
  protected boolean executeOnlyLocally(final String localNodeName, final ODistributedConfiguration dbCfg,
      final OCommandExecutor exec, final Collection<String> involvedClusters, final Collection<String> nodes) {
    boolean executeLocally = false;
    if (exec.isIdempotent()) {
      final int availableNodes = nodes.size();

      // IDEMPOTENT: CHECK IF CAN WORK LOCALLY ONLY
      int maxReadQuorum;
      if (involvedClusters.isEmpty())
        maxReadQuorum = dbCfg.getReadQuorum(null, availableNodes, localNodeName);
      else {
        maxReadQuorum = 0;
        for (String cl : involvedClusters)
          maxReadQuorum = Math.max(maxReadQuorum, dbCfg.getReadQuorum(cl, availableNodes, localNodeName));
      }

      if (nodes.contains(localNodeName) && maxReadQuorum <= 1)
        executeLocally = true;
    }

    return executeLocally;
  }

  public boolean isLocalEnv() {
    return localDistributedDatabase == null || dManager == null || distributedConfiguration == null || OScenarioThreadLocal.INSTANCE
        .isRunModeDistributed();
  }

  public OStorageOperationResult<ORawBuffer> readRecord(final ORecordId iRecordId, final String iFetchPlan,
      final boolean iIgnoreCache, final boolean prefetchRecords, final ORecordCallback<ORawBuffer> iCallback) {

    if (isLocalEnv()) {
      // ALREADY DISTRIBUTED
      return wrapped.readRecord(iRecordId, iFetchPlan, iIgnoreCache, prefetchRecords, iCallback);
    }

    final ORawBuffer memCopy = localDistributedDatabase.getRecordIfLocked(iRecordId);
    if (memCopy != null)
      return new OStorageOperationResult<ORawBuffer>(memCopy);

    try {
      final String clusterName = getClusterNameByRID(iRecordId);

      final ODistributedConfiguration dbCfg = distributedConfiguration;
      final List<String> nodes = dbCfg.getServers(clusterName, null);
      final int availableNodes = nodes.size();

      // CHECK IF LOCAL NODE OWNS THE DATA AND READ-QUORUM = 1: GET IT LOCALLY BECAUSE IT'S FASTER
      final String localNodeName = dManager.getLocalNodeName();

      if (nodes.isEmpty()
          || nodes.contains(dManager.getLocalNodeName()) && dbCfg.getReadQuorum(clusterName, availableNodes, localNodeName) <= 1) {
        // DON'T REPLICATE
        return (OStorageOperationResult<ORawBuffer>) OScenarioThreadLocal.executeAsDistributed(new Callable() {
          @Override
          public Object call() throws Exception {
            return wrapped.readRecord(iRecordId, iFetchPlan, iIgnoreCache, prefetchRecords, iCallback);
          }
        });
      }

      final OReadRecordTask task = ((OReadRecordTask) dManager.getTaskFactoryManager().getFactoryByServerNames(nodes)
          .createTask(OReadRecordTask.FACTORYID)).init(iRecordId);

      // DISTRIBUTE IT
      final ODistributedResponse response = dManager
          .sendRequest(getName(), Collections.singleton(clusterName), nodes, task, dManager.getNextMessageIdCounter(),
              EXECUTION_MODE.RESPONSE, null, null, null);
      final Object dResult = response != null ? response.getPayload() : null;

      if (dResult instanceof ONeedRetryException)
        throw (ONeedRetryException) dResult;
      else if (dResult instanceof Exception)
        throw OException
            .wrapException(new ODistributedException("Error on execution distributed read record"), (Exception) dResult);

      return new OStorageOperationResult<ORawBuffer>((ORawBuffer) dResult);

    } catch (ONeedRetryException e) {
      // PASS THROUGH
      throw e;
    } catch (Exception e) {
      handleDistributedException("Cannot route read record operation for %s to the distributed node", e, iRecordId);
      // UNREACHABLE
      return null;
    }
  }

  @Override
  public OStorageOperationResult<ORawBuffer> readRecordIfVersionIsNotLatest(final ORecordId rid, final String fetchPlan,
      final boolean ignoreCache, final int recordVersion) throws ORecordNotFoundException {
    if (isLocalEnv()) {
      return wrapped.readRecordIfVersionIsNotLatest(rid, fetchPlan, ignoreCache, recordVersion);
    }
    final ORawBuffer memCopy = localDistributedDatabase.getRecordIfLocked(rid);
    if (memCopy != null)
      return new OStorageOperationResult<ORawBuffer>(memCopy);

    try {
      final String clusterName = getClusterNameByRID(rid);

      final ODistributedConfiguration dbCfg = distributedConfiguration;
      final List<String> nodes = dbCfg.getServers(clusterName, null);
      final int availableNodes = nodes.size();

      // CHECK IF LOCAL NODE OWNS THE DATA AND READ-QUORUM = 1: GET IT LOCALLY BECAUSE IT'S FASTER
      final String localNodeName = dManager.getLocalNodeName();

      if (nodes.isEmpty()
          || nodes.contains(dManager.getLocalNodeName()) && dbCfg.getReadQuorum(clusterName, availableNodes, localNodeName) <= 1) {
        // DON'T REPLICATE
        return (OStorageOperationResult<ORawBuffer>) OScenarioThreadLocal.executeAsDistributed(new Callable() {
          @Override
          public Object call() throws Exception {
            return wrapped.readRecordIfVersionIsNotLatest(rid, fetchPlan, ignoreCache, recordVersion);
          }
        });
      }

      final OReadRecordIfNotLatestTask task = (OReadRecordIfNotLatestTask) dManager.getTaskFactoryManager()
          .getFactoryByServerNames(nodes).createTask(OReadRecordIfNotLatestTask.FACTORYID);
      task.init(rid, recordVersion);

      // DISTRIBUTE IT
      final Object result = dManager
          .sendRequest(getName(), Collections.singleton(clusterName), nodes, task, dManager.getNextMessageIdCounter(),
              EXECUTION_MODE.RESPONSE, null, null, null).getPayload();

      if (result instanceof ONeedRetryException)
        throw (ONeedRetryException) result;
      else if (result instanceof Exception)
        throw OException.wrapException(new ODistributedException("Error on execution distributed read record"), (Exception) result);

      return new OStorageOperationResult<ORawBuffer>((ORawBuffer) result);

    } catch (ONeedRetryException e) {
      // PASS THROUGH
      throw e;
    } catch (Exception e) {
      handleDistributedException("Cannot route read record operation for %s to the distributed node", e, rid);
      // UNREACHABLE
      return null;
    }
  }

  @Override
  public OStorageOperationResult<Boolean> deleteRecord(final ORecordId iRecordId, final int iVersion, final int iMode,
      final ORecordCallback<Boolean> iCallback) {
    // IF is a real delete should be with a tx
    return wrapped.deleteRecord(iRecordId, iVersion, iMode, iCallback);
  }

  @Override
  public OSBTreeCollectionManager getSBtreeCollectionManager() {
    return wrapped.getSBtreeCollectionManager();
  }

  @Override
  public OStorageOperationResult<Integer> recyclePosition(ORecordId iRecordId, byte[] iContent, int iVersion, byte iRecordType) {
    return wrapped.recyclePosition(iRecordId, iContent, iVersion, iRecordType);
  }

  public int getConfigurationUpdated() {
    return distributedConfiguration.getVersion();
  }

  @Override
  public OStorageOperationResult<Boolean> hideRecord(ORecordId recordId, int mode, ORecordCallback<Boolean> callback) {
    throw new UnsupportedOperationException();
  }

  @Override
  public ORecordMetadata getRecordMetadata(ORID rid) {
    return wrapped.getRecordMetadata(rid);
  }

  @Override
  public boolean cleanOutRecord(ORecordId recordId, final int recordVersion, int iMode, ORecordCallback<Boolean> callback) {
    return wrapped.cleanOutRecord(recordId, recordVersion, iMode, callback);
  }

  @Override
  public boolean existsResource(final String iName) {
    return wrapped.existsResource(iName);
  }

  public OCluster getClusterByName(final String iName) {
    return wrapped.getClusterByName(iName);
  }

  @Override
  public ORecordConflictStrategy getConflictStrategy() {
    return getUnderlying().getConflictStrategy();
  }

  @Override
  public void setConflictStrategy(final ORecordConflictStrategy iResolver) {
    getUnderlying().setConflictStrategy(iResolver);
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T> T removeResource(final String iName) {
    return (T) wrapped.removeResource(iName);
  }

  @Override
  public <T> T getResource(final String iName, final Callable<T> iCallback) {
    return (T) wrapped.getResource(iName, iCallback);
  }

  @Override
  public void open(final String iUserName, final String iUserPassword, final OContextConfiguration iProperties) {
    wrapped.open(iUserName, iUserPassword, iProperties);
  }

  @Override
  public void create(final OContextConfiguration iProperties) throws IOException {
    wrapped.create(iProperties);
  }

  @Override
  public boolean exists() {
    return wrapped.exists();
  }

  @Override
  public void reload() {
    wrapped.reload();
  }

  @Override
  public void delete() {
    if (wrapped instanceof OLocalPaginatedStorage)
      dropStorageFiles();

    wrapped.delete();
  }

  @Override
  public String incrementalBackup(final String backupDirectory, OCallable<Void, Void> started) {
    return wrapped.incrementalBackup(backupDirectory, started);
  }

  @Override
  public void restoreFromIncrementalBackup(final String filePath) {
    wrapped.restoreFromIncrementalBackup(filePath);
  }

  @Override
  public void close() {
    close(false, false);
  }

  @Override
  public void close(final boolean iForce, final boolean onDelete) {
    if (wrapped == null)
      return;

    if (onDelete && wrapped instanceof OLocalPaginatedStorage) {
      // REMOVE distributed-config.json and distributed-sync.json files to allow removal of directory
      dropStorageFiles();
    }

    wrapped.close(iForce, onDelete);

  }

  public void closeOnDrop() {
    if (wrapped == null)
      return;
    if (wrapped instanceof OLocalPaginatedStorage) {
      // REMOVE distributed-config.json and distributed-sync.json files to allow removal of directory
      dropStorageFiles();
    }

  }

  @Override
  public boolean isClosed() {
    if (wrapped == null)
      return true;

    return wrapped.isClosed();
  }

  @Override
  public List<ORecordOperation> commit(final OTransactionInternal iTx) {
    return wrapped.commit(iTx);
  }

  @Override
  public void rollback(final OTransactionInternal iTx) {
    wrapped.rollback(iTx);
  }

  @Override
  public OStorageConfiguration getConfiguration() {
    return wrapped.getConfiguration();
  }

  @Override
  public int getClusters() {
    return wrapped.getClusters();
  }

  @Override
  public Set<String> getClusterNames() {
    return wrapped.getClusterNames();
  }

  @Override
  public OCluster getClusterById(int iId) {
    return wrapped.getClusterById(iId);
  }

  @Override
  public Collection<? extends OCluster> getClusterInstances() {
    return wrapped.getClusterInstances();
  }

  @Override
  public int addCluster(final String iClusterName, final Object... iParameters) {
    return wrapped.addCluster(iClusterName, iParameters);
  }

  @Override
  public int addCluster(String iClusterName, int iRequestedId, Object... iParameters) {
    resetLastValidBackup();
    for (int retry = 0; retry < 10; ++retry) {
      final AtomicInteger clId = new AtomicInteger();

      if (!isLocalEnv()) {

        final StringBuilder cmd = new StringBuilder("create cluster `");
        cmd.append(iClusterName);
        cmd.append("`");
        cmd.append(" ID ");
        cmd.append(iRequestedId);

        // EXECUTE THIS OUTSIDE LOCK TO AVOID DEADLOCKS
        Object result = null;
        try {
          result = dManager
              .executeInDistributedDatabaseLock(getName(), 20000, dManager.getDatabaseConfiguration(getName()).modify(),
                  new OCallable<Object, OModifiableDistributedConfiguration>() {
                    @Override
                    public Object call(OModifiableDistributedConfiguration iArgument) {
                      clId.set(wrapped.addCluster(iClusterName, iRequestedId, iParameters));

                      final OCommandSQL commandSQL = new OCommandSQL(cmd.toString());
                      commandSQL.addExcludedNode(getNodeId());
                      return command(commandSQL);
                    }
                  });
        } catch (Exception e) {
          // RETRY
          wrapped.dropCluster(iClusterName, false);

          try {
            Thread.sleep(300);
          } catch (InterruptedException e2) {
          }

          continue;
        }

        if (result != null && ((Integer) result).intValue() != clId.get()) {
          ODistributedServerLog.warn(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
              "Error on creating cluster '%s' on distributed nodes: ids are different (local=%d and remote=%d). Local clusters %s. Retrying %d/%d...",
              iClusterName, clId.get(), ((Integer) result).intValue(), getClusterNames(), retry, 10);

          wrapped.dropCluster(clId.get(), false);

          // REMOVE ON REMOTE NODES TOO
          cmd.setLength(0);
          cmd.append("drop cluster ");
          cmd.append(iClusterName);

          final OCommandSQL commandSQL = new OCommandSQL(cmd.toString());
          commandSQL.addExcludedNode(getNodeId());
          command(commandSQL);

          try {
            Thread.sleep(300);
          } catch (InterruptedException e) {
          }

          wrapped.reload(); // TODO: RELOAD DOESN'T DO ANYTHING WHILE HERE IT'S NEEDED A WAY TO CLOSE/OPEN THE DB
          continue;
        }
      } else
        clId.set(wrapped.addCluster(iClusterName, iRequestedId, iParameters));
      return clId.get();
    }

    throw new ODistributedException(
        "Error on creating cluster '" + iClusterName + "' on distributed nodes: local and remote ids assigned are different");
  }

  public boolean dropCluster(final String iClusterName, final boolean iTruncate) {
    resetLastValidBackup();
    final AtomicBoolean clId = new AtomicBoolean();

    if (!isLocalEnv()) {

      final StringBuilder cmd = new StringBuilder();
      if (iTruncate) {
        cmd.append("truncate cluster `");
      } else {
        cmd.append("create cluster `");
      }
      cmd.append(iClusterName);
      cmd.append("`");

      // EXECUTE THIS OUTSIDE LOCK TO AVOID DEADLOCKS
      dManager.executeInDistributedDatabaseLock(getName(), 20000, dManager.getDatabaseConfiguration(getName()).modify(),
          new OCallable<Object, OModifiableDistributedConfiguration>() {
            @Override
            public Object call(OModifiableDistributedConfiguration iArgument) {
              clId.set(wrapped.dropCluster(iClusterName, iTruncate));

              final OCommandSQL commandSQL = new OCommandSQL(cmd.toString());
              commandSQL.addExcludedNode(getNodeId());
              return command(commandSQL);
            }
          });
      return clId.get();
    } else
      return wrapped.dropCluster(iClusterName, iTruncate);
  }

  @Override
  public boolean dropCluster(final int iId, final boolean iTruncate) {
    resetLastValidBackup();
    return wrapped.dropCluster(iId, iTruncate);
  }

  @Override
  public long count(final int iClusterId) {
    return wrapped.count(iClusterId);
  }

  @Override
  public long count(int iClusterId, boolean countTombstones) {
    return wrapped.count(iClusterId, countTombstones);
  }

  public long count(final int[] iClusterIds) {
    return wrapped.count(iClusterIds);
  }

  @Override
  public long count(int[] iClusterIds, boolean countTombstones) {
    // TODO: SUPPORT SHARDING HERE
    return wrapped.count(iClusterIds, countTombstones);
  }

  @Override
  public long getSize() {
    return wrapped.getSize();
  }

  @Override
  public long countRecords() {
    return wrapped.countRecords();
  }

  @Override
  public int getDefaultClusterId() {
    return wrapped.getDefaultClusterId();
  }

  @Override
  public void setDefaultClusterId(final int defaultClusterId) {
    wrapped.setDefaultClusterId(defaultClusterId);
  }

  @Override
  public int getClusterIdByName(String iClusterName) {
    return wrapped.getClusterIdByName(iClusterName);
  }

  @Override
  public String getPhysicalClusterNameById(final int iClusterId) {
    return wrapped.getPhysicalClusterNameById(iClusterId);
  }

  @Override
  public boolean checkForRecordValidity(final OPhysicalPosition ppos) {
    return wrapped.checkForRecordValidity(ppos);
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public String getURL() {
    return wrapped.getURL();
  }

  @Override
  public long getVersion() {
    return wrapped.getVersion();
  }

  @Override
  public void synch() {
    wrapped.synch();
  }

  @Override
  public long[] getClusterDataRange(final int currentClusterId) {
    return wrapped.getClusterDataRange(currentClusterId);
  }

  @Override
  public <V> V callInLock(final Callable<V> iCallable, final boolean iExclusiveLock) {
    return wrapped.callInLock(iCallable, iExclusiveLock);
  }

  public STATUS getStatus() {
    return wrapped.getStatus();
  }

  public ODistributedStorageEventListener getEventListener() {
    return eventListener;
  }

  public void setEventListener(final ODistributedStorageEventListener eventListener) {
    this.eventListener = eventListener;
  }

  @Override
  public OPhysicalPosition[] higherPhysicalPositions(int currentClusterId, OPhysicalPosition entry) {
    return wrapped.higherPhysicalPositions(currentClusterId, entry);
  }

  public OServer getServer() {
    return serverInstance;
  }

  public ODistributedServerManager getDistributedManager() {
    return dManager;
  }

  public ODistributedConfiguration getDistributedConfiguration() {
    return null;
  }

  public void setDistributedConfiguration(final OModifiableDistributedConfiguration distributedConfiguration) {
    if (this.distributedConfiguration == null || distributedConfiguration.getVersion() > this.distributedConfiguration
        .getVersion()) {
      this.distributedConfiguration = new ODistributedConfiguration(distributedConfiguration.getDocument().copy());

      // PRINT THE NEW CONFIGURATION
      final String cfgOutput = ODistributedOutput
          .formatClusterTable(dManager, getName(), distributedConfiguration, dManager.getTotalNodes(getName()));

      ODistributedServerLog.info(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
          "Setting new distributed configuration for database: %s (version=%d)%s\n", getName(),
          distributedConfiguration.getVersion(), cfgOutput);

      saveDatabaseConfiguration();
    }
  }

  @Override
  public OPhysicalPosition[] ceilingPhysicalPositions(int clusterId, OPhysicalPosition physicalPosition) {
    return wrapped.ceilingPhysicalPositions(clusterId, physicalPosition);
  }

  @Override
  public OPhysicalPosition[] floorPhysicalPositions(int clusterId, OPhysicalPosition physicalPosition) {
    return wrapped.floorPhysicalPositions(clusterId, physicalPosition);
  }

  @Override
  public OPhysicalPosition[] lowerPhysicalPositions(int currentClusterId, OPhysicalPosition entry) {
    return wrapped.lowerPhysicalPositions(currentClusterId, entry);
  }

  public OStorage getUnderlying() {
    return wrapped;
  }

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

  @Override
  public OCurrentStorageComponentsFactory getComponentsFactory() {
    return wrapped.getComponentsFactory();
  }

  @Override
  public String getType() {
    return "distributed";
  }

  @Override
  public void freeze(final boolean throwException) {
    final String localNode = dManager.getLocalNodeName();

    prevStatus = dManager.getDatabaseStatus(localNode, getName());
    if (prevStatus == ODistributedServerManager.DB_STATUS.ONLINE)
      // SET STATUS = BACKUP
      dManager.setDatabaseStatus(localNode, getName(), ODistributedServerManager.DB_STATUS.BACKUP);

    getFreezableStorage().freeze(throwException);
  }

  @Override
  public boolean isFrozen() {
    return getFreezableStorage().isFrozen();
  }

  @Override
  public void release() {
    final String localNode = dManager.getLocalNodeName();

    if (prevStatus == ODistributedServerManager.DB_STATUS.ONLINE)
      // RESTORE PREVIOUS STATUS
      dManager.setDatabaseStatus(localNode, getName(), ODistributedServerManager.DB_STATUS.ONLINE);

    getFreezableStorage().release();
  }

  @Override
  public List<String> backup(final OutputStream out, final Map<String, Object> options, final Callable<Object> callable,
      final OCommandOutputListener iListener, final int compressionLevel, final int bufferSize) throws IOException {
    // THIS CAUSES DEADLOCK
    // try {
    // return (List<String>) executeOperationInLock(new OCallable<Object, Void>() {
    // @Override
    // public Object call(Void iArgument) {
    final String localNode = dManager.getLocalNodeName();

    final ODistributedServerManager.DB_STATUS prevStatus = dManager.getDatabaseStatus(localNode, getName());
    if (prevStatus == ODistributedServerManager.DB_STATUS.ONLINE)
      // SET STATUS = BACKUP
      dManager.setDatabaseStatus(localNode, getName(), ODistributedServerManager.DB_STATUS.BACKUP);

    try {

      return wrapped.backup(out, options, callable, iListener, compressionLevel, bufferSize);

    } catch (IOException e) {
      throw OException.wrapException(new OIOException("Error on executing backup"), e);
    } finally {
      if (prevStatus == ODistributedServerManager.DB_STATUS.ONLINE)
        // RESTORE PREVIOUS STATUS
        dManager.setDatabaseStatus(localNode, getName(), ODistributedServerManager.DB_STATUS.ONLINE);
    }
    // }
    // });
    // } catch (InterruptedException e) {
    // Thread.currentThread().interrupt();
    // throw OException.wrapException(new OIOException("Backup interrupted"), e);
    // }
  }

  @Override
  public void restore(final InputStream in, final Map<String, Object> options, final Callable<Object> callable,
      final OCommandOutputListener iListener) throws IOException {
    wrapped.restore(in, options, callable, iListener);
  }

  public String getClusterNameByRID(final ORecordId iRid) {
    final OCluster cluster = getClusterById(iRid.getClusterId());
    return cluster != null ? cluster.getName() : "*";
  }

  @Override
  public String getStorageId() {
    return dManager.getLocalNodeName() + "." + getName();
  }

  @Override
  public String getNodeId() {
    return dManager != null ? dManager.getLocalNodeName() : "?";
  }

  @Override
  public void shutdown() {
    close(true, false);
  }

  protected void checkNodeIsMaster(final String localNodeName, final ODistributedConfiguration dbCfg, final String operation) {
    final ODistributedConfiguration.ROLES nodeRole = dbCfg.getServerRole(localNodeName);
    if (nodeRole != ODistributedConfiguration.ROLES.MASTER)
      throw new OWriteOperationNotPermittedException(
          "Cannot execute write operation (" + operation + ") on node '" + localNodeName + "' because is non a master");
  }

  public OBackgroundBackup getLastValidBackup() {
    return lastValidBackup;
  }

  public void setLastValidBackup(final OBackgroundBackup lastValidBackup) {
    this.lastValidBackup = lastValidBackup;
  }

  protected void handleDistributedException(final String iMessage, final Exception e, final Object... iParams) {
    if (e != null) {
      if (e instanceof OException)
        throw (OException) e;
      else if (e.getCause() instanceof OException)
        throw (OException) e.getCause();
      else if (e.getCause() != null && e.getCause().getCause() instanceof OException)
        throw (OException) e.getCause().getCause();
    }

    OLogManager.instance().error(this, iMessage, e, iParams);
    throw OException.wrapException(new OStorageException(String.format(iMessage, iParams)), e);
  }

  private OFreezableStorageComponent getFreezableStorage() {
    if (wrapped instanceof OFreezableStorageComponent)
      return ((OFreezableStorageComponent) wrapped);
    else
      throw new UnsupportedOperationException("Storage engine " + wrapped.getType() + " does not support freeze operation");
  }

  public void resetLastValidBackup() {
    lastValidBackup = null;
  }

  protected void dropStorageFiles() {
    dropStorageFiles((OLocalPaginatedStorage) wrapped);
  }

  public static void dropStorageFiles(OLocalPaginatedStorage storage) {
    // REMOVE distributed-config.json and distributed-sync.json files to allow removal of directory
    final File dCfg = new File(storage.getStoragePath() + "/" + ODistributedServerManager.FILE_DISTRIBUTED_DB_CONFIG);

    try {
      if (dCfg.exists()) {
        for (int i = 0; i < 10; ++i) {
          if (dCfg.delete())
            break;
          Thread.sleep(100);
        }
      }

      final File dCfg2 = new File(storage.getStoragePath() + "/" + ODistributedDatabaseImpl.DISTRIBUTED_SYNC_JSON_FILENAME);
      if (dCfg2.exists()) {
        for (int i = 0; i < 10; ++i) {
          if (dCfg2.delete())
            break;
          Thread.sleep(100);
        }
      }
    } catch (InterruptedException e) {
      // IGNORE IT
    }
  }

  public ODocument loadDatabaseConfiguration(final File file) {
    if (!file.exists() || file.length() == 0)
      return null;

    ODistributedServerLog.info(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
        "Loaded configuration for database '%s' from disk: %s", getName(), file);

    FileInputStream f = null;
    try {
      f = new FileInputStream(file);
      final byte[] buffer = new byte[(int) file.length()];
      f.read(buffer);

      final ODocument doc = (ODocument) new ODocument().fromJSON(new String(buffer), "noMap");
      doc.field("version", 1);
      return doc;

    } catch (Exception e) {
      ODistributedServerLog.error(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
          "Error on loading distributed configuration file in: %s", e, file.getAbsolutePath());
    } finally {
      if (f != null)
        try {
          f.close();
        } catch (IOException e) {
        }
    }
    return null;
  }

  protected void saveDatabaseConfiguration() {
    // SAVE THE CONFIGURATION TO DISK
    FileOutputStream f = null;
    try {
      File file = getDistributedConfigFile();

      ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
          "Saving distributed configuration file for database '%s' to: %s", getName(), file);

      if (!file.exists()) {
        file.getParentFile().mkdirs();
        file.createNewFile();
      }

      f = new FileOutputStream(file);
      f.write(distributedConfiguration.getDocument().toJSON().getBytes());
      f.flush();
    } catch (Exception e) {
      ODistributedServerLog.error(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
          "Error on saving distributed configuration file", e);

    } finally {
      if (f != null)
        try {
          f.close();
        } catch (IOException e) {
        }
    }
  }

  @Override
  public void restoreFullIncrementalBackup(InputStream stream) throws UnsupportedOperationException {
    wrapped.restoreFullIncrementalBackup(stream);
  }

  @Override
  public boolean supportIncremental() {
    return wrapped.supportIncremental();
  }

  @Override
  public void fullIncrementalBackup(OutputStream stream) throws UnsupportedOperationException {
    wrapped.fullIncrementalBackup(stream);
  }

  protected File getDistributedConfigFile() {
    return new File(serverInstance.getDatabaseDirectory() + getName() + "/" + ODistributedServerManager.FILE_DISTRIBUTED_DB_CONFIG);
  }

  public ODistributedDatabase getLocalDistributedDatabase() {
    return localDistributedDatabase;
  }

  @Override
  public void setSchemaRecordId(String schemaRecordId) {
    wrapped.setSchemaRecordId(schemaRecordId);
  }

  @Override
  public void setDateFormat(String dateFormat) {
    wrapped.setDateFormat(dateFormat);
  }

  @Override
  public void setTimeZone(TimeZone timeZoneValue) {
    wrapped.setTimeZone(timeZoneValue);
  }

  @Override
  public void setLocaleLanguage(String locale) {
    wrapped.setLocaleLanguage(locale);
  }

  @Override
  public void setCharset(String charset) {
    wrapped.setCharset(charset);
  }

  @Override
  public void setIndexMgrRecordId(String indexMgrRecordId) {
    wrapped.setIndexMgrRecordId(indexMgrRecordId);
  }

  @Override
  public void setDateTimeFormat(String dateTimeFormat) {
    wrapped.setDateTimeFormat(dateTimeFormat);
  }

  @Override
  public void setLocaleCountry(String localeCountry) {
    wrapped.setLocaleCountry(localeCountry);
  }

  @Override
  public void setClusterSelection(String clusterSelection) {
    wrapped.setClusterSelection(clusterSelection);
  }

  @Override
  public void setMinimumClusters(int minimumClusters) {
    wrapped.setMinimumClusters(minimumClusters);
  }

  @Override
  public void setValidation(boolean validation) {
    wrapped.setValidation(validation);
  }

  @Override
  public void removeProperty(String property) {
    wrapped.removeProperty(property);
  }

  @Override
  public void setProperty(String property, String value) {
    wrapped.setProperty(property, value);
  }

  @Override
  public void setRecordSerializer(String recordSerializer, int version) {
    wrapped.setRecordSerializer(recordSerializer, version);
  }

  @Override
  public void clearProperties() {
    wrapped.clearProperties();
  }
}


/*
 *
 *  *  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.object.jpa.parsing;

import java.net.URL;
import java.util.Collection;
import java.util.Stack;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import com.orientechnologies.orient.object.jpa.OJPAPersistenceUnitInfo;

/**
 * This code is responsible for parsing the persistence.xml into PersistenceUnits
 */
public class JPAHandler extends DefaultHandler {
  /**
   * The Persistence Units that we have parsed
   */
  private final Stack<OJPAPersistenceUnitInfo> persistenceUnits = new Stack<OJPAPersistenceUnitInfo>();
  /**
   * root of the persistence unit
   */
  private final URL                            persistenceXmlRoot;
  /**
   * The version of the persistence.xml file
   */
  private       String                         xmlSchemaVersion;
  /**
   * The name of the current element
   */
  private       PersistenceXml                 element;
  /**
   * A StringBuilder for caching the information from getCharacters
   */
  private       StringBuilder                  builder          = new StringBuilder();

  // /**
  // * Create a new JPA Handler for the given persistence.xml. If no xmlSchemaVersion defined it will be parsed
  // */
  // public JPAHandler() {
  // }

  /**
   * Create a new JPA Handler for the given persistence.xml
   *
   * @param persistenceXmlRoot
   * @param jpaVersion         the version of the JPA schema used in the xml
   */
  public JPAHandler(URL persistenceXmlRoot, JPAVersion jpaVersion) {
    this.persistenceXmlRoot = persistenceXmlRoot;
    this.xmlSchemaVersion = jpaVersion.getVersion();
  }

  @Override
  public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
    // Do this setting first as we use it later.
    element = PersistenceXml.parse((localName == null || localName.isEmpty()) ? name : localName);

    switch (element) {
    case TAG_PERSISTENCE:
      if (xmlSchemaVersion == null) {
        xmlSchemaVersion = PersistenceXmlUtil.parseSchemaVersion(uri, element, attributes);
      }
      break;
    case TAG_PERSISTENCE_UNIT:
      String unitName = attributes.getValue(PersistenceXml.ATTR_UNIT_NAME.toString());
      String transactionType = attributes.getValue(PersistenceXml.ATTR_TRANSACTION_TYPE.toString());
      persistenceUnits.push(new OJPAPersistenceUnitInfo(unitName, transactionType, persistenceXmlRoot, xmlSchemaVersion));
      break;
    case TAG_EXCLUDE_UNLISTED_CLASSES:
      persistenceUnits.peek().setExcludeUnlisted(true);
      break;
    case TAG_PROPERTY:
      persistenceUnits.peek().addProperty(attributes.getValue("name"), attributes.getValue("value"));
      break;
    default:
    }

  }

  @Override
  public void endElement(String uri, String localName, String name) throws SAXException {
    String s = builder.toString().trim();
    // This step is VERY important, otherwise we pollute subsequent
    // ELEMENTS
    builder = new StringBuilder();

    if (s.isEmpty()) {
      return;
    }

    OJPAPersistenceUnitInfo pu = persistenceUnits.peek();

    switch (element) {
    case TAG_PROVIDER:
      pu.setProviderClassName(s);
      break;
    case TAG_JTA_DATA_SOURCE:
      pu.setJtaDataSource(s);
      break;
    case TAG_NON_JTA_DATA_SOURCE:
      pu.setNonJtaDataSource(s);
      break;
    case TAG_MAPPING_FILE:
      pu.addMappingFileName(s);
      break;
    case TAG_JAR_FILE:
      pu.addJarFileName(s);
      break;
    case TAG_CLASS:
      pu.addClassName(s);
      break;
    case TAG_EXCLUDE_UNLISTED_CLASSES:
      pu.setExcludeUnlisted(Boolean.parseBoolean(s));
      break;
    case TAG_SHARED_CACHE_MODE:
      pu.setSharedCacheMode(s);
      break;
    case TAG_VALIDATION_MODE:
      pu.setValidationMode(s);
      break;
    default:
    }
  }

  /**
   * Collect up the characters, as element's characters may be split across multiple calls. Isn't SAX lovely...
   */
  @Override
  public void characters(char[] ch, int start, int length) throws SAXException {
    builder.append(ch, start, length);
  }

  /**
   * We throw this exception to be caught further up and logged as an error there
   */
  @Override
  public void error(SAXParseException spe) throws SAXException {
    throw spe;
  }

  /**
   * @return The collection of persistence units that we have parsed
   */
  public Collection<OJPAPersistenceUnitInfo> getPersistenceUnits() {
    return persistenceUnits;
  }
}


package com.orientechnologies.lucene.builder;

import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.util.ODateHelper;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * This utility class replace the {@link org.apache.lucene.document.DateTools} from Lucene code base.
 *

 * It uses the {@link java.util.TimeZone} defined at database level and maintains only methods for string conversion to {@link Date}
 * or long value.
 *
 * @author frank
 */
public class OLuceneDateTools {

  //indexed by format length
  private static final Resolution[] RESOLUTIONS;

  static {
    RESOLUTIONS = new Resolution[Resolution.MILLISECOND.formatLen + 1];
    for (Resolution resolution : Resolution.values()) {
      RESOLUTIONS[resolution.formatLen] = resolution;
    }

  }

  // cannot create, the class has static methods only
  private OLuceneDateTools() {
  }

  /**
   * Converts a string produced by timeToString or
   * dateToString back to a time, represented as the
   * number of milliseconds since January 1, 1970, 00:00:00 GMT.
   *
   * @param dateString the date string to be converted
   *
   * @return the number of milliseconds since January 1, 1970, 00:00:00 GMT
   *
   * @throws ParseException if dateString is not in the expected format
   */
  public static long stringToTime(String dateString) throws ParseException {
    return stringToDate(dateString).getTime();
  }

  /**
   * Converts a string produced by timeToString or
   * dateToString back to a time, represented as a
   * Date object.
   *
   * @param dateString the date string to be converted
   *
   * @return the parsed time as a Date object
   *
   * @throws ParseException if dateString is not in the expected format
   */
  public static Date stringToDate(String dateString) throws ParseException {
    try {
      SimpleDateFormat format = RESOLUTIONS[dateString.length()].format();
      return format.parse(dateString);
    } catch (Exception e) {
      OLogManager.instance().error(OLuceneDateTools.class, "Exception is suppressed, original exception is ", e);

      //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
      throw new ParseException("Input is not a valid date string: " + dateString, 0);
    }
  }

  /**
   * Specifies the time granularity.
   */
  public enum Resolution {

    /**
     * Limit a date's resolution to year granularity.
     */
    YEAR(4),
    /**
     * Limit a date's resolution to month granularity.
     */
    MONTH(6),
    /**
     * Limit a date's resolution to day granularity.
     */
    DAY(8),
    /**
     * Limit a date's resolution to hour granularity.
     */
    HOUR(10),
    /**
     * Limit a date's resolution to minute granularity.
     */
    MINUTE(12),
    /**
     * Limit a date's resolution to second granularity.
     */
    SECOND(14),
    /**
     * Limit a date's resolution to millisecond granularity.
     */
    MILLISECOND(17);

    final int formatLen;

    Resolution(int formatLen) {
      this.formatLen = formatLen;
    }

    public SimpleDateFormat format() {
      // formatLen 10's place:                     11111111
      // formatLen  1's place:            12345678901234567

      SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSS".substring(0, formatLen), Locale.ROOT);
      format.setTimeZone(ODateHelper.getDatabaseTimeZone());

      return format;
    }

    /**
     * this method returns the name of the resolution
     * in lowercase (for backwards compatibility)
     */
    @Override
    public String toString() {
      return super.toString().toLowerCase(Locale.ROOT);
    }

  }

}