/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  Licensed under the Apache License, Version 2.0 (the "License");
 *  *  you may not use this file except in compliance with the License.
 *  *  You may obtain a copy of the License at
 *  *
 *  *       http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *  Unless required by applicable law or agreed to in writing, software
 *  *  distributed under the License is distributed on an "AS IS" BASIS,
 *  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *  See the License for the specific language governing permissions and
 *  *  limitations under the License.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.storage.index.hashindex.local;

import com.orientechnologies.common.comparator.ODefaultComparator;
import com.orientechnologies.common.serialization.types.OBinarySerializer;
import com.orientechnologies.common.serialization.types.OByteSerializer;
import com.orientechnologies.common.serialization.types.OIntegerSerializer;
import com.orientechnologies.common.serialization.types.OLongSerializer;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.encryption.OEncryption;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.storage.cache.OCacheEntry;
import com.orientechnologies.orient.core.storage.impl.local.paginated.base.ODurablePage;

import java.io.IOException;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * @author Andrey Lomakin (a.lomakin-at-orientdb.com)
 * @since 2/17/13
 */
public class OHashIndexBucket<K, V> extends ODurablePage implements Iterable<OHashIndexBucket.Entry<K, V>> {
  private static final int FREE_POINTER_OFFSET = NEXT_FREE_POSITION;
  private static final int DEPTH_OFFSET        = FREE_POINTER_OFFSET + OIntegerSerializer.INT_SIZE;
  private static final int SIZE_OFFSET         = DEPTH_OFFSET + OByteSerializer.BYTE_SIZE;
  private static final int HISTORY_OFFSET      = SIZE_OFFSET + OIntegerSerializer.INT_SIZE;

  private static final int NEXT_REMOVED_BUCKET_OFFSET = HISTORY_OFFSET + OLongSerializer.LONG_SIZE * 64;
  private static final int POSITIONS_ARRAY_OFFSET     = NEXT_REMOVED_BUCKET_OFFSET + OLongSerializer.LONG_SIZE;

  public static final int MAX_BUCKET_SIZE_BYTES = OGlobalConfiguration.DISK_CACHE_PAGE_SIZE.getValueAsInteger() * 1024;

  private final OBinarySerializer<K> keySerializer;
  private final OBinarySerializer<V> valueSerializer;
  private final OType[]              keyTypes;
  private final Comparator           keyComparator = ODefaultComparator.INSTANCE;
  private final OEncryption          encryption;

  public OHashIndexBucket(int depth, OCacheEntry cacheEntry, OBinarySerializer<K> keySerializer,
      OBinarySerializer<V> valueSerializer, OType[] keyTypes, OEncryption encryption) throws IOException {
    super(cacheEntry);

    this.keySerializer = keySerializer;
    this.valueSerializer = valueSerializer;
    this.keyTypes = keyTypes;
    this.encryption = encryption;

    init(depth);
  }

  public OHashIndexBucket(OCacheEntry cacheEntry, OBinarySerializer<K> keySerializer, OBinarySerializer<V> valueSerializer,
      OType[] keyTypes, OEncryption encryption) {
    super(cacheEntry);

    this.keySerializer = keySerializer;
    this.valueSerializer = valueSerializer;
    this.keyTypes = keyTypes;
    this.encryption = encryption;
  }

  public void init(int depth) throws IOException {
    setByteValue(DEPTH_OFFSET, (byte) depth);
    setIntValue(FREE_POINTER_OFFSET, MAX_BUCKET_SIZE_BYTES);
    setIntValue(SIZE_OFFSET, 0);
  }

  public Entry<K, V> find(final K key, final long hashCode) {
    final int index = binarySearch(key, hashCode);
    if (index < 0)
      return null;

    return getEntry(index);
  }

  private int binarySearch(K key, long hashCode) {
    int low = 0;
    int high = size() - 1;

    while (low <= high) {
      final int mid = (low + high) >>> 1;

      final long midHashCode = getHashCode(mid);
      final int cmp;
      if (lessThanUnsigned(midHashCode, hashCode))
        cmp = -1;
      else if (greaterThanUnsigned(midHashCode, hashCode))
        cmp = 1;
      else {
        final K midVal = getKey(mid);
        cmp = keyComparator.compare(midVal, key);
      }

      if (cmp < 0)
        low = mid + 1;
      else if (cmp > 0)
        high = mid - 1;
      else
        return mid; // key found
    }
    return -(low + 1); // key not found.
  }

  private static boolean lessThanUnsigned(long longOne, long longTwo) {
    return (longOne + Long.MIN_VALUE) < (longTwo + Long.MIN_VALUE);
  }

  private static boolean greaterThanUnsigned(long longOne, long longTwo) {
    return (longOne + Long.MIN_VALUE) > (longTwo + Long.MIN_VALUE);
  }

  public Entry<K, V> getEntry(int index) {
    int entryPosition = getIntValue(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE);

    final long hashCode = getLongValue(entryPosition);
    entryPosition += OLongSerializer.LONG_SIZE;

    final K key;
    if (encryption == null) {
      key = deserializeFromDirectMemory(keySerializer, entryPosition);

      entryPosition += getObjectSizeInDirectMemory(keySerializer, entryPosition);
    } else {
      final int encryptedLength = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE;

      final byte[] encryptedKey = getBinaryValue(entryPosition, encryptedLength);
      entryPosition += encryptedLength;

      final byte[] binaryKey = encryption.decrypt(encryptedKey);

      key = keySerializer.deserializeNativeObject(binaryKey, 0);
    }

    final V value = deserializeFromDirectMemory(valueSerializer, entryPosition);
    return new Entry<K, V>(key, value, hashCode);
  }

  /**
   * Obtains the value stored under the given index in this bucket.
   *
   * @param index the value index.
   *
   * @return the obtained value.
   */
  public V getValue(int index) {
    int entryPosition = getIntValue(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE);

    // skip hash code
    entryPosition += OLongSerializer.LONG_SIZE;

    if (encryption == null) {
      // skip key
      entryPosition += getObjectSizeInDirectMemory(keySerializer, entryPosition);
    } else {
      final int encryptedLength = getIntValue(entryPosition);
      entryPosition += encryptedLength + OIntegerSerializer.INT_SIZE;
    }

    return deserializeFromDirectMemory(valueSerializer, entryPosition);
  }

  public long getHashCode(int index) {
    int entryPosition = getIntValue(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE);
    return getLongValue(entryPosition);
  }

  public K getKey(int index) {
    int entryPosition = getIntValue(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE);

    if (encryption == null) {
      return deserializeFromDirectMemory(keySerializer, entryPosition + OLongSerializer.LONG_SIZE);
    } else {
      final int encryptedLength = getIntValue(entryPosition + OLongSerializer.LONG_SIZE);
      final byte[] encryptedBinaryKey = getBinaryValue(entryPosition + OLongSerializer.LONG_SIZE + OIntegerSerializer.INT_SIZE,
          encryptedLength);
      final byte[] decryptedBinaryKey = encryption.decrypt(encryptedBinaryKey);
      return keySerializer.deserializeNativeObject(decryptedBinaryKey, 0);
    }
  }

  public int getIndex(final long hashCode, final K key) {
    return binarySearch(key, hashCode);
  }

  public int size() {
    return getIntValue(SIZE_OFFSET);
  }

  public Iterator<Entry<K, V>> iterator() {
    return new EntryIterator(0);
  }

  public Iterator<Entry<K, V>> iterator(int index) {
    return new EntryIterator(index);
  }

  public int mergedSize(OHashIndexBucket buddyBucket) {
    return POSITIONS_ARRAY_OFFSET + size() * OIntegerSerializer.INT_SIZE + (MAX_BUCKET_SIZE_BYTES - getIntValue(
        FREE_POINTER_OFFSET)) + buddyBucket.size() * OIntegerSerializer.INT_SIZE + (MAX_BUCKET_SIZE_BYTES - getIntValue(
        FREE_POINTER_OFFSET));
  }

