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

import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.io.OIOUtils;
import com.orientechnologies.common.util.OPair;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.config.OStorageConfiguration;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.record.*;
import com.orientechnologies.orient.core.db.record.ORecordElement.STATUS;
import com.orientechnologies.orient.core.db.record.ridbag.ORidBag;
import com.orientechnologies.orient.core.exception.OQueryParsingException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordInternal;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;
import com.orientechnologies.orient.core.serialization.serializer.record.string.ORecordSerializerStringAbstract;
import com.orientechnologies.orient.core.sql.OSQLEngine;
import com.orientechnologies.orient.core.sql.OSQLHelper;
import com.orientechnologies.orient.core.sql.executor.OResult;
import com.orientechnologies.orient.core.sql.filter.OSQLPredicate;
import com.orientechnologies.orient.core.sql.functions.OSQLFunctionRuntime;
import com.orientechnologies.orient.core.sql.method.OSQLMethod;
import com.orientechnologies.orient.core.util.ODateHelper;

import java.lang.reflect.Array;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;

/**
 * Helper class to manage documents.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class ODocumentHelper {
  public static final String ATTRIBUTE_THIS    = "@this";
  public static final String ATTRIBUTE_RID     = "@rid";
  public static final String ATTRIBUTE_RID_ID  = "@rid_id";
  public static final String ATTRIBUTE_RID_POS = "@rid_pos";
  public static final String ATTRIBUTE_VERSION = "@version";
  public static final String ATTRIBUTE_CLASS   = "@class";
  public static final String ATTRIBUTE_TYPE    = "@type";
  public static final String ATTRIBUTE_SIZE    = "@size";
  public static final String ATTRIBUTE_FIELDS  = "@fields";
  public static final String ATTRIBUTE_RAW     = "@raw";

  public static interface ODbRelatedCall<T> {
    T call(ODatabaseDocumentInternal database);
  }

  public static interface RIDMapper {
    ORID map(ORID rid);
  }

  public static Set<String> getReservedAttributes() {
    Set<String> retSet = new HashSet<>();
    retSet.add(ATTRIBUTE_THIS);
    retSet.add(ATTRIBUTE_RID);
    retSet.add(ATTRIBUTE_RID_ID);
    retSet.add(ATTRIBUTE_RID_POS);
    retSet.add(ATTRIBUTE_VERSION);
    retSet.add(ATTRIBUTE_CLASS);
    retSet.add(ATTRIBUTE_TYPE);
    retSet.add(ATTRIBUTE_SIZE);
    retSet.add(ATTRIBUTE_FIELDS);
    retSet.add(ATTRIBUTE_RAW);
    return retSet;
  }

  public static void sort(List<? extends OIdentifiable> ioResultSet, List<OPair<String, String>> iOrderCriteria,
      OCommandContext context) {
    if (ioResultSet != null)
      Collections.sort(ioResultSet, new ODocumentComparator(iOrderCriteria, context));
  }

  @SuppressWarnings("unchecked")
  public static <RET> RET convertField(final ODocument iDocument, final String iFieldName, OType type, Class<?> iFieldType,
      Object iValue) {
    if (iFieldType == null && type != null)
      iFieldType = type.getDefaultJavaType();

    if (iFieldType == null)
      return (RET) iValue;

    if (ORID.class.isAssignableFrom(iFieldType)) {
      if (iValue instanceof ORID) {
        return (RET) iValue;
      } else if (iValue instanceof String) {
        return (RET) new ORecordId((String) iValue);
      } else if (iValue instanceof ORecord) {
        return (RET) ((ORecord) iValue).getIdentity();
      }
    } else if (OIdentifiable.class.isAssignableFrom(iFieldType)) {
      if (iValue instanceof ORID || iValue instanceof ORecord) {
        return (RET) iValue;
      } else if (iValue instanceof String) {
        return (RET) new ORecordId((String) iValue);
      } else if (OMultiValue.isMultiValue(iValue) && OMultiValue.getSize(iValue) == 1) {
        Object val = OMultiValue.getFirstValue(iValue);
        if (val instanceof OResult) {
          val = ((OResult) val).getIdentity().orElse(null);
        }
        if (val instanceof OIdentifiable) {
          return (RET) val;
        }
      }
    } else if (Set.class.isAssignableFrom(iFieldType)) {
      if (!(iValue instanceof Set)) {
        // CONVERT IT TO SET
        final Collection<?> newValue;

        if (type.isLink())
          newValue = new ORecordLazySet(iDocument);
        else
          newValue = new OTrackedSet<Object>(iDocument);

        if (iValue instanceof Collection<?>) {
          ((Collection<Object>) newValue).addAll((Collection<Object>) iValue);
          return (RET) newValue;
        } else if (iValue instanceof Map) {
          ((Collection<Object>) newValue).addAll(((Map<String, Object>) iValue).values());
          return (RET) newValue;
        } else if (iValue instanceof String) {
          final String stringValue = (String) iValue;

          if (stringValue != null && !stringValue.isEmpty()) {
            final String[] items = stringValue.split(",");
            for (String s : items) {
              ((Collection<Object>) newValue).add(s);
            }
          }
          return (RET) newValue;
        } else if (OMultiValue.isMultiValue(iValue)) {
          // GENERIC MULTI VALUE
          for (Object s : OMultiValue.getMultiValueIterable(iValue, false)) {
            ((Collection<Object>) newValue).add(s);
          }
          return (RET) newValue;
        }
      } else {
        return (RET) iValue;
      }
    } else if (List.class.isAssignableFrom(iFieldType)) {
      if (!(iValue instanceof List)) {
        // CONVERT IT TO LIST
        final Collection<?> newValue;

        if (type.isLink())
          newValue = new ORecordLazyList(iDocument);
        else
          newValue = new OTrackedList<Object>(iDocument);

        if (iValue instanceof Collection) {
          ((Collection<Object>) newValue).addAll((Collection<Object>) iValue);
          return (RET) newValue;
        } else if (iValue instanceof Map) {
          ((Collection<Object>) newValue).addAll(((Map<String, Object>) iValue).values());
          return (RET) newValue;
        } else if (iValue instanceof String) {
          final String stringValue = (String) iValue;

          if (stringValue != null && !stringValue.isEmpty()) {
            final String[] items = stringValue.split(",");
            for (String s : items) {
              ((Collection<Object>) newValue).add(s);
            }
          }
          return (RET) newValue;
        } else if (OMultiValue.isMultiValue(iValue)) {
          // GENERIC MULTI VALUE
          for (Object s : OMultiValue.getMultiValueIterable(iValue)) {
            ((Collection<Object>) newValue).add(s);
          }
          return (RET) newValue;
        }
      } else {
        return (RET) iValue;
      }
    } else if (iValue instanceof Enum) {
      // ENUM
      if (Number.class.isAssignableFrom(iFieldType))
        iValue = ((Enum<?>) iValue).ordinal();
      else
        iValue = iValue.toString();
      if (!(iValue instanceof String) && !iFieldType.isAssignableFrom(iValue.getClass()))
        throw new IllegalArgumentException(
            "Property '" + iFieldName + "' of type '" + iFieldType + "' cannot accept value of type: " + iValue.getClass());
    } else if (Date.class.isAssignableFrom(iFieldType)) {
      if (iValue instanceof String && ODatabaseRecordThreadLocal.instance().isDefined()) {
        final OStorageConfiguration config = ODatabaseRecordThreadLocal.instance().get().getStorage().getConfiguration();

        DateFormat formatter = config.getDateFormatInstance();

        if (((String) iValue).length() > config.getDateFormat().length()) {
          // ASSUMES YOU'RE USING THE DATE-TIME FORMATTE
          formatter = config.getDateTimeFormatInstance();
        }

        try {
          Date newValue = formatter.parse((String) iValue);
          // _fieldValues.put(iFieldName, newValue);
          return (RET) newValue;
        } catch (ParseException pe) {
          final String dateFormat =
              ((String) iValue).length() > config.getDateFormat().length() ? config.getDateTimeFormat() : config.getDateFormat();
          throw OException.wrapException(
              new OQueryParsingException("Error on conversion of date '" + iValue + "' using the format: " + dateFormat), pe);
        }
      }
    }

    iValue = OType.convert(iValue, iFieldType);

    return (RET) iValue;
  }

  @SuppressWarnings("unchecked")
  public static <RET> RET getFieldValue(Object value, final String iFieldName) {
    return (RET) getFieldValue(value, iFieldName, null);
  }

  @SuppressWarnings("unchecked")
  public static <RET> RET getFieldValue(Object value, final String iFieldName, final OCommandContext iContext) {
    if (value == null)
      return null;

    final int fieldNameLength = iFieldName.length();
    if (fieldNameLength == 0)
      return (RET) value;

    OIdentifiable currentRecord = value instanceof OIdentifiable ? (OIdentifiable) value : null;

    int beginPos = iFieldName.charAt(0) == '.' ? 1 : 0;
    int nextSeparatorPos = iFieldName.charAt(0) == '.' ? 1 : 0;
    boolean firstInChain = true;
    do {
      char nextSeparator = ' ';
      for (; nextSeparatorPos < fieldNameLength; ++nextSeparatorPos) {
        nextSeparator = iFieldName.charAt(nextSeparatorPos);
        if (nextSeparator == '.' || nextSeparator == '[')
          break;
      }

      final String fieldName;
      if (nextSeparatorPos < fieldNameLength)
        fieldName = iFieldName.substring(beginPos, nextSeparatorPos);
      else {
        nextSeparator = ' ';
        if (beginPos > 0)
          fieldName = iFieldName.substring(beginPos);
        else
          fieldName = iFieldName;
      }

      if (nextSeparator == '[') {
        if (fieldName != null && fieldName.length() > 0) {
          if (currentRecord != null)
            value = getIdentifiableValue(currentRecord, fieldName);
          else if (value instanceof Map<?, ?>)
            value = getMapEntry((Map<String, ?>) value, fieldName);
          else if (OMultiValue.isMultiValue(value)) {
            final HashSet<Object> temp = new LinkedHashSet<Object>();
            for (Object o : OMultiValue.getMultiValueIterable(value, false)) {
              if (o instanceof OIdentifiable) {
                Object r = getFieldValue(o, iFieldName);
                if (r != null)
                  OMultiValue.add(temp, r);
              }
            }
            value = temp;
          }
        }

        if (value == null)
          return null;
        else if (value instanceof OIdentifiable)
          currentRecord = (OIdentifiable) value;

        // final int end = iFieldName.indexOf(']', nextSeparatorPos);
        final int end = findClosingBracketPosition(iFieldName, nextSeparatorPos);
        if (end == -1)
          throw new IllegalArgumentException("Missed closed ']'");

        String indexPart = iFieldName.substring(nextSeparatorPos + 1, end);
        if (indexPart.length() == 0)
          return null;

        nextSeparatorPos = end;

        if (value instanceof OCommandContext)
          value = ((OCommandContext) value).getVariables();

        if (value instanceof OIdentifiable) {
          final ORecord record =
              currentRecord != null && currentRecord instanceof OIdentifiable ? ((OIdentifiable) currentRecord).getRecord() : null;

          final Object index = getIndexPart(iContext, indexPart);
          final String indexAsString = index != null ? index.toString() : null;

          final List<String> indexParts = OStringSerializerHelper
              .smartSplit(indexAsString, ',', OStringSerializerHelper.DEFAULT_IGNORE_CHARS);
          final List<String> indexRanges = OStringSerializerHelper.smartSplit(indexAsString, '-', ' ');
          final List<String> indexCondition = OStringSerializerHelper.smartSplit(indexAsString, '=', ' ');

          if (indexParts.size() == 1 && indexCondition.size() == 1 && indexRanges.size() == 1)
            // SINGLE VALUE
            value = ((ODocument) record).field(indexAsString);
          else if (indexParts.size() > 1) {
            // MULTI VALUE
            final Object[] values = new Object[indexParts.size()];
            for (int i = 0; i < indexParts.size(); ++i) {
              values[i] = ((ODocument) record).field(OIOUtils.getStringContent(indexParts.get(i)));
            }
            value = values;
          } else if (indexRanges.size() > 1) {

            // MULTI VALUES RANGE
            String from = indexRanges.get(0);
            String to = indexRanges.get(1);

            final ODocument doc = (ODocument) record;

            final String[] fieldNames = doc.fieldNames();
            final int rangeFrom = from != null && !from.isEmpty() ? Integer.parseInt(from) : 0;
            final int rangeTo =
                to != null && !to.isEmpty() ? Math.min(Integer.parseInt(to), fieldNames.length - 1) : fieldNames.length - 1;

            final Object[] values = new Object[rangeTo - rangeFrom + 1];

            for (int i = rangeFrom; i <= rangeTo; ++i)
              values[i - rangeFrom] = doc.field(fieldNames[i]);

            value = values;

          } else if (!indexCondition.isEmpty()) {
            // CONDITION
            final String conditionFieldName = indexCondition.get(0);
            Object conditionFieldValue = ORecordSerializerStringAbstract.getTypeValue(indexCondition.get(1));

            if (conditionFieldValue instanceof String)
              conditionFieldValue = OIOUtils.getStringContent(conditionFieldValue);

            final Object fieldValue = getFieldValue(currentRecord, conditionFieldName);

            if (conditionFieldValue != null && fieldValue != null)
              conditionFieldValue = OType.convert(conditionFieldValue, fieldValue.getClass());

            if (fieldValue == null && !conditionFieldValue.equals("null") || fieldValue != null && !fieldValue
                .equals(conditionFieldValue))
              value = null;
          }
        } else if (value instanceof Map<?, ?>) {
          final Object index = getIndexPart(iContext, indexPart);
          final String indexAsString = index != null ? index.toString() : null;

          final List<String> indexParts = OStringSerializerHelper
              .smartSplit(indexAsString, ',', OStringSerializerHelper.DEFAULT_IGNORE_CHARS);
          final List<String> indexRanges = OStringSerializerHelper.smartSplit(indexAsString, '-', ' ');
          final List<String> indexCondition = OStringSerializerHelper.smartSplit(indexAsString, '=', ' ');

          final Map<String, ?> map = (Map<String, ?>) value;
          if (indexParts.size() == 1 && indexCondition.size() == 1 && indexRanges.size() == 1)
            // SINGLE VALUE
            value = map.get(index);
          else if (indexParts.size() > 1) {
            // MULTI VALUE
            final Object[] values = new Object[indexParts.size()];
            for (int i = 0; i < indexParts.size(); ++i) {
              values[i] = map.get(OIOUtils.getStringContent(indexParts.get(i)));
            }
            value = values;
          } else if (indexRanges.size() > 1) {

            // MULTI VALUES RANGE
            String from = indexRanges.get(0);
            String to = indexRanges.get(1);

            final List<String> fieldNames = new ArrayList<String>(map.keySet());
            final int rangeFrom = from != null && !from.isEmpty() ? Integer.parseInt(from) : 0;
            final int rangeTo =
                to != null && !to.isEmpty() ? Math.min(Integer.parseInt(to), fieldNames.size() - 1) : fieldNames.size() - 1;

            final Object[] values = new Object[rangeTo - rangeFrom + 1];

            for (int i = rangeFrom; i <= rangeTo; ++i)
              values[i - rangeFrom] = map.get(fieldNames.get(i));

            value = values;

          } else if (!indexCondition.isEmpty()) {
            // CONDITION
            final String conditionFieldName = indexCondition.get(0);
            Object conditionFieldValue = ORecordSerializerStringAbstract.getTypeValue(indexCondition.get(1));

            if (conditionFieldValue instanceof String)
              conditionFieldValue = OIOUtils.getStringContent(conditionFieldValue);

            final Object fieldValue = map.get(conditionFieldName);

            if (conditionFieldValue != null && fieldValue != null)
              conditionFieldValue = OType.convert(conditionFieldValue, fieldValue.getClass());

            if (fieldValue == null && !conditionFieldValue.equals("null") || fieldValue != null && !fieldValue
                .equals(conditionFieldValue))
              value = null;
          }

        } else if (OMultiValue.isMultiValue(value)) {
          // MULTI VALUE
          final Object index = getIndexPart(iContext, indexPart);
          final String indexAsString = index != null ? index.toString() : null;

          final List<String> indexParts = OStringSerializerHelper.smartSplit(indexAsString, ',');
          final List<String> indexRanges = OStringSerializerHelper.smartSplit(indexAsString, '-');
          final List<String> indexCondition = OStringSerializerHelper.smartSplit(indexAsString, '=', ' ');

          if (isFieldName(indexAsString)) {
            // SINGLE VALUE
            if (value instanceof Map<?, ?>)
              value = getMapEntry((Map<String, ?>) value, index);
            else if (Character.isDigit(indexAsString.charAt(0)))
              value = OMultiValue.getValue(value, Integer.parseInt(indexAsString));
            else
              // FILTER BY FIELD
              value = getFieldValue(value, indexAsString, iContext);

          } else if (isListOfNumbers(indexParts)) {

            // MULTI VALUES
            final Object[] values = new Object[indexParts.size()];
            for (int i = 0; i < indexParts.size(); ++i)
              values[i] = OMultiValue.getValue(value, Integer.parseInt(indexParts.get(i)));
            if (indexParts.size() > 1) {
              value = values;
            } else {
              value = values[0];
            }

          } else if (isListOfNumbers(indexRanges)) {

            // MULTI VALUES RANGE
            String from = indexRanges.get(0);
            String to = indexRanges.get(1);

            final int rangeFrom = from != null && !from.isEmpty() ? Integer.parseInt(from) : 0;
            final int rangeTo = to != null && !to.isEmpty() ?
                Math.min(Integer.parseInt(to), OMultiValue.getSize(value) - 1) :
                OMultiValue.getSize(value) - 1;

            int arraySize = rangeTo - rangeFrom + 1;
            if (arraySize < 0) {
              arraySize = 0;
            }
            final Object[] values = new Object[arraySize];
            for (int i = rangeFrom; i <= rangeTo; ++i)
              values[i - rangeFrom] = OMultiValue.getValue(value, i);
            value = values;

          } else {
            // CONDITION
            OSQLPredicate pred = new OSQLPredicate(indexAsString);
            final HashSet<Object> values = new LinkedHashSet<Object>();

            for (Object v : OMultiValue.getMultiValueIterable(value)) {
              if (v instanceof OIdentifiable) {
                Object result = pred.evaluate((OIdentifiable) v, (ODocument) ((OIdentifiable) v).getRecord(), iContext);
                if (Boolean.TRUE.equals(result)) {
                  values.add(v);
                }
              } else if (v instanceof Map) {
                ODocument doc = new ODocument().fromMap((Map<String, ? extends Object>) v);
                Object result = pred.evaluate(doc, doc, iContext);
                if (Boolean.TRUE.equals(result)) {
                  values.add(v);
                }
              }
            }

            if (values.isEmpty())
              // RETURNS NULL
              value = values;
            else if (values.size() == 1)
              // RETURNS THE SINGLE ODOCUMENT
              value = values.iterator().next();
            else
              // RETURNS THE FILTERED COLLECTION
              value = values;
          }
        }
      } else {
        if (fieldName.length() == 0) {
          // NO FIELD NAME: THIS IS THE CASE OF NOT USEFUL . AFTER A ] OR .
          beginPos = ++nextSeparatorPos;
          continue;
        }

        if (fieldName.startsWith("$"))
          value = iContext.getVariable(fieldName);
        else if (fieldName.contains("(")) {
          boolean executedMethod = false;
          if (!firstInChain && fieldName.endsWith("()")) {
            OSQLMethod method = OSQLEngine.getMethod(fieldName.substring(0, fieldName.length() - 2));
            if (method != null) {
              value = method.execute(value, currentRecord, iContext, value, new Object[] {});
              executedMethod = true;
            }
          }
          if (!executedMethod) {
            value = evaluateFunction(value, fieldName, iContext);
          }
        } else {
          final List<String> indexCondition = OStringSerializerHelper.smartSplit(fieldName, '=', ' ');

          if (indexCondition.size() == 2) {
            final String conditionFieldName = indexCondition.get(0);
            Object conditionFieldValue = ORecordSerializerStringAbstract.getTypeValue(indexCondition.get(1));

            if (conditionFieldValue instanceof String)
              conditionFieldValue = OIOUtils.getStringContent(conditionFieldValue);

            value = filterItem(conditionFieldName, conditionFieldValue, value);

          } else if (currentRecord != null) {
            // GET THE LINKED OBJECT IF ANY
            value = getIdentifiableValue(currentRecord, fieldName);
            if (value != null && value instanceof ORecord && ((ORecord) value).getInternalStatus() == STATUS.NOT_LOADED)
              // RELOAD IT
              ((ORecord) value).reload();
          } else if (value instanceof Map<?, ?>)
            value = getMapEntry((Map<String, ?>) value, fieldName);
          else if (OMultiValue.isMultiValue(value)) {
            final Set<Object> values = new LinkedHashSet<Object>();
            for (Object v : OMultiValue.getMultiValueIterable(value, false)) {
              final Object item;

              if (v instanceof OIdentifiable)
                item = getIdentifiableValue((OIdentifiable) v, fieldName);
              else if (v instanceof Map)
                item = ((Map<?, ?>) v).get(fieldName);
              else
                item = null;

              if (item != null)
                if (item instanceof Collection<?>)
                  values.addAll((Collection<? extends Object>) item);
                else
                  values.add(item);
            }

            if (values.isEmpty())
              value = null;
            else
              value = values;
          } else
            return null;
        }
      }

      if (value instanceof OIdentifiable)
        currentRecord = (OIdentifiable) value;
      else
        currentRecord = null;

      beginPos = ++nextSeparatorPos;
      firstInChain = false;
    } while (nextSeparatorPos < fieldNameLength && value != null);

    return (RET) value;
  }

  private static int findClosingBracketPosition(String iFieldName, int nextSeparatorPos) {
    Character currentQuote = null;
    boolean escaping = false;
    int innerBrackets = 0;
    char[] chars = iFieldName.toCharArray();
    for (int i = nextSeparatorPos + 1; i < chars.length; i++) {
      char next = chars[i];
      if (escaping) {
        escaping = false;
      } else if (next == '\\') {
        escaping = true;
      } else if (next == '`' || next == '\'' || next == '"') {
        if (currentQuote == null) {
          currentQuote = next;
        } else if (currentQuote == next) {
          currentQuote = null;
        }

      } else if (next == '[') {
        innerBrackets++;
      } else if (next == ']') {
        if (innerBrackets == 0) {
          return i;
        }
        innerBrackets--;
      }
    }
    return -1;
  }

  private static boolean isFieldName(String indexAsString) {
    indexAsString = indexAsString.trim();
    if (indexAsString.startsWith("`") && indexAsString.endsWith("`")) {
      // quoted identifier
      return !indexAsString.substring(1, indexAsString.length() - 1).contains("`");
    }
    boolean firstChar = true;
    for (char c : indexAsString.toCharArray()) {
      if (isLetter(c) || (isNumber(c) && !firstChar)) {
        firstChar = false;
        continue;
      }
      return false;
    }
    return true;
  }

  private static boolean isNumber(char c) {
    return c >= '0' && c <= '9';
  }

  private static boolean isLetter(char c) {
    if (c == '$' || c == '_' || c == '@') {
      return true;
    }
    if (c >= 'a' && c <= 'z') {
      return true;
    }
    if (c >= 'A' && c <= 'Z') {
      return true;
    }

    return false;
  }

  private static boolean isListOfNumbers(List<String> list) {
    for (String s : list) {
      try {
        Integer.parseInt(s);
      } catch (NumberFormatException ignore) {
        return false;
      }
    }
    return true;
  }

  protected static Object getIndexPart(final OCommandContext iContext, final String indexPart) {
    Object index = indexPart;
    if (indexPart.indexOf(',') == -1 && (indexPart.charAt(0) == '"' || indexPart.charAt(0) == '\''))
      index = OIOUtils.getStringContent(indexPart);
    else if (indexPart.charAt(0) == '$') {
      final Object ctxValue = iContext.getVariable(indexPart);
      if (ctxValue == null)
        return null;
      index = ctxValue;
    } else if (!Character.isDigit(indexPart.charAt(0)))
      // GET FROM CURRENT VALUE
      index = indexPart;
    return index;
  }

  @SuppressWarnings("unchecked")
  protected static Object filterItem(final String iConditionFieldName, final Object iConditionFieldValue, final Object iValue) {
    if (iValue instanceof OIdentifiable) {
      final ORecord rec = ((OIdentifiable) iValue).getRecord();
      if (rec instanceof ODocument) {
        final ODocument doc = (ODocument) rec;

        Object fieldValue = doc.field(iConditionFieldName);

        if (iConditionFieldValue == null)
          return fieldValue == null ? doc : null;

        fieldValue = OType.convert(fieldValue, iConditionFieldValue.getClass());
        if (fieldValue != null && fieldValue.equals(iConditionFieldValue))
          return doc;
      }
    } else if (iValue instanceof Map<?, ?>) {
      final Map<String, ?> map = (Map<String, ?>) iValue;
      Object fieldValue = getMapEntry(map, iConditionFieldName);

      fieldValue = OType.convert(fieldValue, iConditionFieldValue.getClass());
      if (fieldValue != null && fieldValue.equals(iConditionFieldValue))
        return map;
    }
    return null;
  }

  /**
   * Retrieves the value crossing the map with the dotted notation
   *
   * @param iKey Field(s) to retrieve. If are multiple fields, then the dot must be used as separator
   * @param iMap
   *
   * @return
   */
  @SuppressWarnings("unchecked")
  public static Object getMapEntry(final Map<String, ?> iMap, final Object iKey) {
    if (iMap == null || iKey == null)
      return null;

    if (iKey instanceof String) {
      String iName = (String) iKey;
      int pos = iName.indexOf('.');
      if (pos > -1)
        iName = iName.substring(0, pos);

      final Object value = iMap.get(iName);
      if (value == null)
        return null;

      if (pos > -1) {
        final String restFieldName = iName.substring(pos + 1);
        if (value instanceof ODocument)
          return getFieldValue(value, restFieldName);
        else if (value instanceof Map<?, ?>)
          return getMapEntry((Map<String, ?>) value, restFieldName);
      }

      return value;
    } else
      return iMap.get(iKey);
  }

  public static Object getIdentifiableValue(final OIdentifiable iCurrent, final String iFieldName) {
    if (iFieldName == null)
      return null;

    if (iFieldName.length() > 0) {
      final char begin = iFieldName.charAt(0);
      if (begin == '@') {
        // RETURN AN ATTRIBUTE
        if (iFieldName.equalsIgnoreCase(ATTRIBUTE_THIS))
          return iCurrent.getRecord();
        else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_RID))
          return iCurrent.getIdentity();
        else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_RID_ID))
          return iCurrent.getIdentity().getClusterId();
        else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_RID_POS))
          return iCurrent.getIdentity().getClusterPosition();
        else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_VERSION))
          return iCurrent.getRecord().getVersion();
        else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_CLASS))
          return ((ODocument) iCurrent.getRecord()).getClassName();
        else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_TYPE))
          return Orient.instance().getRecordFactoryManager().getRecordTypeName(ORecordInternal.getRecordType(iCurrent.getRecord()));
        else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_SIZE)) {
          final byte[] stream = iCurrent.getRecord().toStream();
          return stream != null ? stream.length : 0;
        } else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_FIELDS))
          return ((ODocument) iCurrent.getRecord()).fieldNames();
        else if (iFieldName.equalsIgnoreCase(ATTRIBUTE_RAW))
          return new String(iCurrent.getRecord().toStream());
      }
    }
    if (iCurrent == null)
      return null;

    final ODocument doc = ((ODocument) iCurrent.getRecord());
    if (doc == null) {// broken link
      return null;
    }
    doc.checkForFields(iFieldName);
    ODocumentEntry entry = doc._fields.get(iFieldName);
    return entry != null ? entry.value : null;
  }

  public static Object evaluateFunction(final Object currentValue, final String iFunction, final OCommandContext iContext) {
    if (currentValue == null)
      return null;

    Object result = null;

    final String function = iFunction.toUpperCase(Locale.ENGLISH);

    if (function.startsWith("SIZE("))
      result = currentValue instanceof ORecord ? 1 : OMultiValue.getSize(currentValue);
    else if (function.startsWith("LENGTH("))
      result = currentValue.toString().length();
    else if (function.startsWith("TOUPPERCASE("))
      result = currentValue.toString().toUpperCase(Locale.ENGLISH);
    else if (function.startsWith("TOLOWERCASE("))
      result = currentValue.toString().toLowerCase(Locale.ENGLISH);
    else if (function.startsWith("TRIM("))
      result = currentValue.toString().trim();
    else if (function.startsWith("TOJSON("))
      result = currentValue instanceof ODocument ? ((ODocument) currentValue).toJSON() : null;
    else if (function.startsWith("KEYS("))
      result = currentValue instanceof Map<?, ?> ? ((Map<?, ?>) currentValue).keySet() : null;
    else if (function.startsWith("VALUES("))
      result = currentValue instanceof Map<?, ?> ? ((Map<?, ?>) currentValue).values() : null;
    else if (function.startsWith("ASSTRING("))
      result = currentValue.toString();
    else if (function.startsWith("ASINTEGER("))
      result = new Integer(currentValue.toString());
    else if (function.startsWith("ASFLOAT("))
      result = new Float(currentValue.toString());
    else if (function.startsWith("ASBOOLEAN(")) {
      if (currentValue instanceof String)
        result = new Boolean((String) currentValue);
      else if (currentValue instanceof Number) {
        final int bValue = ((Number) currentValue).intValue();
        if (bValue == 0)
          result = Boolean.FALSE;
        else if (bValue == 1)
          result = Boolean.TRUE;
      }
    } else if (function.startsWith("ASDATE("))
      if (currentValue instanceof Date)
        result = currentValue;
      else if (currentValue instanceof Number)
        result = new Date(((Number) currentValue).longValue());
      else
        try {
          result = ODatabaseRecordThreadLocal.instance().get().getStorage().getConfiguration().getDateFormatInstance()
              .parse(currentValue.toString());
        } catch (ParseException ignore) {
        }
    else if (function.startsWith("ASDATETIME("))
      if (currentValue instanceof Date)
        result = currentValue;
      else if (currentValue instanceof Number)
        result = new Date(((Number) currentValue).longValue());
      else
        try {
          result = ODatabaseRecordThreadLocal.instance().get().getStorage().getConfiguration().getDateTimeFormatInstance()
              .parse(currentValue.toString());
        } catch (ParseException ignore) {
        }
    else {
      // EXTRACT ARGUMENTS
      final List<String> args = OStringSerializerHelper.getParameters(iFunction.substring(iFunction.indexOf('(')));

      final ORecord currentRecord = iContext != null ? (ORecord) iContext.getVariable("$current") : null;
      for (int i = 0; i < args.size(); ++i) {
        final String arg = args.get(i);
        final Object o = OSQLHelper.getValue(arg, currentRecord, iContext);
        if (o != null)
          args.set(i, o.toString());
      }

      if (function.startsWith("CHARAT("))
        result = currentValue.toString().charAt(Integer.parseInt(args.get(0)));
      else if (function.startsWith("INDEXOF("))
        if (args.size() == 1)
          result = currentValue.toString().indexOf(OIOUtils.getStringContent(args.get(0)));
        else
          result = currentValue.toString().indexOf(OIOUtils.getStringContent(args.get(0)), Integer.parseInt(args.get(1)));
      else if (function.startsWith("SUBSTRING("))
        if (args.size() == 1)
          result = currentValue.toString().substring(Integer.parseInt(args.get(0)));
        else
          result = currentValue.toString().substring(Integer.parseInt(args.get(0)), Integer.parseInt(args.get(1)));
      else if (function.startsWith("APPEND("))
        result = currentValue.toString() + OIOUtils.getStringContent(args.get(0));
      else if (function.startsWith("PREFIX("))
        result = OIOUtils.getStringContent(args.get(0)) + currentValue.toString();
      else if (function.startsWith("FORMAT("))
        if (currentValue instanceof Date) {
          SimpleDateFormat formatter = new SimpleDateFormat(OIOUtils.getStringContent(args.get(0)));
          formatter.setTimeZone(ODateHelper.getDatabaseTimeZone());
          result = formatter.format(currentValue);
        } else
          result = String.format(OIOUtils.getStringContent(args.get(0)), currentValue.toString());
      else if (function.startsWith("LEFT(")) {
        final int len = Integer.parseInt(args.get(0));
        final String stringValue = currentValue.toString();
        result = stringValue.substring(0, len <= stringValue.length() ? len : stringValue.length());
      } else if (function.startsWith("RIGHT(")) {
        final int offset = Integer.parseInt(args.get(0));
        final String stringValue = currentValue.toString();
        result = stringValue.substring(offset < stringValue.length() ? stringValue.length() - offset : 0);
      } else {
        final OSQLFunctionRuntime f = OSQLHelper.getFunction(null, iFunction);
        if (f != null)
          result = f.execute(currentRecord, currentRecord, null, iContext);
      }
    }

    return result;
  }

  @SuppressWarnings("unchecked")
  public static Object cloneValue(ODocument iCloned, final Object fieldValue) {

    if (fieldValue != null) {
      if (fieldValue instanceof ODocument && ((ODocument) fieldValue).isEmbedded()) {
        // EMBEDDED DOCUMENT
        return ((ODocument) fieldValue).copy();
      } else if (fieldValue instanceof ORidBag) {
        ORidBag newBag = ((ORidBag) fieldValue).copy();
        newBag.setOwner(null);
        newBag.setOwner(iCloned);
        return newBag;

      } else if (fieldValue instanceof ORecordLazyList) {
        return ((ORecordLazyList) fieldValue).copy(iCloned);

      } else if (fieldValue instanceof ORecordTrackedList) {
        final ORecordTrackedList newList = new ORecordTrackedList(iCloned);
        newList.addAll((ORecordTrackedList) fieldValue);
        return newList;

      } else if (fieldValue instanceof OTrackedList<?>) {
        final OTrackedList<Object> newList = new OTrackedList<Object>(iCloned);
        newList.addAll((OTrackedList<Object>) fieldValue);
        return newList;

      } else if (fieldValue instanceof List<?>) {
        return new ArrayList<Object>((List<Object>) fieldValue);

        // SETS
      } else if (fieldValue instanceof ORecordLazySet) {
        final ORecordLazySet newList = new ORecordLazySet(iCloned);
        newList.addAll((ORecordLazySet) fieldValue);
        return newList;

      } else if (fieldValue instanceof ORecordTrackedSet) {
        final ORecordTrackedSet newList = new ORecordTrackedSet(iCloned);
        newList.addAll((ORecordTrackedSet) fieldValue);
        return newList;

      } else if (fieldValue instanceof OTrackedSet<?>) {
        final OTrackedSet<Object> newList = new OTrackedSet<Object>(iCloned);
        newList.addAll((OTrackedSet<Object>) fieldValue);
        return newList;

      } else if (fieldValue instanceof Set<?>) {
        return new HashSet<Object>((Set<Object>) fieldValue);
        // MAPS
      } else if (fieldValue instanceof ORecordLazyMap) {
        final ORecordLazyMap newMap = new ORecordLazyMap(iCloned, ((ORecordLazyMap) fieldValue).getRecordType());
        newMap.putAll((ORecordLazyMap) fieldValue);
        return newMap;

      } else if (fieldValue instanceof OTrackedMap) {
        final OTrackedMap<Object> newMap = new OTrackedMap<Object>(iCloned);
        newMap.putAll((OTrackedMap<Object>) fieldValue);
        return newMap;

      } else if (fieldValue instanceof Map<?, ?>) {
        return new LinkedHashMap<String, Object>((Map<String, Object>) fieldValue);
      } else
        return fieldValue;
    }
    // else if (iCloned.getImmutableSchemaClass() != null) {
    // final OProperty prop = iCloned.getImmutableSchemaClass().getProperty(iEntry.getKey());
    // if (prop != null && prop.isMandatory())
    // return fieldValue;
    // }
    return null;
  }

  public static boolean hasSameContentItem(final Object iCurrent, ODatabaseDocumentInternal iMyDb, final Object iOther,
      final ODatabaseDocumentInternal iOtherDb, RIDMapper ridMapper) {
    if (iCurrent instanceof ODocument) {
      final ODocument current = (ODocument) iCurrent;
      if (iOther instanceof ORID) {
        if (!current.isDirty()) {
          ORID id;
          if (ridMapper != null) {
            ORID mappedId = ridMapper.map(current.getIdentity());
            if (mappedId != null)
              id = mappedId;
            else
              id = current.getIdentity();
          } else
            id = current.getIdentity();

          if (!id.equals(iOther)) {
            return false;
          }
        } else {
          final ODocument otherDoc = iOtherDb.load((ORID) iOther);
          if (!ODocumentHelper.hasSameContentOf(current, iMyDb, otherDoc, iOtherDb, ridMapper))
            return false;
        }
      } else if (!ODocumentHelper.hasSameContentOf(current, iMyDb, (ODocument) iOther, iOtherDb, ridMapper))
        return false;
    } else if (!compareScalarValues(iCurrent, iMyDb, iOther, iOtherDb, ridMapper))
      return false;
    return true;
  }

  /**
   * Makes a deep comparison field by field to check if the passed ODocument instance is identical as identity and content to the
   * current one. Instead equals() just checks if the RID are the same.
   *
   * @param iOther ODocument instance
   *
   * @return true if the two document are identical, otherwise false
   *
   * @see #equals(Object)
   */
  @SuppressWarnings("unchecked")
  public static boolean hasSameContentOf(final ODocument iCurrent, final ODatabaseDocumentInternal iMyDb, final ODocument iOther,
      final ODatabaseDocumentInternal iOtherDb, RIDMapper ridMapper) {
    return hasSameContentOf(iCurrent, iMyDb, iOther, iOtherDb, ridMapper, true);
  }

  /**
   * Makes a deep comparison field by field to check if the passed ODocument instance is identical in the content to the current
   * one. Instead equals() just checks if the RID are the same.
   *
   * @param iOther ODocument instance
   *
   * @return true if the two document are identical, otherwise false
   *
   * @see #equals(Object)
   */
  @SuppressWarnings("unchecked")
  public static boolean hasSameContentOf(final ODocument iCurrent, final ODatabaseDocumentInternal iMyDb, final ODocument iOther,
      final ODatabaseDocumentInternal iOtherDb, RIDMapper ridMapper, final boolean iCheckAlsoIdentity) {
    if (iOther == null)
      return false;

    if (iCheckAlsoIdentity && iCurrent.getIdentity().isValid() && !iCurrent.getIdentity().equals(iOther.getIdentity()))
      return false;

    if (iMyDb != null)
      makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
        public Object call(ODatabaseDocumentInternal database) {
          if (iCurrent.getInternalStatus() == STATUS.NOT_LOADED)
            iCurrent.reload();
          return null;
        }
      });

    if (iOtherDb != null)
      makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
        public Object call(ODatabaseDocumentInternal database) {
          if (iOther.getInternalStatus() == STATUS.NOT_LOADED)
            iOther.reload();
          return null;
        }
      });

    if (iMyDb != null)
      makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
        public Object call(ODatabaseDocumentInternal database) {
          iCurrent.checkForFields();
          return null;
        }
      });
    else
      iCurrent.checkForFields();

    if (iOtherDb != null)
      makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
        public Object call(ODatabaseDocumentInternal database) {
          iOther.checkForFields();
          return null;
        }
      });
    else
      iOther.checkForFields();

    if (iCurrent.fields() != iOther.fields())
      return false;

    // CHECK FIELD-BY-FIELD
    Object myFieldValue;
    Object otherFieldValue;
    for (Entry<String, Object> f : iCurrent) {
      myFieldValue = f.getValue();
      otherFieldValue = iOther._fields.get(f.getKey()).value;

      if (myFieldValue == otherFieldValue)
        continue;

      // CHECK FOR NULLS
      if (myFieldValue == null) {
        if (otherFieldValue != null)
          return false;
      } else if (otherFieldValue == null)
        return false;

      if (myFieldValue != null)
        if (myFieldValue instanceof Set && otherFieldValue instanceof Set) {
          if (!compareSets(iMyDb, (Set<?>) myFieldValue, iOtherDb, (Set<?>) otherFieldValue, ridMapper))
            return false;
        } else if (myFieldValue instanceof Collection && otherFieldValue instanceof Collection) {
          if (!compareCollections(iMyDb, (Collection<?>) myFieldValue, iOtherDb, (Collection<?>) otherFieldValue, ridMapper))
            return false;
        } else if (myFieldValue instanceof ORidBag && otherFieldValue instanceof ORidBag) {
          if (!compareBags(iMyDb, (ORidBag) myFieldValue, iOtherDb, (ORidBag) otherFieldValue, ridMapper))
            return false;
        } else if (myFieldValue instanceof Map && otherFieldValue instanceof Map) {
          if (!compareMaps(iMyDb, (Map<Object, Object>) myFieldValue, iOtherDb, (Map<Object, Object>) otherFieldValue, ridMapper))
            return false;
        } else if (myFieldValue instanceof ODocument && otherFieldValue instanceof ODocument) {
          if (!hasSameContentOf((ODocument) myFieldValue, iMyDb, (ODocument) otherFieldValue, iOtherDb, ridMapper))
            return false;
        } else {
          if (!compareScalarValues(myFieldValue, iMyDb, otherFieldValue, iOtherDb, ridMapper))
            return false;
        }
    }

    return true;
  }

  public static boolean compareMaps(ODatabaseDocumentInternal iMyDb, Map<Object, Object> myFieldValue,
      ODatabaseDocumentInternal iOtherDb, Map<Object, Object> otherFieldValue, RIDMapper ridMapper) {
    // CHECK IF THE ORDER IS RESPECTED
    final Map<Object, Object> myMap = myFieldValue;
    final Map<Object, Object> otherMap = otherFieldValue;

    if (myMap.size() != otherMap.size())
      return false;

    boolean oldMyAutoConvert = false;
    boolean oldOtherAutoConvert = false;

    if (myMap instanceof ORecordLazyMultiValue) {
      oldMyAutoConvert = ((ORecordLazyMultiValue) myMap).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) myMap).setAutoConvertToRecord(false);
    }

    if (otherMap instanceof ORecordLazyMultiValue) {
      oldOtherAutoConvert = ((ORecordLazyMultiValue) otherMap).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) otherMap).setAutoConvertToRecord(false);
    }

    try {
      final Iterator<Entry<Object, Object>> myEntryIterator = makeDbCall(iMyDb,
          new ODbRelatedCall<Iterator<Entry<Object, Object>>>() {
            public Iterator<Entry<Object, Object>> call(ODatabaseDocumentInternal database) {
              return myMap.entrySet().iterator();
            }
          });

      while (makeDbCall(iMyDb, new ODbRelatedCall<Boolean>() {
        public Boolean call(ODatabaseDocumentInternal database) {
          return myEntryIterator.hasNext();
        }
      })) {
        final Entry<Object, Object> myEntry = makeDbCall(iMyDb, new ODbRelatedCall<Entry<Object, Object>>() {
          public Entry<Object, Object> call(ODatabaseDocumentInternal database) {
            return myEntryIterator.next();
          }
        });
        final Object myKey = makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
          public Object call(ODatabaseDocumentInternal database) {
            return myEntry.getKey();
          }
        });

        if (makeDbCall(iOtherDb, new ODbRelatedCall<Boolean>() {
          public Boolean call(ODatabaseDocumentInternal database) {
            return !otherMap.containsKey(myKey);
          }
        }))
          return false;

        if (myEntry.getValue() instanceof ODocument) {
          if (!hasSameContentOf(makeDbCall(iMyDb, new ODbRelatedCall<ODocument>() {
            public ODocument call(ODatabaseDocumentInternal database) {
              return (ODocument) myEntry.getValue();
            }
          }), iMyDb, makeDbCall(iOtherDb, new ODbRelatedCall<ODocument>() {
            public ODocument call(ODatabaseDocumentInternal database) {
              return (ODocument) otherMap.get(myEntry.getKey());
            }
          }), iOtherDb, ridMapper))
            return false;
        } else {
          final Object myValue = makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
            public Object call(ODatabaseDocumentInternal database) {
              return myEntry.getValue();
            }
          });

          final Object otherValue = makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
            public Object call(ODatabaseDocumentInternal database) {
              return otherMap.get(myEntry.getKey());
            }
          });

          if (!compareScalarValues(myValue, iMyDb, otherValue, iOtherDb, ridMapper))
            return false;
        }
      }
      return true;
    } finally {
      if (myMap instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) myMap).setAutoConvertToRecord(oldMyAutoConvert);

      if (otherMap instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) otherMap).setAutoConvertToRecord(oldOtherAutoConvert);
    }
  }

  public static boolean compareCollections(ODatabaseDocumentInternal iMyDb, Collection<?> myFieldValue,
      ODatabaseDocumentInternal iOtherDb, Collection<?> otherFieldValue, RIDMapper ridMapper) {
    final Collection<?> myCollection = myFieldValue;
    final Collection<?> otherCollection = otherFieldValue;

    if (myCollection.size() != otherCollection.size())
      return false;

    boolean oldMyAutoConvert = false;
    boolean oldOtherAutoConvert = false;

    if (myCollection instanceof ORecordLazyMultiValue) {
      oldMyAutoConvert = ((ORecordLazyMultiValue) myCollection).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) myCollection).setAutoConvertToRecord(false);
    }

    if (otherCollection instanceof ORecordLazyMultiValue) {
      oldOtherAutoConvert = ((ORecordLazyMultiValue) otherCollection).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) otherCollection).setAutoConvertToRecord(false);
    }

    try {
      final Iterator<?> myIterator = makeDbCall(iMyDb, new ODbRelatedCall<Iterator<?>>() {
        public Iterator<?> call(ODatabaseDocumentInternal database) {
          return myCollection.iterator();
        }
      });

      final Iterator<?> otherIterator = makeDbCall(iOtherDb, new ODbRelatedCall<Iterator<?>>() {
        public Iterator<?> call(ODatabaseDocumentInternal database) {
          return otherCollection.iterator();
        }
      });

      while (makeDbCall(iMyDb, new ODbRelatedCall<Boolean>() {
        public Boolean call(ODatabaseDocumentInternal database) {
          return myIterator.hasNext();
        }
      })) {
        final Object myNextVal = makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
          public Object call(ODatabaseDocumentInternal database) {
            return myIterator.next();
          }
        });

        final Object otherNextVal = makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
          public Object call(ODatabaseDocumentInternal database) {
            return otherIterator.next();
          }
        });

        if (!hasSameContentItem(myNextVal, iMyDb, otherNextVal, iOtherDb, ridMapper))
          return false;
      }
      return true;
    } finally {
      if (myCollection instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) myCollection).setAutoConvertToRecord(oldMyAutoConvert);

      if (otherCollection instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) otherCollection).setAutoConvertToRecord(oldOtherAutoConvert);
    }
  }

  public static boolean compareSets(ODatabaseDocumentInternal iMyDb, Set<?> myFieldValue, ODatabaseDocumentInternal iOtherDb,
      Set<?> otherFieldValue, RIDMapper ridMapper) {
    final Set<?> mySet = myFieldValue;
    final Set<?> otherSet = otherFieldValue;

    final int mySize = makeDbCall(iMyDb, new ODbRelatedCall<Integer>() {
      public Integer call(ODatabaseDocumentInternal database) {
        return mySet.size();
      }
    });

    final int otherSize = makeDbCall(iOtherDb, new ODbRelatedCall<Integer>() {
      public Integer call(ODatabaseDocumentInternal database) {
        return otherSet.size();
      }
    });

    if (mySize != otherSize)
      return false;

    boolean oldMyAutoConvert = false;
    boolean oldOtherAutoConvert = false;

    if (mySet instanceof ORecordLazyMultiValue) {
      oldMyAutoConvert = ((ORecordLazyMultiValue) mySet).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) mySet).setAutoConvertToRecord(false);
    }

    if (otherSet instanceof ORecordLazyMultiValue) {
      oldOtherAutoConvert = ((ORecordLazyMultiValue) otherSet).isAutoConvertToRecord();
      ((ORecordLazyMultiValue) otherSet).setAutoConvertToRecord(false);
    }

    try {
      final Iterator<?> myIterator = makeDbCall(iMyDb, new ODbRelatedCall<Iterator<?>>() {
        public Iterator<?> call(ODatabaseDocumentInternal database) {
          return mySet.iterator();
        }
      });

      while (makeDbCall(iMyDb, new ODbRelatedCall<Boolean>() {
        public Boolean call(ODatabaseDocumentInternal database) {
          return myIterator.hasNext();
        }
      })) {

        final Iterator<?> otherIterator = makeDbCall(iOtherDb, new ODbRelatedCall<Iterator<?>>() {
          public Iterator<?> call(ODatabaseDocumentInternal database) {
            return otherSet.iterator();
          }
        });

        final Object myNextVal = makeDbCall(iMyDb, new ODbRelatedCall<Object>() {
          public Object call(ODatabaseDocumentInternal database) {
            return myIterator.next();
          }
        });

        boolean found = false;
        while (!found && makeDbCall(iOtherDb, new ODbRelatedCall<Boolean>() {
          public Boolean call(ODatabaseDocumentInternal database) {
            return otherIterator.hasNext();
          }
        })) {
          final Object otherNextVal = makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
            public Object call(ODatabaseDocumentInternal database) {
              return otherIterator.next();
            }
          });

          found = hasSameContentItem(myNextVal, iMyDb, otherNextVal, iOtherDb, ridMapper);
        }

        if (!found)
          return false;
      }
      return true;
    } finally {
      if (mySet instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) mySet).setAutoConvertToRecord(oldMyAutoConvert);

      if (otherSet instanceof ORecordLazyMultiValue)
        ((ORecordLazyMultiValue) otherSet).setAutoConvertToRecord(oldOtherAutoConvert);
    }
  }

  public static boolean compareBags(ODatabaseDocumentInternal iMyDb, ORidBag myFieldValue, ODatabaseDocumentInternal iOtherDb,
      ORidBag otherFieldValue, RIDMapper ridMapper) {
    final ORidBag myBag = myFieldValue;
    final ORidBag otherBag = otherFieldValue;

    final ODatabaseDocumentInternal currentDb = ODatabaseRecordThreadLocal.instance().getIfDefined();
    try {

      final int mySize = makeDbCall(iMyDb, new ODbRelatedCall<Integer>() {
        public Integer call(ODatabaseDocumentInternal database) {
          return myBag.size();
        }
      });

      final int otherSize = makeDbCall(iOtherDb, new ODbRelatedCall<Integer>() {
        public Integer call(ODatabaseDocumentInternal database) {
          return otherBag.size();
        }
      });

      if (mySize != otherSize)
        return false;

      final boolean oldMyAutoConvert = myBag.isAutoConvertToRecord();
      myBag.setAutoConvertToRecord(false);

      final boolean oldOtherAutoConvert = otherBag.isAutoConvertToRecord();
      otherBag.setAutoConvertToRecord(false);

      final List<ORID> otherBagCopy = makeDbCall(iOtherDb, new ODbRelatedCall<List<ORID>>() {
        @Override
        public List<ORID> call(final ODatabaseDocumentInternal database) {
          final List<ORID> otherRidBag = new LinkedList<ORID>();
          for (OIdentifiable identifiable : otherBag)
            otherRidBag.add(identifiable.getIdentity());

          return otherRidBag;
        }
      });

      try {
        final Iterator<OIdentifiable> myIterator = makeDbCall(iMyDb, new ODbRelatedCall<Iterator<OIdentifiable>>() {
          public Iterator<OIdentifiable> call(final ODatabaseDocumentInternal database) {
            return myBag.iterator();
          }
        });

        while (makeDbCall(iMyDb, new ODbRelatedCall<Boolean>() {
          public Boolean call(final ODatabaseDocumentInternal database) {
            return myIterator.hasNext();
          }
        })) {
          final OIdentifiable myIdentifiable = makeDbCall(iMyDb, new ODbRelatedCall<OIdentifiable>() {
            @Override
            public OIdentifiable call(final ODatabaseDocumentInternal database) {
              return myIterator.next();
            }
          });

          final ORID otherRid;
          if (ridMapper != null) {
            ORID convertedRid = ridMapper.map(myIdentifiable.getIdentity());
            if (convertedRid != null)
              otherRid = convertedRid;
            else
              otherRid = myIdentifiable.getIdentity();
          } else
            otherRid = myIdentifiable.getIdentity();

          makeDbCall(iOtherDb, new ODbRelatedCall<Object>() {
            @Override
            public Object call(final ODatabaseDocumentInternal database) {
              otherBagCopy.remove(otherRid);
              return null;
            }
          });

        }

        return makeDbCall(iOtherDb, new ODbRelatedCall<Boolean>() {
          @Override
          public Boolean call(ODatabaseDocumentInternal database) {
            return otherBagCopy.isEmpty();
          }
        });
      } finally {
        myBag.setAutoConvertToRecord(oldMyAutoConvert);
        otherBag.setAutoConvertToRecord(oldOtherAutoConvert);
      }
    } finally {
      if (currentDb != null)
        currentDb.activateOnCurrentThread();
    }
  }

  private static boolean compareScalarValues(Object myValue, ODatabaseDocumentInternal iMyDb, Object otherValue,
      ODatabaseDocumentInternal iOtherDb, RIDMapper ridMapper) {
    if (myValue == null && otherValue != null || myValue != null && otherValue == null)
      return false;

    if (myValue == null)
      return true;

    if (myValue.getClass().isArray() && !otherValue.getClass().isArray() || !myValue.getClass().isArray() && otherValue.getClass()
        .isArray())
      return false;

    if (myValue.getClass().isArray() && otherValue.getClass().isArray()) {
      final int myArraySize = Array.getLength(myValue);
      final int otherArraySize = Array.getLength(otherValue);

      if (myArraySize != otherArraySize)
        return false;

      for (int i = 0; i < myArraySize; i++) {
        final Object first = Array.get(myValue, i);
        final Object second = Array.get(otherValue, i);
        if (first == null && second != null)
          return false;
        if (first instanceof ODocument && second instanceof ODocument)
          return hasSameContentOf((ODocument) first, iMyDb, (ODocument) second, iOtherDb, ridMapper);

        if (first != null && !first.equals(second))
          return false;
      }

      return true;
    }

    if (myValue instanceof Number && otherValue instanceof Number) {
      final Number myNumberValue = (Number) myValue;
      final Number otherNumberValue = (Number) otherValue;

      if (isInteger(myNumberValue) && isInteger(otherNumberValue))
        return myNumberValue.longValue() == otherNumberValue.longValue();
      else if (isFloat(myNumberValue) && isFloat(otherNumberValue))
        return myNumberValue.doubleValue() == otherNumberValue.doubleValue();
    }

    if (ridMapper != null && myValue instanceof ORID && otherValue instanceof ORID && ((ORID) myValue).isPersistent()) {
      ORID convertedValue = ridMapper.map((ORID) myValue);
      if (convertedValue != null)
        myValue = convertedValue;
    }

    return myValue.equals(otherValue);
  }

  private static boolean isInteger(Number value) {
    return value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long;

  }

  private static boolean isFloat(Number value) {
    return value instanceof Float || value instanceof Double;
  }

  public static void deleteCrossRefs(final ORID iRid, final ODocument iContent) {
    for (String fieldName : iContent.fieldNames()) {
      final Object fieldValue = iContent.field(fieldName);
      if (fieldValue != null) {
        if (fieldValue.equals(iRid)) {
          // REMOVE THE LINK
          iContent.field(fieldName, (ORID) null);
          iContent.save();
        } else if (fieldValue instanceof ODocument && ((ODocument) fieldValue).isEmbedded()) {
          // EMBEDDED DOCUMENT: GO RECURSIVELY
          deleteCrossRefs(iRid, (ODocument) fieldValue);
        } else if (OMultiValue.isMultiValue(fieldValue)) {
          // MULTI-VALUE (COLLECTION, ARRAY OR MAP), CHECK THE CONTENT
          for (final Iterator<?> it = OMultiValue.getMultiValueIterator(fieldValue); it.hasNext(); ) {
            final Object item = it.next();

            if (fieldValue.equals(iRid)) {
              // DELETE ITEM
              it.remove();
            } else if (item instanceof ODocument && ((ODocument) item).isEmbedded()) {
              // EMBEDDED DOCUMENT: GO RECURSIVELY
              deleteCrossRefs(iRid, (ODocument) item);
            }
          }
        }
      }
    }
  }

  public static <T> T makeDbCall(final ODatabaseDocumentInternal databaseRecord, final ODbRelatedCall<T> function) {
    databaseRecord.activateOnCurrentThread();
    return function.call(databaseRecord);
  }

  private static OMultiValueChangeEvent.OChangeType isNestedValueChanged(ONestedMultiValueChangeEvent event, Object value,
      List<Object> ownersTrace, int ownersTraceOffset, Object valueIdentifier) {
    if (event.getTimeLine() != null) {
      List<OMultiValueChangeEvent<Object, Object>> events = event.getTimeLine().getMultiValueChangeEvents();
      if (events != null) {
        for (OMultiValueChangeEvent<Object, Object> nestedEvent : events) {
          if (ownersTraceOffset < ownersTrace.size() && nestedEvent.getKey() == ownersTrace.get(ownersTraceOffset)
              && nestedEvent instanceof ONestedMultiValueChangeEvent) {
            ONestedMultiValueChangeEvent ne = (ONestedMultiValueChangeEvent) nestedEvent;
            OMultiValueChangeEvent.OChangeType ret = isNestedValueChanged(ne, value, ownersTrace, ownersTraceOffset + 1,
                valueIdentifier);
            if (ret != null) {
              return ret;
            }
          } else {
            if (nestedEvent.getKey().equals(valueIdentifier) && nestedEvent.getValue() == value && ownersTraceOffset == ownersTrace
                .size()) {
              return nestedEvent.getChangeType();
            }
          }
        }
      }
    }

    return null;
  }

  public static OMultiValueChangeEvent.OChangeType isNestedValueChanged(ODocumentEntry entry, Object value,
      List<Object> ownersTrace, int ownersTraceOffset, Object valueIdentifier) {
    if (entry.timeLine != null) {
      List<OMultiValueChangeEvent<Object, Object>> timeline = entry.timeLine.getMultiValueChangeEvents();
      if (timeline != null) {
        for (OMultiValueChangeEvent<Object, Object> event : timeline) {
          if (ownersTraceOffset < ownersTrace.size() && event.getKey() == ownersTrace.get(ownersTraceOffset)
              && event instanceof ONestedMultiValueChangeEvent) {
            ONestedMultiValueChangeEvent nestedEvent = (ONestedMultiValueChangeEvent) event;
            OMultiValueChangeEvent.OChangeType ret = isNestedValueChanged(nestedEvent, value, ownersTrace, ownersTraceOffset + 1,
                valueIdentifier);
            if (ret != null) {
              return ret;
            }
          } else if ((event.getKey().equals(valueIdentifier) || event.getKey() == value) && event.getValue() == value
              && ownersTraceOffset == ownersTrace.size()) {
            return event.getChangeType();
          }
        }
      }
    }

    return null;
  }

  private static boolean isInNestedEvent(ONestedMultiValueChangeEvent event, Object checkObject, List<Object> ownersTrace,
      int ownersTraceOffset) {
    if (event.getKey() == checkObject) {
      return true;
    }
    if (event.getTimeLine() != null) {
      List<OMultiValueChangeEvent<Object, Object>> timeline = event.getTimeLine().getMultiValueChangeEvents();
      for (OMultiValueChangeEvent<Object, Object> nestedEvent : timeline) {
        if (nestedEvent.getKey() == checkObject) {
          return true;
        } else {
          if (nestedEvent instanceof ONestedMultiValueChangeEvent && isInNestedEvent((ONestedMultiValueChangeEvent) nestedEvent,
              checkObject, ownersTrace, ownersTraceOffset + 1)) {
            return true;
          }
        }
      }
    }
    return false;
  }

  public static boolean isChangedCollection(Collection list, ODocumentEntry entry, List<Object> ownersTrace,
      int ownersTraceOffset) {
    ownersTrace.add(list);

    //We can't use same logic as in isNestedValueChanged, because
    //change of ODocument contained in list will not fire adequate change event
    for (Object element : list) {
      if (element instanceof ODocument) {
        if (((ODocument) element).isChangedInDepth()) {
          return true;
        }
      } else if (element instanceof Collection) {
        if (isChangedCollection((Collection) element, entry, ownersTrace, ownersTraceOffset + 1)) {
          return true;
        }
      } else if (element instanceof Map) {
        if (isChangedMap((Map) element, entry, ownersTrace, ownersTraceOffset + 1)) {
          return true;
        }
      }
    }

    if (entry.timeLine != null) {
      List<OMultiValueChangeEvent<Object, Object>> timeline = entry.timeLine.getMultiValueChangeEvents();
      if (timeline != null) {
        for (OMultiValueChangeEvent<Object, Object> event : timeline) {
          Object key = event.getKey();
          if (key == list) {
            return true;
          }
          if (event instanceof ONestedMultiValueChangeEvent && isInNestedEvent((ONestedMultiValueChangeEvent) event, list,
              ownersTrace, ownersTraceOffset + 1)) {
            return true;
          }
        }
      }
    }

    return false;
  }

  public static boolean isChangedRidbag(ORidBag ridbag, ODocumentEntry entry) {

    if (entry.timeLine != null) {
      List<OMultiValueChangeEvent<Object, Object>> timeline = entry.timeLine.getMultiValueChangeEvents();
      if (timeline != null) {
        return !timeline.isEmpty();
      }
    }

    return false;
  }

  public static boolean isChangedMap(Map<Object, Object> map, ODocumentEntry entry, List<Object> ownersTrace,
      int ownersTraceOffset) {
    ownersTrace.add(map);

    //We can't use same logic as in isNestedValueChanged, because
    //change of ODocument contained in list will not fire adequate change event
    for (Map.Entry<Object, Object> mapEntry : map.entrySet()) {
      Object element = mapEntry.getValue();
      if (element instanceof ODocument) {
        if (((ODocument) element).isChangedInDepth()) {
          return true;
        }
      } else if (element instanceof Collection) {
        if (isChangedCollection((Collection) element, entry, ownersTrace, ownersTraceOffset + 1)) {
          return true;
        }
      } else if (element instanceof Map) {
        if (isChangedMap((Map) element, entry, ownersTrace, ownersTraceOffset + 1)) {
          return true;
        }
      }
    }

    if (entry.timeLine != null) {
      List<OMultiValueChangeEvent<Object, Object>> timeline = entry.timeLine.getMultiValueChangeEvents();
      if (timeline != null) {
        for (OMultiValueChangeEvent<Object, Object> event : timeline) {
          Object key = event.getKey();
          if (key == map) {
            return true;
          }
          if (event instanceof ONestedMultiValueChangeEvent && isInNestedEvent((ONestedMultiValueChangeEvent) event, map,
              ownersTrace, ownersTraceOffset + 1)) {
            return true;
          }
        }
      }
    }

    return false;
  }

  protected static boolean hasNonExistingInList(List list) {
    for (Object element : list) {
      if (element instanceof ODocument) {
        if (((ODocument) element).hasNonExistingInDepth()) {
          return true;
        }
      } else if (element instanceof List) {
        if (hasNonExistingInList((List) element)) {
          return true;
        }
      }
    }
    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.orient.server;

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.exception.OSystemException;
import com.orientechnologies.orient.client.binary.OBinaryRequestExecutor;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.metadata.security.OToken;
import com.orientechnologies.orient.core.sql.executor.OExecutionPlan;
import com.orientechnologies.orient.core.sql.executor.OInternalExecutionPlan;
import com.orientechnologies.orient.core.sql.executor.OResultSet;
import com.orientechnologies.orient.enterprise.channel.binary.OChannelBinary;
import com.orientechnologies.orient.enterprise.channel.binary.OTokenSecurityException;
import com.orientechnologies.orient.server.config.OServerUserConfiguration;
import com.orientechnologies.orient.server.network.protocol.ONetworkProtocol;
import com.orientechnologies.orient.server.network.protocol.ONetworkProtocolData;
import com.orientechnologies.orient.server.network.protocol.binary.ONetworkProtocolBinary;

import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OClientConnection {
  private final int  id;
  private final long since;
  private Set<ONetworkProtocol> protocols = Collections.newSetFromMap(new WeakHashMap<ONetworkProtocol, Boolean>());
  private volatile ONetworkProtocol          protocol;
  private volatile ODatabaseDocumentInternal database;
  private volatile OServerUserConfiguration  serverUser;
  private ONetworkProtocolData   data  = new ONetworkProtocolData();
  private OClientConnectionStats stats = new OClientConnectionStats();
  private Lock                   lock  = new ReentrantLock();
  private Boolean                tokenBased;
  private byte[]                 tokenBytes;
  private OToken                 token;
  private boolean                disconnectOnAfter;
  private OBinaryRequestExecutor executor;

  public OClientConnection(final int id, final ONetworkProtocol protocol) {
    this.id = id;
    this.protocol = protocol;
    this.protocols.add(protocol);
    this.since = System.currentTimeMillis();
    this.executor = protocol.executor(this);
  }

  public void close() {
    if (getDatabase() != null) {
      if (!getDatabase().isClosed()) {
        getDatabase().activateOnCurrentThread();
        try {
          getDatabase().close();
        } catch (Exception e) {
          // IGNORE IT (ALREADY CLOSED?)
        }
      }

      setDatabase(null);
    }
  }

  /**
   * Acquires the connection. This is fundamental to manage concurrent requests using the same session id.
   */
  public void acquire() {
    lock.lock();
  }

  /**
   * Releases an acquired connection.
   */
  public void release() {
    lock.unlock();
  }

  @Override
  public String toString() {
    return "OClientConnection [id=" + getId() + ", source=" + (
        getProtocol() != null && getProtocol().getChannel() != null && getProtocol().getChannel().socket != null ?
            getProtocol().getChannel().socket.getRemoteSocketAddress() :
            "?") + ", since=" + getSince() + "]";
  }

  /**
   * Returns the remote network address in the format <ip>:<port>.
   */
  public String getRemoteAddress() {
    Socket socket = null;
    if (getProtocol() != null) {
      socket = getProtocol().getChannel().socket;
    } else {
      for (ONetworkProtocol protocol : this.protocols) {
        socket = protocol.getChannel().socket;
        if (socket != null)
          break;
      }
    }

    if (socket != null) {
      final InetSocketAddress remoteAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
      return remoteAddress.getAddress().getHostAddress() + ":" + remoteAddress.getPort();
    }
    return null;
  }

  @Override
  public int hashCode() {
    return getId();
  }

  @Override
  public boolean equals(final Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    final OClientConnection other = (OClientConnection) obj;
    if (getId() != other.getId())
      return false;
    return true;
  }

  public OChannelBinary getChannel() {
    return (OChannelBinary) getProtocol().getChannel();
  }

  public ONetworkProtocol getProtocol() {
    return protocol;
  }

  public byte[] getTokenBytes() {
    return tokenBytes;
  }

  public void validateSession(byte[] tokenFromNetwork, OTokenHandler handler, ONetworkProtocolBinary protocol) {
    if (tokenFromNetwork == null || tokenFromNetwork.length == 0) {
      if (!protocols.contains(protocol))
        throw new OTokenSecurityException("No valid session found, provide a token");
    } else {
      // IF the byte from the network are the same of the one i have a don't check them
      if (tokenBytes != null && tokenBytes.length > 0) {
        if (Arrays.equals(tokenBytes, tokenFromNetwork)) // SAME SESSION AND TOKEN NO NEED CHECK VALIDITY
          return;
      }

      OToken token = null;
      try {
        if (tokenFromNetwork != null)
          token = handler.parseBinaryToken(tokenFromNetwork);
      } catch (Exception e) {
        throw OException.wrapException(new OSystemException("Error on token parse"), e);
      }

      if (token == null || !token.getIsVerified()) {
        cleanSession();
        protocol.getServer().getClientConnectionManager().disconnect(this);
        throw new OTokenSecurityException("The token provided is not a valid token, signature does not match");
      }
      if (!handler.validateBinaryToken(token)) {
        cleanSession();
        protocol.getServer().getClientConnectionManager().disconnect(this);
        throw new OTokenSecurityException("The token provided is expired");
      }
      if (tokenBased == null) {
        tokenBased = Boolean.TRUE;
      }
      if (!Arrays.equals(this.tokenBytes, tokenFromNetwork))
        cleanSession();
      this.tokenBytes = tokenFromNetwork;
      this.token = token;
      protocols.add(protocol);
    }
  }

  public void cleanSession() {
    if (database != null && !database.isClosed()) {
      database.activateOnCurrentThread();
      database.close();
    }
    database = null;
    protocols.clear();

  }

  public void endOperation() {
    if (database != null)
      if (!database.isClosed() && !database.getTransaction().isActive() && database.getLocalCache() != null)
        database.getLocalCache().clear();

    stats.lastCommandExecutionTime = System.currentTimeMillis() - stats.lastCommandReceived;
    stats.totalCommandExecutionTime += stats.lastCommandExecutionTime;

    stats.lastCommandInfo = data.commandInfo;
    stats.lastCommandDetail = data.commandDetail;

    data.commandDetail = "-";
    release();

  }

  public void init(final OServer server) {
    if (database == null) {
      setData(server.getTokenHandler().getProtocolDataFromToken(this, token));

      if (data == null)
        throw new OTokenSecurityException("missing in token data");

      final String db = token.getDatabase();
      final String type = token.getDatabaseType();
      if (db != null && type != null) {
        if (data.serverUser) {
          setDatabase(server.openDatabase(db, token.getUserName(), null, data, true));
        } else
          setDatabase(server.openDatabase(db, token));
      }
    }
  }

  public Boolean getTokenBased() {
    return tokenBased;
  }

  public void setTokenBased(Boolean tokenBased) {
    this.tokenBased = tokenBased;
  }

  public void setTokenBytes(byte[] tokenBytes) {
    this.tokenBytes = tokenBytes;
  }

  public OToken getToken() {
    return token;
  }

  public void setToken(OToken token) {
    this.token = token;
  }

  public int getId() {
    return id;
  }

  public long getSince() {
    return since;
  }

  public void setProtocol(ONetworkProtocol protocol) {
    this.protocol = protocol;
  }

  public ODatabaseDocumentInternal getDatabase() {
    return database;
  }

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

  public OServerUserConfiguration getServerUser() {
    return serverUser;
  }

  public void setServerUser(OServerUserConfiguration serverUser) {
    this.serverUser = serverUser;
  }

  public ONetworkProtocolData getData() {
    return data;
  }

  public void setData(ONetworkProtocolData data) {
    this.data = data;
  }

  public OClientConnectionStats getStats() {
    return stats;
  }

  public void statsUpdate() {

    if (database != null) {
      database.activateOnCurrentThread();
      stats.lastDatabase = database.getName();
      stats.lastUser = database.getUser() != null ? database.getUser().getName() : null;
      stats.activeQueries = getActiveQueries(database);
    } else {
      stats.lastDatabase = null;
      stats.lastUser = null;
    }

    ++stats.totalRequests;
    data.commandInfo = "Listening";
    data.commandDetail = "-";
    stats.lastCommandReceived = System.currentTimeMillis();
  }

  private List<String> getActiveQueries(ODatabaseDocumentInternal database) {
    try {
      List<String> result = new ArrayList<>();
      Map<String, OResultSet> queries = database.getActiveQueries();
      for (OResultSet oResultSet : queries.values()) {
        Optional<OExecutionPlan> plan = oResultSet.getExecutionPlan();
        if (!plan.isPresent()) {
          continue;
        }
        OExecutionPlan p = plan.get();
        if (p instanceof OInternalExecutionPlan) {
          String stm = ((OInternalExecutionPlan) p).getStatement();
          if (stm != null) {
            result.add(stm);
          }
        }
      }
      return result;
    } catch (Exception e) {
    }
    return null;
  }

  public void setDisconnectOnAfter(boolean disconnectOnAfter) {
    this.disconnectOnAfter = disconnectOnAfter;
  }

  public boolean isDisconnectOnAfter() {
    return disconnectOnAfter;
  }

  public OBinaryRequestExecutor getExecutor() {
    return executor;
  }

  public boolean tryAcquireForExpire() {
    try {
      return lock.tryLock(1, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    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.orient.core.iterator;

import java.util.*;

import com.orientechnologies.common.log.OLogManager;
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.OSecurityException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.metadata.security.ORule;
import com.orientechnologies.orient.core.metadata.security.OSecurityUser;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordInternal;
import com.orientechnologies.orient.core.storage.OCluster;
import com.orientechnologies.orient.core.storage.OPhysicalPosition;
import com.orientechnologies.orient.core.storage.OStorage;

/**
 * Iterator class to browse forward and backward the records of a cluster. Once browsed in a direction, the iterator cannot change
 * it.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public abstract class OIdentifiableIterator<REC extends OIdentifiable> implements Iterator<REC>, Iterable<REC> {
  protected final ODatabaseDocumentInternal database;
  protected final ORecordId                 current                = new ORecordId();
  private final   OStorage                  dbStorage;
  protected       boolean                   liveUpdated            = false;
  protected       long                      limit                  = -1;
  protected       long                      browsedRecords         = 0;
  protected       OStorage.LOCKING_STRATEGY lockingStrategy        = OStorage.LOCKING_STRATEGY.NONE;
  protected       long                      totalAvailableRecords;
  protected       List<ORecordOperation>    txEntries;
  protected       int                       currentTxEntryPosition = -1;
  protected       long                      firstClusterEntry      = 0;
  protected       long                      lastClusterEntry       = Long.MAX_VALUE;
  private         String                    fetchPlan;
  private         ORecord                   reusedRecord           = null;                          // DEFAULT = NOT
  // REUSE IT
  private         Boolean                   directionForward;
  private         long                      currentEntry           = ORID.CLUSTER_POS_INVALID;
  private         int                       currentEntryPosition   = -1;
  private         OPhysicalPosition[]       positionsToProcess     = null;

  /**
   * Set of RIDs of records which were indicated as broken during cluster iteration. Mainly used during JSON export/import procedure
   * to fix links on broken records.
   */
  final Set<ORID> brokenRIDs = new HashSet<>();

  public OIdentifiableIterator(final ODatabaseDocumentInternal iDatabase) {
    this(iDatabase, OStorage.LOCKING_STRATEGY.NONE);
  }

  /**
   * @deprecated usage of this constructor may lead to deadlocks.
   */
  @Deprecated
  public OIdentifiableIterator(final ODatabaseDocumentInternal iDatabase, final OStorage.LOCKING_STRATEGY iLockingStrategy) {
    database = iDatabase;
    lockingStrategy = iLockingStrategy;

    dbStorage = database.getStorage();
    current.setClusterPosition(ORID.CLUSTER_POS_INVALID); // DEFAULT = START FROM THE BEGIN
  }

  public abstract boolean hasPrevious();

  public abstract OIdentifiable previous();

  public abstract OIdentifiableIterator<REC> begin();

  public abstract OIdentifiableIterator<REC> last();

  public ORecord current() {
    return readCurrentRecord(getRecord(), 0);
  }

  public String getFetchPlan() {
    return fetchPlan;
  }

  public Set<ORID> getBrokenRIDs() {
    return brokenRIDs;
  }

  public void setFetchPlan(String fetchPlan) {
    this.fetchPlan = fetchPlan;
  }

  public void remove() {
    throw new UnsupportedOperationException("remove");
  }

  /**
   * Tells if the iterator is using the same record for browsing.
   *
   * @see #setReuseSameRecord(boolean)
   */
  public boolean isReuseSameRecord() {
    return reusedRecord != null;
  }

  /**
   * Tell to the iterator to use the same record for browsing. The record will be reset before every use. This improve the
   * performance and reduce memory utilization since it does not create a new one for each operation, but pay attention to copy the
   * data of the record once read otherwise they will be reset to the next operation.
   *
   * @param reuseSameRecord if true the same record will be used for iteration. If false new record will be created each time
   *                        iterator retrieves record from db.
   *
   * @return @see #isReuseSameRecord()
   */
  public OIdentifiableIterator<REC> setReuseSameRecord(final boolean reuseSameRecord) {
    reusedRecord = (ORecord) (reuseSameRecord ? database.newInstance() : null);
    return this;
  }

  public long getCurrentEntry() {
    return currentEntry;
  }

  /**
   * Return the iterator to be used in Java5+ constructs

   *

   *
   * for( ORecordDocument rec : database.browseCluster( "Animal" ) ){
 ...
 }

   *
   */
  public Iterator<REC> iterator() {
    return this;
  }

  /**
   * Return the current limit on browsing record. -1 means no limits (default).
   *
   * @return The limit if setted, otherwise -1
   *
   * @see #setLimit(long)
   */
  public long getLimit() {
    return limit;
  }

  /**
   * Set the limit on browsing record. -1 means no limits. You can set the limit even while you're browsing.
   *
   * @param limit The current limit on browsing record. -1 means no limits (default).
   *
   * @see #getLimit()
   */
  public OIdentifiableIterator<REC> setLimit(final long limit) {
    this.limit = limit;
    return this;
  }

  /**
   * Return current configuration of live updates.
   *
   * @return True to activate it, otherwise false (default)
   *
   * @see #setLiveUpdated(boolean)
   */
  public boolean isLiveUpdated() {
    return liveUpdated;
  }

  /**
   * Tell to the iterator that the upper limit must be checked at every cycle. Useful when concurrent deletes or additions change
   * the size of the cluster while you're browsing it. Default is false.
   *
   * @param liveUpdated True to activate it, otherwise false (default)
   *
   * @see #isLiveUpdated()
   */
  public OIdentifiableIterator<REC> setLiveUpdated(final boolean liveUpdated) {
    this.liveUpdated = liveUpdated;
    return this;
  }

  protected ORecord getTransactionEntry() {
    boolean noPhysicalRecordToBrowse;

    if (current.getClusterPosition() < ORID.CLUSTER_POS_INVALID)
      noPhysicalRecordToBrowse = true;
    else if (directionForward)
      noPhysicalRecordToBrowse = lastClusterEntry <= currentEntry;
    else
      noPhysicalRecordToBrowse = currentEntry <= firstClusterEntry;

    if (!noPhysicalRecordToBrowse && positionsToProcess.length == 0)
      noPhysicalRecordToBrowse = true;

    if (noPhysicalRecordToBrowse && txEntries != null) {
      // IN TX
      currentTxEntryPosition++;
      if (currentTxEntryPosition >= txEntries.size())
        throw new NoSuchElementException();
      else
        return txEntries.get(currentTxEntryPosition).getRecord();
    }
    return null;
  }

  /**
   * Return the record to use for the operation.
   *
   * @return the record to use for the operation.
   */
  protected ORecord getRecord() {
    final ORecord record;
    if (reusedRecord != null) {
      // REUSE THE SAME RECORD AFTER HAVING RESETTED IT
      record = reusedRecord;
      record.reset();
    } else
      record = null;
    return record;
  }

  protected void checkDirection(final boolean iForward) {
    if (directionForward == null)
      // SET THE DIRECTION
      directionForward = iForward;
    else if (directionForward != iForward)
      throw new OIterationException("Iterator cannot change direction while browsing");
  }

  /**
   * Read the current record and increment the counter if the record was found.
   *
   * @param iRecord to read value from database inside it. If record is null link will be created and stored in it.
   *
   * @return record which was read from db.
   */
  protected ORecord readCurrentRecord(ORecord iRecord, final int iMovement) {
    if (limit > -1 && browsedRecords >= limit)
      // LIMIT REACHED
      return null;

    do {
      final boolean moveResult;
      switch (iMovement) {
      case 1:
        moveResult = nextPosition();
        break;
      case -1:
        moveResult = prevPosition();
        break;
      case 0:
        moveResult = checkCurrentPosition();
        break;
      default:
        throw new IllegalStateException("Invalid movement value : " + iMovement);
      }

      if (!moveResult)
        return null;

      try {
        if (iRecord != null) {
          ORecordInternal.setIdentity(iRecord, new ORecordId(current.getClusterId(), current.getClusterPosition()));
          iRecord = database.load(iRecord, fetchPlan, false);
        } else
          iRecord = database.load(current, fetchPlan, false);
      } catch (ODatabaseException e) {
        if (Thread.interrupted() || database.isClosed())
          // THREAD INTERRUPTED: RETURN
          throw e;

        if (e.getCause() instanceof OSecurityException)
          throw e;

        brokenRIDs.add(current.copy());

        OLogManager.instance().error(this, "Error on fetching record during browsing. The record has been skipped", e);
      }

      if (iRecord != null) {
        browsedRecords++;
        return iRecord;
      }
    } while (iMovement != 0);

    return null;
  }

  protected boolean nextPosition() {
    if (positionsToProcess == null) {
      positionsToProcess = dbStorage.ceilingPhysicalPositions(current.getClusterId(), new OPhysicalPosition(firstClusterEntry));
      if (positionsToProcess == null)
        return false;
    } else {
      if (currentEntry >= lastClusterEntry)
        return false;
    }

    incrementEntreePosition();
    while (positionsToProcess.length > 0 && currentEntryPosition >= positionsToProcess.length) {
      positionsToProcess = dbStorage
          .higherPhysicalPositions(current.getClusterId(), positionsToProcess[positionsToProcess.length - 1]);

      currentEntryPosition = -1;
      incrementEntreePosition();
    }

    if (positionsToProcess.length == 0)
      return false;

    currentEntry = positionsToProcess[currentEntryPosition].clusterPosition;

    if (currentEntry > lastClusterEntry || currentEntry == ORID.CLUSTER_POS_INVALID)
      return false;

    current.setClusterPosition(currentEntry);
    return true;
  }

  protected boolean checkCurrentPosition() {
    if (currentEntry == ORID.CLUSTER_POS_INVALID || firstClusterEntry > currentEntry || lastClusterEntry < currentEntry)
      return false;

    current.setClusterPosition(currentEntry);
    return true;
  }

  protected boolean prevPosition() {
    if (positionsToProcess == null) {
      positionsToProcess = dbStorage.floorPhysicalPositions(current.getClusterId(), new OPhysicalPosition(lastClusterEntry));
      if (positionsToProcess == null)
        return false;

      if (positionsToProcess.length == 0)
        return false;

      currentEntryPosition = positionsToProcess.length;
    } else {
      if (currentEntry < firstClusterEntry)
        return false;
    }

    decrementEntreePosition();

    while (positionsToProcess.length > 0 && currentEntryPosition < 0) {
      positionsToProcess = dbStorage.lowerPhysicalPositions(current.getClusterId(), positionsToProcess[0]);
      currentEntryPosition = positionsToProcess.length;

      decrementEntreePosition();
    }

    if (positionsToProcess.length == 0)
      return false;

    currentEntry = positionsToProcess[currentEntryPosition].clusterPosition;

    if (currentEntry < firstClusterEntry)
      return false;

    current.setClusterPosition(currentEntry);
    return true;
  }

  protected void resetCurrentPosition() {
    currentEntry = ORID.CLUSTER_POS_INVALID;
    positionsToProcess = null;
    currentEntryPosition = -1;
  }

  protected long currentPosition() {
    return currentEntry;
  }

  protected void checkForSystemClusters(final ODatabaseDocumentInternal iDatabase, final int[] iClusterIds) {
    for (int clId : iClusterIds) {
      final OCluster cl = iDatabase.getStorage().getClusterById(clId);
      if (cl != null && cl.isSystemCluster()) {
        final OSecurityUser dbUser = iDatabase.getUser();
        if (dbUser == null || dbUser.allow(ORule.ResourceGeneric.SYSTEM_CLUSTERS, null, ORole.PERMISSION_READ) != null)
          // AUTHORIZED
          break;
      }
    }
  }

  private void decrementEntreePosition() {
    if (positionsToProcess.length > 0)
      currentEntryPosition--;

  }

  private void incrementEntreePosition() {
    if (positionsToProcess.length > 0)
      currentEntryPosition++;

  }
}