  public int getContentSize() {
    return POSITIONS_ARRAY_OFFSET + size() * OIntegerSerializer.INT_SIZE + (MAX_BUCKET_SIZE_BYTES - getIntValue(
        FREE_POINTER_OFFSET));
  }

  public int updateEntry(int index, V value) throws IOException {
    int entryPosition = getIntValue(POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE);
    entryPosition += OLongSerializer.LONG_SIZE;

    if (encryption == null) {
      entryPosition += getObjectSizeInDirectMemory(keySerializer, entryPosition);
    } else {
      final int encryptedSize = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE + encryptedSize;
    }

    final int newSize = valueSerializer.getObjectSize(value);
    final int oldSize = getObjectSizeInDirectMemory(valueSerializer, entryPosition);
    if (newSize != oldSize)
      return -1;

    byte[] newSerializedValue = new byte[newSize];
    valueSerializer.serializeNativeObject(value, newSerializedValue, 0);

    byte[] oldSerializedValue = getBinaryValue(entryPosition, oldSize);

    if (ODefaultComparator.INSTANCE.compare(oldSerializedValue, newSerializedValue) == 0)
      return 0;

    setBinaryValue(entryPosition, newSerializedValue);
    return 1;
  }

  public Entry<K, V> deleteEntry(int index) throws IOException {
    final Entry<K, V> removedEntry = getEntry(index);

    final int freePointer = getIntValue(FREE_POINTER_OFFSET);

    final int positionOffset = POSITIONS_ARRAY_OFFSET + index * OIntegerSerializer.INT_SIZE;
    final int entryPosition = getIntValue(positionOffset);

    final int keySize;
    if (encryption == null) {
      keySize = getObjectSizeInDirectMemory(keySerializer, entryPosition + OLongSerializer.LONG_SIZE);
    } else {
      final int encryptedSize = getIntValue(entryPosition + OLongSerializer.LONG_SIZE);
      keySize = encryptedSize + OIntegerSerializer.INT_SIZE;
    }

    final int ridSize = getObjectSizeInDirectMemory(valueSerializer, entryPosition + keySize + OLongSerializer.LONG_SIZE);
    final int entrySize = keySize + ridSize + OLongSerializer.LONG_SIZE;

    moveData(positionOffset + OIntegerSerializer.INT_SIZE, positionOffset,
        size() * OIntegerSerializer.INT_SIZE - (index + 1) * OIntegerSerializer.INT_SIZE);

    if (entryPosition > freePointer)
      moveData(freePointer, freePointer + entrySize, entryPosition - freePointer);

    int currentPositionOffset = POSITIONS_ARRAY_OFFSET;
    int size = size();
    for (int i = 0; i < size - 1; i++) {
      int currentEntryPosition = getIntValue(currentPositionOffset);
      if (currentEntryPosition < entryPosition)
        setIntValue(currentPositionOffset, currentEntryPosition + entrySize);
      currentPositionOffset += OIntegerSerializer.INT_SIZE;
    }

    setIntValue(FREE_POINTER_OFFSET, freePointer + entrySize);
    setIntValue(SIZE_OFFSET, size - 1);

    return removedEntry;
  }

  public boolean addEntry(long hashCode, K key, V value) throws IOException {
    int entreeSize;

    byte[] encryptedKey = null;
    if (encryption == null) {
      entreeSize =
          keySerializer.getObjectSize(key, (Object[]) keyTypes) + valueSerializer.getObjectSize(value) + OLongSerializer.LONG_SIZE;
    } else {
      final int keySize = keySerializer.getObjectSize(key, (Object[]) keyTypes);
      final byte[] serializedKey = new byte[keySize];
      keySerializer.serializeNativeObject(key, serializedKey, 0, (Object[]) keyTypes);

      encryptedKey = encryption.encrypt(serializedKey);
      entreeSize =
          encryptedKey.length + OIntegerSerializer.INT_SIZE + valueSerializer.getObjectSize(value) + OLongSerializer.LONG_SIZE;
    }

    int freePointer = getIntValue(FREE_POINTER_OFFSET);

    int size = size();
    if (freePointer - entreeSize < POSITIONS_ARRAY_OFFSET + (size + 1) * OIntegerSerializer.INT_SIZE)
      return false;

    final int index = binarySearch(key, hashCode);
    if (index >= 0)
      throw new IllegalArgumentException("Given value is present in bucket.");

    final int insertionPoint = -index - 1;
    insertEntry(hashCode, key, value, insertionPoint, entreeSize, encryptedKey);

    return true;
  }

  private void insertEntry(long hashCode, K key, V value, int insertionPoint, int entreeSize, byte[] encryptedKey)
      throws IOException {
    int freePointer = getIntValue(FREE_POINTER_OFFSET);
    int size = size();

    final int positionsOffset = insertionPoint * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET;

    moveData(positionsOffset, positionsOffset + OIntegerSerializer.INT_SIZE,
        size() * OIntegerSerializer.INT_SIZE - insertionPoint * OIntegerSerializer.INT_SIZE);

    final int entreePosition = freePointer - entreeSize;
    setIntValue(positionsOffset, entreePosition);
    serializeEntry(hashCode, key, value, entreePosition, encryptedKey);

    setIntValue(FREE_POINTER_OFFSET, entreePosition);
    setIntValue(SIZE_OFFSET, size + 1);
  }

  public void appendEntry(long hashCode, K key, V value) throws IOException {
    final int positionsOffset = size() * OIntegerSerializer.INT_SIZE + POSITIONS_ARRAY_OFFSET;
    final int entreeSize;
    byte[] encryptedKey = null;
    if (encryption == null) {
      entreeSize =
          keySerializer.getObjectSize(key, (Object[]) keyTypes) + valueSerializer.getObjectSize(value) + OLongSerializer.LONG_SIZE;
    } else {
      final int keySize = keySerializer.getObjectSize(key, (Object[]) keyTypes);
      final byte[] serializedKey = new byte[keySize];
      keySerializer.serializeNativeObject(key, serializedKey, 0, (Object[]) keyTypes);

      encryptedKey = encryption.encrypt(serializedKey);
      entreeSize =
          OIntegerSerializer.INT_SIZE + encryptedKey.length + valueSerializer.getObjectSize(value) + OLongSerializer.LONG_SIZE;
    }

    final int freePointer = getIntValue(FREE_POINTER_OFFSET);
    final int entreePosition = freePointer - entreeSize;

    setIntValue(positionsOffset, entreePosition);
    serializeEntry(hashCode, key, value, entreePosition, encryptedKey);

    setIntValue(FREE_POINTER_OFFSET, freePointer - entreeSize);
    setIntValue(SIZE_OFFSET, size() + 1);
  }

  private void serializeEntry(long hashCode, K key, V value, int entryOffset, byte[] encryptedKey) throws IOException {
    setLongValue(entryOffset, hashCode);
    entryOffset += OLongSerializer.LONG_SIZE;

    if (encryptedKey == null) {
      final int keySize = keySerializer.getObjectSize(key, (Object[]) keyTypes);
      byte[] binaryKey = new byte[keySize];
      keySerializer.serializeNativeObject(key, binaryKey, 0, (Object[]) keyTypes);
      setBinaryValue(entryOffset, binaryKey);

      entryOffset += keySize;
    } else {
      setIntValue(entryOffset, encryptedKey.length);
      entryOffset += OIntegerSerializer.INT_SIZE;

      setBinaryValue(entryOffset, encryptedKey);
      entryOffset += encryptedKey.length;
    }

    final int valueSize = valueSerializer.getObjectSize(value);
    final byte[] binaryValue = new byte[valueSize];
    valueSerializer.serializeNativeObject(value, binaryValue, 0);

    setBinaryValue(entryOffset, binaryValue);
  }

  public int getDepth() {
    return getByteValue(DEPTH_OFFSET);
  }

  public void setDepth(int depth) {
    setByteValue(DEPTH_OFFSET, (byte) depth);
  }

  public long getNextRemovedBucketPair() {
    return getLongValue(NEXT_REMOVED_BUCKET_OFFSET);
  }

  public void setNextRemovedBucketPair(long nextRemovedBucketPair) throws IOException {
    setLongValue(NEXT_REMOVED_BUCKET_OFFSET, nextRemovedBucketPair);
  }

  public long getSplitHistory(int level) {
    return getLongValue(HISTORY_OFFSET + OLongSerializer.LONG_SIZE * level);
  }

  public void setSplitHistory(int level, long position) throws IOException {
    setLongValue(HISTORY_OFFSET + OLongSerializer.LONG_SIZE * level, position);
  }

  public static class Entry<K, V> {
    public final K    key;
    public final V    value;
    public final long hashCode;

    public Entry(K key, V value, long hashCode) {
      this.key = key;
      this.value = value;
      this.hashCode = hashCode;
    }
  }

  private final class EntryIterator implements Iterator<Entry<K, V>> {
    private int currentIndex;

    private EntryIterator(int currentIndex) {
      this.currentIndex = currentIndex;
    }

    @Override
    public boolean hasNext() {
      return currentIndex < size();
    }

    @Override
    public Entry<K, V> next() {
      if (currentIndex >= size())
        throw new NoSuchElementException("Iterator was reached last element");

      final Entry<K, V> entry = getEntry(currentIndex);
      currentIndex++;
      return entry;
    }

    @Override
    public void remove() {
      throw new UnsupportedOperationException("Remove operation is not supported");
    }
  }
}


/*
  *
  *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
  *  *
  *  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  *  you may not use this file except in compliance with the License.
  *  *  You may obtain a copy of the License at
  *  *
  *  *       http://www.apache.org/licenses/LICENSE-2.0
  *  *
  *  *  Unless required by applicable law or agreed to in writing, software
  *  *  distributed under the License is distributed on an "AS IS" BASIS,
  *  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  *  *  See the License for the specific language governing permissions and
  *  *  limitations under the License.
  *  *
  *  * For more information: http://orientdb.com
  *
  */

package com.orientechnologies.orient.core.sql;

import com.orientechnologies.common.util.OPair;
import com.orientechnologies.orient.core.index.OIndex;
import com.orientechnologies.orient.core.index.OIndexDefinition;

import java.util.List;

/**
 * @author Artem Orobets (enisher-at-gmail.com)
 */
public class OOrderByOptimizer {
  boolean canBeUsedByOrderBy(OIndex<?> index, List<OPair<String, String>> orderedFields) {
    if (orderedFields.isEmpty())
      return false;

    if (!index.supportsOrderedIterations())
      return false;

    final OIndexDefinition definition = index.getDefinition();
    final List<String> fields = definition.getFields();
    final int endIndex = Math.min(fields.size(), orderedFields.size());

    final String firstOrder = orderedFields.get(0).getValue();
    for (int i = 0; i < endIndex; i++) {
      final OPair<String, String> pair = orderedFields.get(i);

      if (!firstOrder.equals(pair.getValue()))
        return false;

      final String orderFieldName = orderedFields.get(i).getKey();
      final String indexFieldName = fields.get(i);

      if (!orderFieldName.equals(indexFieldName))
        return false;
    }

    return true;
  }

  /**
   * checks if, given a list of "=" conditions and a set of ORDER BY fields
   *
   * @param index
   * @param equalsFilterFields
   * @param orderedFields
   * @return
   */
  boolean canBeUsedByOrderByAfterFilter(OIndex<?> index, List<String> equalsFilterFields,
      List<OPair<String, String>> orderedFields) {
    if (orderedFields.isEmpty())
      return false;

    if (!index.supportsOrderedIterations())
      return false;

    final OIndexDefinition definition = index.getDefinition();
    final List<String> indexFields = definition.getFields();
    int endIndex = Math.min(indexFields.size(), equalsFilterFields.size());

    final String firstOrder = orderedFields.get(0).getValue();

    //check that all the "equals" clauses are a prefix for the index
    for (int i = 0; i < endIndex; i++) {
      final String equalsFieldName = equalsFilterFields.get(i);
      final String indexFieldName = indexFields.get(i);
      if (!equalsFieldName.equals(indexFieldName))
        return false;
    }

    endIndex = Math.min(indexFields.size(), orderedFields.size() + equalsFilterFields.size());
    if (endIndex == equalsFilterFields.size()) {
      //the index is used only for filtering
      return false;
    }
    //check that after that prefix there all the Order By fields in the right order
    for (int i = equalsFilterFields.size(); i < endIndex; i++) {
      int fieldOrderInOrderByClause = i - equalsFilterFields.size();
      final OPair<String, String> pair = orderedFields.get(fieldOrderInOrderByClause);

      if (!firstOrder.equals(pair.getValue()))
        return false;

      final String orderFieldName = pair.getKey();
      final String indexFieldName = indexFields.get(i);

      if (!orderFieldName.equals(indexFieldName))
        return false;
    }

    return true;
  }

}


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

import java.util.ArrayList;

/**
 * String parser utility class
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 *
 */
public class OStringParser {

  public static final String WHITE_SPACE = " ";
  public static final String COMMON_JUMP = " \r\n";

  public static String[] getWords(String iRecord, final String iSeparatorChars) {
    return getWords(iRecord, iSeparatorChars, false);
  }

  public static String[] getWords(String iRecord, final String iSeparatorChars, final boolean iIncludeStringSep) {
    return getWords(iRecord, iSeparatorChars, " \n\r\t", iIncludeStringSep);
  }

  public static String[] getWords(String iText, final String iSeparatorChars, final String iJumpChars,
      final boolean iIncludeStringSep) {
    iText = iText.trim();

    final ArrayList<String> fields = new ArrayList<String>();
    final StringBuilder buffer = new StringBuilder(64);
    char stringBeginChar = ' ';
    char c;
    int openBraket = 0;
    int openGraph = 0;
    boolean charFound;
    boolean escape = false;

    for (int i = 0; i < iText.length(); ++i) {
      c = iText.charAt(i);

      if (!escape && c == '\\' && ((i + 1) < iText.length())) {
        // ESCAPE CHARS
        final char nextChar = iText.charAt(i + 1);

        if (nextChar == 'u') {
          i = readUnicode(iText, i + 2, buffer);
        } else if (nextChar == 'n') {
          buffer.append(stringBeginChar == ' ' ? "\n" : "\\\n");
          i++;
        } else if (nextChar == 'r') {
          buffer.append(stringBeginChar == ' ' ? "\r" : "\\\r");
          i++;
        } else if (nextChar == 't') {
          buffer.append(stringBeginChar == ' ' ? "\t" : "\\\t");
          i++;
        } else if (nextChar == 'f') {
          buffer.append(stringBeginChar == ' ' ? "\f" : "\\\f");
          i++;
        } else if (stringBeginChar != ' ' && nextChar == '\'' || nextChar == '"') {
          buffer.append('\\');
          buffer.append(nextChar);
          i++;
        } else {
          buffer.append('\\');
          escape = true;
        }

        continue;
      }

      if (!escape && (c == '\'' || c == '"')) {
        if (stringBeginChar != ' ') {
          // CLOSE THE STRING?
          if (stringBeginChar == c) {
            // SAME CHAR AS THE BEGIN OF THE STRING: CLOSE IT AND PUSH
            stringBeginChar = ' ';

            if (iIncludeStringSep)
              buffer.append(c);
            continue;
          }
        } else {
          // START STRING
          stringBeginChar = c;
          if (iIncludeStringSep)
            buffer.append(c);

          continue;
        }
      } else if (stringBeginChar == ' ') {
        if (c == '[')
          openBraket++;
        else if (c == ']')
          openBraket--;
        else if (c == '{')
          openGraph++;
        else if (c == '}')
          openGraph--;
        else if (openBraket == 0 && openGraph == 0) {
          charFound = false;
          for (int sepIndex = 0; sepIndex < iSeparatorChars.length(); ++sepIndex) {
            if (iSeparatorChars.charAt(sepIndex) == c) {
              charFound = true;
              if (buffer.length() > 0) {
                // SEPARATOR (OUTSIDE A STRING): PUSH
                fields.add(buffer.toString());
                buffer.setLength(0);
              }
              break;
            }
          }

          if (charFound)
            continue;
        }

        if (stringBeginChar == ' ') {
          // CHECK FOR CHAR TO JUMP
          charFound = false;

          for (int jumpIndex = 0; jumpIndex < iJumpChars.length(); ++jumpIndex) {
            if (iJumpChars.charAt(jumpIndex) == c) {
              charFound = true;
              break;
            }
          }

          if (charFound)
            continue;
        }
      }

      buffer.append(c);

      if (escape)
        escape = false;
    }

    if (buffer.length() > 0) {
      // ADD THE LAST WORD IF ANY
      fields.add(buffer.toString());
    }

    String[] result = new String[fields.size()];
    fields.toArray(result);
    return result;
  }

  public static String[] split(String iText, final char iSplitChar, String iJumpChars) {
    iText = iText.trim();

    ArrayList<String> fields = new ArrayList<String>();
    StringBuilder buffer = new StringBuilder(256);
    char c;
    char stringChar = ' ';
    boolean escape = false;
    boolean jumpSplitChar = false;
    boolean charFound;

    for (int i = 0; i < iText.length(); i++) {
      c = iText.charAt(i);

      if (!escape && c == '\\' && ((i + 1) < iText.length())) {
        if (iText.charAt(i + 1) == 'u') {
          i = readUnicode(iText, i + 2, buffer);
        } else {
          escape = true;
          buffer.append(c);
        }
        continue;
      }

      if (c == '\'' || c == '"') {
        if (!jumpSplitChar) {
          jumpSplitChar = true;
          stringChar = c;
        } else {
          if (!escape && c == stringChar)
            jumpSplitChar = false;
        }
      }

      if (c == iSplitChar) {
        if (!jumpSplitChar) {
          fields.add(buffer.toString());
          buffer.setLength(0);
          continue;
        }
      }

      // CHECK IF IT MUST JUMP THE CHAR
      if (buffer.length() == 0) {
        charFound = false;

        for (int jumpIndex = 0; jumpIndex < iJumpChars.length(); ++jumpIndex) {
          if (iJumpChars.charAt(jumpIndex) == c) {
            charFound = true;
            break;
          }
        }

        if (charFound)
          continue;
      }

      buffer.append(c);

      if (escape)
        escape = false;
    }

    if (buffer.length() > 0) {
      fields.add(buffer.toString());
      buffer.setLength(0);
    }
    String[] result = new String[fields.size()];
    fields.toArray(result);
    return result;
  }

  /**
   * Finds a character inside a string specyfing the limits and direction. If iFrom is minor than iTo, then it moves forward,
   * otherwise backward.
   */
  public static int indexOfOutsideStrings(final String iText, final char iToFind, int iFrom, int iTo) {
    if (iTo == -1)
      iTo = iText.length() - 1;
    if (iFrom == -1)
      iFrom = iText.length() - 1;

    char c;
    char stringChar = ' ';
    boolean escape = false;

    final StringBuilder buffer = new StringBuilder(1024);

    int i = iFrom;
    while (true) {
      c = iText.charAt(i);

      if (!escape && c == '\\' && ((i + 1) < iText.length())) {
        if (iText.charAt(i + 1) == 'u') {
          i = readUnicode(iText, i + 2, buffer);
        } else
          escape = true;
      } else {
        if (c == '\'' || c == '"') {
          // BEGIN/END STRING
          if (stringChar == ' ') {
            // BEGIN
            stringChar = c;
          } else {
            // END
            if (!escape && c == stringChar)
              stringChar = ' ';
          }
        }

        if (c == iToFind && stringChar == ' ')
          return i;

        if (escape)
          escape = false;
      }

      if (iFrom < iTo) {
        // MOVE FORWARD
        if (++i > iTo)
          break;
      } else {
        // MOVE BACKWARD
        if (--i < iFrom)
          break;
      }
    }
    return -1;
  }

  /**
   * Jump white spaces.
   *
   * @param iText
   *          String to analyze
   * @param iCurrentPosition
   *          Current position in text
   * @param iMaxPosition
   *          TODO
   * @return The new offset inside the string analyzed
   */
  public static int jumpWhiteSpaces(final CharSequence iText, final int iCurrentPosition, final int iMaxPosition) {
    return jump(iText, iCurrentPosition, iMaxPosition, COMMON_JUMP);
  }

  /**
   * Jump some characters reading from an offset of a String.
   *
   * @param iText
   *          String to analyze
   * @param iCurrentPosition
   *          Current position in text
   * @param iMaxPosition
   *          Maximum position to read
   * @param iJumpChars
   *          String as char array of chars to jump
   * @return The new offset inside the string analyzed
   */
  public static int jump(final CharSequence iText, int iCurrentPosition, final int iMaxPosition, final String iJumpChars) {
    if (iCurrentPosition < 0)
      return -1;

    final int size = iMaxPosition > -1 ? Math.min(iMaxPosition, iText.length()) : iText.length();
    final int jumpCharSize = iJumpChars.length();
    boolean found = true;
    char c;
    for (; iCurrentPosition < size; ++iCurrentPosition) {
      found = false;
      c = iText.charAt(iCurrentPosition);
      for (int jumpIndex = 0; jumpIndex < jumpCharSize; ++jumpIndex) {
        if (iJumpChars.charAt(jumpIndex) == c) {
          found = true;
          break;
        }
      }

      if (!found)
        break;
    }

    return iCurrentPosition >= size ? -1 : iCurrentPosition;
  }

  public static int readUnicode(String iText, int position, final StringBuilder buffer) {
    // DECODE UNICODE CHAR
    final StringBuilder buff = new StringBuilder(64);
    final int lastPos = position + 4;
    for (; position < lastPos; ++position)
      buff.append(iText.charAt(position));

    buffer.append((char) Integer.parseInt(buff.toString(), 16));
    return position - 1;
  }

  public static int readUnicode(char[] iText, int position, final StringBuilder buffer) {
    // DECODE UNICODE CHAR
    final StringBuilder buff = new StringBuilder(64);
    final int lastPos = position + 4;
    for (; position < lastPos; ++position)
      buff.append(iText[position]);

    buffer.append((char) Integer.parseInt(buff.toString(), 16));
    return position - 1;
  }

  public static String replaceAll(final String iText, final String iToReplace, final String iReplacement) {
    if (iText == null || iText.length() <= 0 || iToReplace == null || iToReplace.length() <= 0)
      return iText;
    int pos = iText.indexOf(iToReplace);
    int lastAppend = 0;
    final StringBuffer buffer = new StringBuffer(1024);
    while (pos > -1) {
      buffer.append(iText.substring(lastAppend, pos));
      buffer.append(iReplacement);
      lastAppend = pos + iToReplace.length();
      pos = iText.indexOf(iToReplace, lastAppend);
    }
    buffer.append(iText.substring(lastAppend));
    return buffer.toString();
  }

  /**
   * Like String.startsWith() but ignoring case
   */
  public static boolean startsWithIgnoreCase(final String iText, final String iToFind) {
    if (iText.length() < iToFind.length())
      return false;

    return iText.substring(0, iToFind.length()).equalsIgnoreCase(iToFind);
  }
}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  Licensed under the Apache License, Version 2.0 (the "License");
 *  *  you may not use this file except in compliance with the License.
 *  *  You may obtain a copy of the License at
 *  *
 *  *       http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *  Unless required by applicable law or agreed to in writing, software
 *  *  distributed under the License is distributed on an "AS IS" BASIS,
 *  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *  See the License for the specific language governing permissions and
 *  *  limitations under the License.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.query;

import com.orientechnologies.orient.core.command.OCommandRequestAbstract;
import com.orientechnologies.orient.core.fetch.OFetchHelper;

@SuppressWarnings("serial")
public abstract class OQueryAbstract<T extends Object> extends OCommandRequestAbstract implements OQuery<T> {
  public OQueryAbstract() {
    useCache = true;
  }

  @SuppressWarnings("unchecked")
  public <RET> RET execute(final Object... iArgs) {
    return (RET) run(iArgs);
  }

  /**
   * Returns the current fetch plan.
   */
  public String getFetchPlan() {
    return fetchPlan;
  }

  /**
   * Sets the fetch plan to use.
   */
  public OQueryAbstract setFetchPlan(final String fetchPlan) {
    OFetchHelper.checkFetchPlanValid(fetchPlan);
    if (fetchPlan != null && fetchPlan.length() == 0)
      this.fetchPlan = null;
    else
      this.fetchPlan = fetchPlan;
    return this;
  }

  /**
   * Resets the query removing the result set. Call this to reuse the Query object preventing a pagination.
   */
  @Override
  public void reset() {
  }

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

}


/*
 *
 *  *  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.log.OLogManager;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.exception.OConfigurationException;
import com.orientechnologies.orient.core.exception.ORecordNotFoundException;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordVersionHelper;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.storage.ORawBuffer;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.storage.OStorageOperationResult;
import com.orientechnologies.orient.server.distributed.*;
import com.orientechnologies.orient.server.distributed.conflict.OAbstractDistributedConflictResolver;
import com.orientechnologies.orient.server.distributed.conflict.ODistributedConflictResolver;
import com.orientechnologies.orient.distributed.impl.task.*;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

import static com.orientechnologies.orient.server.distributed.conflict.ODistributedConflictResolver.NOT_FOUND;

/**
 * Distributed database repairer that, based on the reported records to check, executes repair of record in configurable batches.
 * This assure better performance by grouping multiple requests. The repair is based on the chain of conflict resolver.
 *
 * @author Luca Garulli (l.garulli--at--orientdb.com)
 */
public class OConflictResolverDatabaseRepairer implements ODistributedDatabaseRepairer {
  private final ODistributedServerManager dManager;
  private final String                    databaseName;

  private final AtomicLong recordProcessed     = new AtomicLong(0);
  private final AtomicLong recordCanceled      = new AtomicLong(0);
  private final AtomicLong totalTimeProcessing = new AtomicLong(0);
  private final boolean active;

  private ConcurrentMap<ORecordId, Boolean> records  = new ConcurrentHashMap<ORecordId, Boolean>();
  private ConcurrentMap<Integer, Boolean>   clusters = new ConcurrentHashMap<Integer, Boolean>();

  private final TimerTask checkTask;

  private List<ODistributedConflictResolver> conflictResolvers = new ArrayList<ODistributedConflictResolver>();

  public OConflictResolverDatabaseRepairer(final ODistributedServerManager manager, final String databaseName) {
    this.dManager = manager;
    this.databaseName = databaseName;

    // REGISTER THE CHAIN OF CONFLICT RESOLVERS
    final String chain = OGlobalConfiguration.DISTRIBUTED_CONFLICT_RESOLVER_REPAIRER_CHAIN.getValueAsString();
    final String[] items = chain.split(",");
    for (String item : items) {
      final String name;
      final ODocument config;

      if (item.endsWith("}")) {
        // EXTRACT CFG
        final int pos = item.indexOf('{');
        if (pos < 0)
          throw new OConfigurationException("Invalid configuration for conflict resolver: " + item);

        name = item.substring(0, pos);
        config = new ODocument().fromJSON(item.substring(pos, item.length()));
      } else {
        name = item;
        config = null;
      }

      final ODistributedConflictResolver cr = manager.getConflictResolverFactory().getImplementation(name);
      if (cr == null)
        throw new OConfigurationException(
            "Cannot find '" + name + "' conflict resolver implementation. Available are: " + manager.getConflictResolverFactory()
                .getRegisteredImplementationNames());

      if (config != null)
        cr.configure(config);

      conflictResolvers.add(cr);
    }

    checkTask = new TimerTask() {
      @Override
      public void run() {

        final long start = System.currentTimeMillis();
        try {

          check();

        } catch (Exception t) {
          OLogManager.instance().error(this, "Error on repairing distributed database", t);
          // IGNORE THE EXCEPTION
        } finally {
          totalTimeProcessing.addAndGet(System.currentTimeMillis() - start);
        }
      }

    };

    final long time = OGlobalConfiguration.DISTRIBUTED_CONFLICT_RESOLVER_REPAIRER_CHECK_EVERY.getValueAsLong();
    if (time > 0) {
      Orient.instance().scheduleTask(checkTask, time, time);
      active = true;
    } else
      active = false;
  }

  @Override
  public void enqueueRepairRecords(final Collection<ORecordId> rids) {
    for (ORecordId rid : rids)
      enqueueRepairRecord(rid);
  }

  /**
   * Adds the record to repair int the map of records and cluster. The decision about repairing is taken by the timer task.
   *
   * @param rid RecordId to repair
   */
  @Override
  public void enqueueRepairRecord(final ORecordId rid) {
    if (!active)
      return;

    if (rid == null || !rid.isPersistent())
      return;

    if (rid.getClusterPosition() < -1)
      // SKIP TRANSACTIONAL RIDS
      return;

    recordProcessed.incrementAndGet();

    // ADD RECORD TO REPAIR
    records.put(rid, Boolean.TRUE);
  }

  /**
   * Cancel the repair against a record because the update succeed.
   *
   * @param rid RecordId to remove from repair
   */
  @Override
  public void cancelRepairRecord(final ORecordId rid) {
    if (!active)
      return;

    if (rid.getClusterPosition() < -1)
      // SKIP TRANSACTIONAL RIDS
      return;

    // REMOVE THE RECORD TO REPAIR
    if (records.remove(rid) != null)
      // REMOVED
      recordCanceled.incrementAndGet();
  }

  /**
   * Enqueues the request to repair a cluster. The decision about repairing is taken by the timer task.
   *
   * @param clusterId Broken cluster id to start repairing
   */
  @Override
  public void enqueueRepairCluster(final int clusterId) {
    if (!active)
      return;

    if (clusterId < -1)
      // SKIP TRANSACTIONAL RIDS
      return;

    recordProcessed.incrementAndGet();

    // ADD CLUSTER TO REPAIR
    clusters.put(clusterId, Boolean.TRUE);
  }

  private void check() throws Exception {
    // OPEN THE DATABASE ONLY IF NEEDED
    ODatabaseDocumentInternal db = null;
    try {
      final int batchMax = OGlobalConfiguration.DISTRIBUTED_CONFLICT_RESOLVER_REPAIRER_BATCH.getValueAsInteger();
      final List<ORecordId> rids = new ArrayList<ORecordId>(batchMax);

      // REPAIR CLUSTER FIRST
      for (Integer clusterId : clusters.keySet()) {
        //repairCluster(db, clusterId);
      }
      clusters.clear();

      // REPAIR RECORDS
      for (ORecordId rid : records.keySet()) {
        rids.add(rid);
        if (rids.size() >= batchMax)
          // REACHED MAXIMUM FOR BATCH
          break;
      }

      if (!rids.isEmpty()) {
        // REPAIR RECORDS IN BATCH
        db = getDatabase();
        if (repairRecords(db, rids)) {
          // SUCCEED: REMOVE REPAIRED RECORDS
          for (ORecordId rid : rids)
            records.remove(rid);
        }
      }

    } finally {
      if (db != null)
        db.close();
    }
  }

  private void repairCluster(ODatabaseDocumentInternal db, final Integer clusterId) throws Exception {
    if (clusterId < 0)
      return;

    final ODistributedConfiguration dCfg = dManager.getDatabaseConfiguration(databaseName);

    final ODistributedRequestId requestId = new ODistributedRequestId(dManager.getLocalNodeId(),
        dManager.getNextMessageIdCounter());

    final ODistributedDatabase localDistributedDatabase = dManager.getMessageService().getDatabase(databaseName);

    if (db == null)
      db = getDatabase();

    final String clusterName = db.getClusterNameById(clusterId);

    // ASSURE LOCAL NODE IS THE CLUSTER OWNER
    final String serverOwner = dCfg.getClusterOwner(clusterName);
    if (serverOwner == null || !serverOwner.equals(dManager.getLocalNodeName())) {
      ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
          "Cannot auto repair cluster '%s' (%d) because current server (%s) is not the owner (owner=%s reqId=%s)", clusterName,
          clusterId, dManager.getLocalNodeName(), serverOwner, requestId);
      return;
    }

    final ODistributedTxContext ctx = localDistributedDatabase.registerTxContext(requestId);
    try {

      // ACQUIRE LOCK ON THE CLUSTER (LOCKING -1 AS CLUSTER POSITION)
      final List<ORecordId> rids = new ArrayList<ORecordId>(1);
      rids.add(new ORecordId(clusterId, -1));

      // ACQUIRE LOCKS ON LOCAL SERVER FIRST
      ODistributedTransactionManager.acquireMultipleRecordLocks(this, dManager, rids, null, ctx, -1);

      final List<String> clusterNames = new ArrayList<String>();
      clusterNames.add(clusterName);

      final Collection<String> involvedServers = dCfg.getServers(clusterNames);
      final Set<String> nonLocalServers = new HashSet<String>(involvedServers);
      nonLocalServers.remove(dManager.getLocalNodeName());

      if (nonLocalServers.isEmpty())
        return;

      ODistributedServerLog
          .debug(this, dManager.getLocalNodeName(), involvedServers.toString(), ODistributedServerLog.DIRECTION.OUT,
              "Auto repairing cluster '%s' (%d) on servers %s (reqId=%s)...", clusterName, clusterId, involvedServers, requestId);

      // CREATE TX TASK
      final OClusterRepairInfoTask task = new OClusterRepairInfoTask(clusterId);

      final ODistributedResponse response = dManager
          .sendRequest(databaseName, clusterNames, nonLocalServers, task, requestId.getMessageId(),
              ODistributedRequest.EXECUTION_MODE.RESPONSE, null, null, null);

      int repaired = 0;

      try {
        if (response != null) {
          final Object payload = response.getPayload();
          if (payload instanceof Map)
            repaired = repairClusterAtBlocks(db, clusterNames, clusterId, (Map<String, Object>) payload);
        }
      } finally {
        if (repaired == 0)
          ODistributedServerLog
              .debug(this, dManager.getLocalNodeName(), involvedServers.toString(), ODistributedServerLog.DIRECTION.OUT,
                  "Auto repairing of cluster '%s' completed. No fix is needed (reqId=%s)", clusterName, repaired, requestId);
        else
          ODistributedServerLog
              .info(this, dManager.getLocalNodeName(), involvedServers.toString(), ODistributedServerLog.DIRECTION.OUT,
                  "Auto repairing of cluster '%s' completed. Repaired %d records (reqId=%s)", clusterName, repaired, requestId);
      }

    } catch (Exception e) {
      ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
          "Error executing auto repairing on cluster '%s' (error=%s, reqId=%s)", clusterName, e.toString(), requestId);
      return;
    } finally {
      // RELEASE LOCKS AND REMOVE TX CONTEXT
      localDistributedDatabase.popTxContext(requestId);
      ctx.destroy();
    }

    return;
  }

  private int repairClusterAtBlocks(final ODatabaseDocumentInternal db, final List<String> clusterNames, final int clusterId,
      final Map<String, Object> repairInfoResult) throws IOException {
    final OStorage storage = db.getStorage().getUnderlying();
    final long localEnd = storage.getClusterById(clusterId).getNextPosition() - 1;

    final int batchMax = OGlobalConfiguration.DISTRIBUTED_CONFLICT_RESOLVER_REPAIRER_BATCH.getValueAsInteger();

    int recordRepaired = 0;

    for (Map.Entry<String, Object> entry : repairInfoResult.entrySet()) {
      final String server = entry.getKey();

      final ODistributedServerManager.DB_STATUS status = dManager.getDatabaseStatus(server, databaseName);

      if (status != ODistributedServerManager.DB_STATUS.ONLINE) {
        ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
            "Cannot align missing records of cluster '%s' on server %s, because is not ONLINE (status=%s)", clusterNames.get(0),
            server, status);
        return 0;
      }

      final Object result = entry.getValue();

      final List<String> servers = new ArrayList<String>(1);
      servers.add(server);

      if (result instanceof Long) {
        final long remoteEnd = (Long) result;

        ORepairClusterTask task = new ORepairClusterTask().init(clusterId);

        for (long pos = remoteEnd + 1; pos <= localEnd; ++pos) {
          final ORecordId rid = new ORecordId(clusterId, pos);
          final ORawBuffer rawRecord = storage.readRecord(rid, null, true, false, null).getResult();
          if (rawRecord == null)
            continue;

          task.add(((OCreateRecordTask) dManager.getTaskFactoryManager().getFactoryByServerNames(servers)
              .createTask(OCreateRecordTask.FACTORYID)).init(rid, rawRecord.buffer, rawRecord.version, rawRecord.recordType));

          recordRepaired++;

          if (task.getTasks().size() > batchMax) {
            // SEND BATCH OF CHANGES
            final ODistributedResponse response = dManager
                .sendRequest(databaseName, clusterNames, servers, task, dManager.getNextMessageIdCounter(),
                    ODistributedRequest.EXECUTION_MODE.RESPONSE, null, null, null);

            task = new ORepairClusterTask().init(clusterId);
          }
        }

        if (!task.getTasks().isEmpty()) {
          // SEND FINAL BATCH OF CHANGES
          final ODistributedResponse response = dManager
              .sendRequest(databaseName, clusterNames, servers, task, dManager.getNextMessageIdCounter(),
                  ODistributedRequest.EXECUTION_MODE.RESPONSE, null, null, null);
        }

        if (task.getTasks().size() == 0)
          ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
              "Auto repair aligned %d records of cluster '%s'", task.getTasks().size(), clusterNames.get(0));
        else
          ODistributedServerLog.info(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
              "Auto repair aligned %d records of cluster '%s'", task.getTasks().size(), clusterNames.get(0));
      }
    }

    return recordRepaired;
  }

  @Override
  public void repairRecords(final Collection<ORecordId> rids) {
    repairRecords(getDatabase(), rids);
  }

  @Override
  public void repairRecord(final ORecordId rid) {
    final List<ORecordId> rids = new ArrayList<ORecordId>();
    rids.add(rid);
    repairRecords(getDatabase(), rids);
  }

  private boolean repairRecords(final ODatabaseDocumentInternal db, final Collection<ORecordId> ridSet) {
    final List<ORecordId> rids = new ArrayList<ORecordId>(ridSet);

    final ODistributedConfiguration dCfg = dManager.getDatabaseConfiguration(databaseName);

    final ODistributedRequestId requestId = new ODistributedRequestId(dManager.getLocalNodeId(),
        dManager.getNextMessageIdCounter());

    final ODistributedDatabase localDistributedDatabase = dManager.getMessageService().getDatabase(databaseName);

    final ODistributedTxContext ctx = localDistributedDatabase.registerTxContext(requestId);
    try {

      // ACQUIRE LOCKS WITH A LARGER TIMEOUT
      ODistributedTransactionManager.acquireMultipleRecordLocks(this, dManager, rids, null, ctx, -1);

      final Set<String> clusterNames = new HashSet();
      for (ORecordId rid : rids)
        clusterNames.add(db.getClusterNameById(rid.getClusterId()));

      final Collection<String> involvedServers = dCfg.getServers(clusterNames);
      final Set<String> nonLocalServers = new HashSet<String>(involvedServers);
      nonLocalServers.remove(dManager.getLocalNodeName());

      if (nonLocalServers.isEmpty())
        // REMOTE SERVER NOT INVOLVED, ALL ALIGNED
        return true;

      // CREATE LOCAL RESULT
      final OTxTaskResult localResult = new OTxTaskResult();
      for (ORecordId rid : rids) {
        final OStorageOperationResult<ORawBuffer> res;
        if (rid.getClusterPosition() > -1)
          res = db.getStorage().readRecord(rid, null, true, false, null);
        else
          res = null;

        if (res != null)
          localResult.results.add(res.getResult());
        else
          localResult.results.add(null);
      }

      ODistributedServerLog
          .debug(this, dManager.getLocalNodeName(), involvedServers.toString(), ODistributedServerLog.DIRECTION.OUT,
              "Auto repairing records %s on servers %s (reqId=%s)...", rids, involvedServers, requestId);

      // CREATE TX TASK
      final ORepairRecordsTask tx = ((ORepairRecordsTask) dManager.getTaskFactoryManager().getFactoryByServerNames(nonLocalServers)
          .createTask(ORepairRecordsTask.FACTORYID));

      for (ORecordId rid : rids)
        tx.add(new OReadRecordTask().init(rid));

      ODistributedResponse response = dManager
          .sendRequest(databaseName, clusterNames, nonLocalServers, tx, requestId.getMessageId(),
              ODistributedRequest.EXECUTION_MODE.RESPONSE, localResult, null, null);

      // MAP OF OCompletedTxTask SERVER/RECORDS. RECORD == NULL MEANS DELETE
      final Map<String, OCompleted2pcTask> repairMap = new HashMap<String, OCompleted2pcTask>(rids.size());
      for (String server : involvedServers) {
        final OCompleted2pcTask completedTask = ((OCompleted2pcTask) dManager.getTaskFactoryManager()
            .getFactoryByServerNames(involvedServers).createTask(OCompleted2pcTask.FACTORYID));
        completedTask.init(requestId, false, tx.getPartitionKey());
        repairMap.put(server, completedTask);
      }

      boolean skipTheRest = false;

      try {
        if (response != null) {
          final Object payload = response.getPayload();
          if (payload instanceof Map) {

            final Map<String, Object> map = (Map<String, Object>) payload;

            // BROWSE FROM LOCAL RESULT
            for (int i = 0; i < localResult.results.size(); ++i) {
              final Map<Object, List<String>> groupedResult = new HashMap<Object, List<String>>();

              final ORecordId rid = rids.get(i);

              for (Map.Entry<String, Object> entry : map.entrySet()) {
                final String serverName = entry.getKey();
                final Object serverResult = entry.getValue();

                if (serverResult instanceof Throwable) {
                  // ABORT IT
                  if (serverResult instanceof ONeedRetryException)
                    ODistributedServerLog.debug(this, dManager.getLocalNodeName(), serverName, ODistributedServerLog.DIRECTION.IN,
                        "Cannot auto repair record %s on servers %s because some of them are locked (error=%s), trying it again later",
                        rid, serverName, serverResult);
                  else
                    ODistributedServerLog.info(this, dManager.getLocalNodeName(), serverName, ODistributedServerLog.DIRECTION.IN,
                        "Cannot auto repair record %s on servers %s (error=%s), trying it again later", rid, serverName,
                        serverResult);
                  return false;
                }

                final OTxTaskResult serverTxResult = (OTxTaskResult) serverResult;
                final Object serverRecordContent = serverTxResult.results.get(i);

                List<String> group = groupedResult.get(serverRecordContent);
                if (group == null) {
                  if (serverRecordContent instanceof ORawBuffer) {
                    if (((ORawBuffer) serverRecordContent).recordType == ODocument.RECORD_TYPE) {

                      for (Map.Entry<Object, List<String>> resultEntry : groupedResult.entrySet()) {
                        if (resultEntry.getKey() instanceof ORawBuffer && OAbstractDistributedConflictResolver
                            .compareRecords((ORawBuffer) serverRecordContent, (ORawBuffer) resultEntry.getKey())) {
                          group = resultEntry.getValue();
                          break;
                        }
                      }
                    }
                  }

                  if (group == null) {
                    group = new ArrayList<String>();
                    groupedResult.put(serverRecordContent, group);
                  }
                }
                group.add(serverName);
              }

              if (groupedResult.size() == 1)
                // NO CONFLICT, SKIP IT
                continue;

              ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
                  "Auto repair found %d groups of contents, analyzing the winner...", groupedResult.size());

              // EXECUTE THE CONFLICT RESOLVE PIPELINE: CONTINUE UNTIL THE WINNER IS NOT NULL (=RESOLVED)
              Object winner = null;
              Map<Object, List<String>> candidates = groupedResult;
              for (ODistributedConflictResolver conflictResolver : conflictResolvers) {
                final ODistributedConflictResolver.OConflictResult conflictResult = conflictResolver
                    .onConflict(databaseName, db.getClusterNameById(rid.getClusterId()), rid, dManager, candidates);

                winner = conflictResult.winner;
                if (winner != NOT_FOUND)
                  // FOUND WINNER
                  break;

                candidates = conflictResult.candidates;
              }

              if (winner == NOT_FOUND) {
                // NO WINNER, SKIP IT

                final StringBuilder buffer = new StringBuilder();
                int resultIndex = 0;
                for (Map.Entry<Object, List<String>> entry : groupedResult.entrySet()) {
                  buffer.append("\n- ");
                  buffer.append(resultIndex++);
                  buffer.append(": ");

                  if (entry.getKey() instanceof ORawBuffer) {
                    final ORawBuffer r = ((ORawBuffer) entry.getKey());
                    if (r.buffer != null) {
                      buffer.append("bytes=");
                      buffer.append(Arrays.toString(r.buffer));

                      final ORecord record = Orient.instance().getRecordFactoryManager().newInstance(r.recordType, -1, getDatabase());
                      record.fromStream(r.buffer);
                      buffer.append(record);
                      buffer.append(" (size=");
                      buffer.append(r.buffer.length);
                      buffer.append(" v=");
                      buffer.append(r.version);
                      buffer.append(")");
                    } else
                      buffer.append("(empty)");
                  } else
                    buffer.append(entry.getKey());

                  buffer.append(" in servers ");
                  buffer.append(entry.getValue());
                }

                ODistributedServerLog.warn(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
                    "Auto repair cannot find a winner for record %s and the following groups of contents: %s", rid, buffer);
                continue;
              }

              for (Map.Entry<Object, List<String>> entry : groupedResult.entrySet()) {
                final Object value = entry.getKey();
                final List<String> servers = entry.getValue();

                for (String server : servers) {
                  if (winner == null && value != null || (winner != null && !winner.equals(value))) {
                    ODistributedServerLog.debug(this, dManager.getLocalNodeName(), server, ODistributedServerLog.DIRECTION.OUT,
                        "Preparing fix for record %s on servers %s, winner=%s remoteValue=%s...", rid, server, winner, value);

                    final OCompleted2pcTask completedTask = repairMap.get(server);

                    if (winner instanceof ORawBuffer && (value == null || value instanceof ORawBuffer)) {
                      // UPDATE THE RECORD
                      final ORawBuffer winnerRecord = (ORawBuffer) winner;

                      completedTask.addFixTask(
                          ((OFixUpdateRecordTask) dManager.getTaskFactoryManager().getFactoryByServerNames(involvedServers)
                              .createTask(OFixUpdateRecordTask.FACTORYID))
                              .init(rid, winnerRecord.buffer, ORecordVersionHelper.setRollbackMode(winnerRecord.version),
                                  winnerRecord.recordType));

                    } else if ((winner == null || winner instanceof ORecordNotFoundException) && value instanceof ORawBuffer) {
                      // DELETE THE RECORD
                      completedTask.addFixTask(
                          ((OFixCreateRecordTask) dManager.getTaskFactoryManager().getFactoryByServerNames(involvedServers)
                              .createTask(OFixCreateRecordTask.FACTORYID)).init(rid, -1));

                    } else if (value instanceof Throwable) {
                      // MANAGE EXCEPTION
                    }
                  }
                }
              }
            }
          }
        }
      } finally {
        int repaired = 0;
        for (Map.Entry<String, OCompleted2pcTask> entry : repairMap.entrySet()) {
          final String server = entry.getKey();
          final OCompleted2pcTask task = entry.getValue();

          repaired += task.getFixTasks().size();

          if (dManager.getLocalNodeName().equals(server))
            // EXECUTE IT LOCALLY
            dManager.executeOnLocalNode(requestId, task, db);
          else {
            // EXECUTE REMOTELY
            final List<String> servers = new ArrayList<String>();
            servers.add(server);

            // FILTER ONLY THE SERVER ONLINE
            dManager.getAvailableNodes(servers, databaseName);

            if (!servers.isEmpty()) {
              response = dManager.sendRequest(databaseName, clusterNames, servers, task, dManager.getNextMessageIdCounter(),
                  ODistributedRequest.EXECUTION_MODE.RESPONSE, null, null, null);

              if (response == null || response.getPayload() instanceof Throwable) {
                ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
                    "Auto repair cannot execute the fix, retrying it later (error=%s)",
                    response != null ? response.getPayload() : "no response");

                //usage of return inside finally can mask thrown exception
                skipTheRest = true;
                break;
              }
            }
          }
        }

        if (!skipTheRest) {
          if (repaired == 0)
            ODistributedServerLog
                .debug(this, dManager.getLocalNodeName(), involvedServers.toString(), ODistributedServerLog.DIRECTION.OUT,
                    "Auto repairing completed. No fix is needed (reqId=%s)", repaired, requestId);
          else
            ODistributedServerLog
                .info(this, dManager.getLocalNodeName(), involvedServers.toString(), ODistributedServerLog.DIRECTION.OUT,
                    "Auto repairing completed. Sent %d fix messages for %d records (reqId=%s)", repaired, rids.size(), requestId);
        }
      }

      if (skipTheRest)
        return false;
    } catch (Exception e) {
      ODistributedServerLog.debug(this, dManager.getLocalNodeName(), null, ODistributedServerLog.DIRECTION.NONE,
          "Error executing auto repairing (error=%s, reqId=%s)", e.toString(), requestId);
      return false;

    } finally {
      // RELEASE LOCKS AND REMOVE TX CONTEXT
      localDistributedDatabase.popTxContext(requestId);
      ctx.destroy();
    }

    return true;
  }

  public long getRecordProcessed() {
    return recordProcessed.get();
  }

  @Override
  public long getTotalTimeProcessing() {
    return totalTimeProcessing.get();
  }

  private ODatabaseDocumentInternal getDatabase() {
    return dManager.getMessageService().getDatabase(databaseName).getDatabaseInstance();
  }

  @Override
  public void shutdown() {
    if (checkTask != null) {
      checkTask.cancel();
    }
    records.clear();
  }
}


/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  Licensed under the Apache License, Version 2.0 (the "License");
 *  *  you may not use this file except in compliance with the License.
 *  *  You may obtain a copy of the License at
 *  *
 *  *       http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *  Unless required by applicable law or agreed to in writing, software
 *  *  distributed under the License is distributed on an "AS IS" BASIS,
 *  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *  See the License for the specific language governing permissions and
 *  *  limitations under the License.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.server.distributed.sql;

import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
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.impl.ODocument;
import com.orientechnologies.orient.core.sql.OCommandExecutorSQLAbstract;
import com.orientechnologies.orient.core.sql.OCommandSQLParsingException;
import com.orientechnologies.orient.core.sql.parser.OHaStatusStatement;
import com.orientechnologies.orient.core.sql.parser.OStatementCache;
import com.orientechnologies.orient.server.OServer;
import com.orientechnologies.orient.server.distributed.ODistributedConfiguration;
import com.orientechnologies.orient.server.distributed.impl.ODatabaseDocumentDistributed;
import com.orientechnologies.orient.server.distributed.impl.ODistributedOutput;
import com.orientechnologies.orient.server.hazelcast.OHazelcastPlugin;

import java.util.Map;

/**
 * SQL HA STATUS command: returns the high availability configuration.
 *
 * @author Luca Garulli
 */
@SuppressWarnings("unchecked")
public class OCommandExecutorSQLHAStatus extends OCommandExecutorSQLAbstract implements OCommandDistributedReplicateRequest {
  public static final String NAME           = "HA STATUS";
  public static final String KEYWORD_HA     = "HA";
  public static final String KEYWORD_STATUS = "STATUS";

  OHaStatusStatement parsedStatement;

  public OCommandExecutorSQLHAStatus parse(final OCommandRequest iRequest) {
    init((OCommandRequestText) iRequest);
    try {
      parsedStatement = (OHaStatusStatement) OStatementCache.get(this.parserText, getDatabase());
      preParsedStatement = parsedStatement;
    } catch (OCommandSQLParsingException sqlx) {
      throw sqlx;
    } catch (Exception e) {
      throwParsingException("Error parsing query: \n" + this.parserText + "\n" + e.getMessage(), e);
    }
    return this;
  }

  /**
   * Execute the command.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    final ODatabaseDocumentInternal database = getDatabase();
    database.checkSecurity(ORule.ResourceGeneric.SERVER, "status", ORole.PERMISSION_READ);

    if (!(database instanceof ODatabaseDocumentDistributed)) {
      throw new OCommandExecutionException("OrientDB is not started in distributed mode");
    }

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

    final String databaseName = database.getName();

    final ODistributedConfiguration cfg = dManager.getDatabaseConfiguration(databaseName);

    if (parsedStatement.outputText) {
      final StringBuilder output = new StringBuilder();
      if (parsedStatement.servers)
        output.append(ODistributedOutput.formatServerStatus(dManager, dManager.getClusterConfiguration()));
      if (parsedStatement.db)
        output.append(ODistributedOutput.formatClusterTable(dManager, databaseName, cfg, dManager.getTotalNodes(databaseName)));
      if (parsedStatement.latency)
        output.append(ODistributedOutput.formatLatency(dManager, dManager.getClusterConfiguration()));
      if (parsedStatement.messages)
        output.append(ODistributedOutput.formatMessages(dManager, dManager.getClusterConfiguration()));
      return output.toString();
    }

    final ODocument output = new ODocument();
    if (parsedStatement.servers)
      output.field("servers", dManager.getClusterConfiguration(), OType.EMBEDDED);
    if (parsedStatement.db)
      output.field("database", cfg.getDocument(), OType.EMBEDDED);

    return output;
  }

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

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

  @Override
  public String getSyntax() {
    return "HA STATUS [-servers] [-db] [-latency] [-messages] [-all] [-output=text]";
  }

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