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

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.listener.OProgressListener;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.util.OArrays;
import com.orientechnologies.common.util.OCommonConst;
import com.orientechnologies.orient.core.command.OCommandResultListener;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.exception.OSchemaException;
import com.orientechnologies.orient.core.exception.OSecurityAccessException;
import com.orientechnologies.orient.core.exception.OSecurityException;
import com.orientechnologies.orient.core.index.OIndex;
import com.orientechnologies.orient.core.index.OIndexDefinition;
import com.orientechnologies.orient.core.index.OIndexDefinitionFactory;
import com.orientechnologies.orient.core.index.OIndexException;
import com.orientechnologies.orient.core.index.OIndexManager;
import com.orientechnologies.orient.core.metadata.schema.clusterselection.OClusterSelectionStrategy;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.metadata.security.ORule;
import com.orientechnologies.orient.core.metadata.security.OSecurityShared;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sharding.auto.OAutoShardingClusterSelectionStrategy;
import com.orientechnologies.orient.core.sql.executor.OResultSet;
import com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery;
import com.orientechnologies.orient.core.storage.OCluster;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 * Schema Class implementation.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
@SuppressWarnings("unchecked")
public abstract class OClassImpl implements OClass {
  private static final   long                      serialVersionUID        = 1L;
  protected static final int                       NOT_EXISTENT_CLUSTER_ID = -1;
  protected final        OSchemaShared             owner;
  protected final        Map<String, OProperty>    properties              = new HashMap<String, OProperty>();
  protected              int                       defaultClusterId        = NOT_EXISTENT_CLUSTER_ID;
  protected              String                    name;
  protected              String                    description;
  protected              int[]                     clusterIds;
  protected              List<OClassImpl>          superClasses            = new ArrayList<OClassImpl>();
  protected              int[]                     polymorphicClusterIds;
  protected              List<OClass>              subclasses;
  protected              float                     overSize                = 0f;
  protected              String                    shortName;
  protected              boolean                   strictMode              = false;                           // @SINCE v1.0rc8
  protected              boolean                   abstractClass           = false;                           // @SINCE v1.2.0
  protected              Map<String, String>       customFields;
  protected volatile     OClusterSelectionStrategy clusterSelection;                                          // @SINCE 1.7
  protected volatile     int                       hashCode;

  private static Set<String> reserved = new HashSet<String>();
  protected      ODocument   document;

  static {
    // reserved.add("select");
    reserved.add("traverse");
    reserved.add("insert");
    reserved.add("update");
    reserved.add("delete");
    reserved.add("from");
    reserved.add("where");
    reserved.add("skip");
    reserved.add("limit");
    reserved.add("timeout");
  }

  /**
   * Constructor used in unmarshalling.
   */
  protected OClassImpl(final OSchemaShared iOwner, final String iName) {
    this(iOwner, new ODocument().setTrackingChanges(false), iName);
  }

  protected OClassImpl(final OSchemaShared iOwner, final String iName, final int[] iClusterIds) {
    this(iOwner, iName);
    setClusterIds(iClusterIds);
    defaultClusterId = iClusterIds[0];
    if (defaultClusterId == NOT_EXISTENT_CLUSTER_ID)
      abstractClass = true;

    if (abstractClass)
      setPolymorphicClusterIds(OCommonConst.EMPTY_INT_ARRAY);
    else
      setPolymorphicClusterIds(iClusterIds);

    clusterSelection = owner.getClusterSelectionFactory().newInstanceOfDefaultClass();
  }

  /**
   * Constructor used in unmarshalling.
   */
  protected OClassImpl(final OSchemaShared iOwner, final ODocument iDocument, final String iName) {
    name = iName;
    document = iDocument;
    owner = iOwner;
  }

  public static int[] readableClusters(final ODatabaseDocument db, final int[] iClusterIds) {
    List<Integer> listOfReadableIds = new ArrayList<Integer>();

    boolean all = true;
    for (int clusterId : iClusterIds) {
      try {
        //////////
        // This will exclude (filter out) any specific classes without explicit read permission.
        // getMetadata().getImmutableSchemaSnapshot()?
        final OClass clazz = db.getMetadata().getSchema().getClassByClusterId(clusterId);

        if (clazz != null)
          db.checkSecurity(ORule.ResourceGeneric.CLASS, ORole.PERMISSION_READ, clazz.getName());
        //////////

        final String clusterName = db.getClusterNameById(clusterId);
        db.checkSecurity(ORule.ResourceGeneric.CLUSTER, ORole.PERMISSION_READ, clusterName);
        listOfReadableIds.add(clusterId);
      } catch (OSecurityAccessException ignore) {
        all = false;
        // if the cluster is inaccessible it's simply not processed in the list.add
      }
    }

    if (all)
      // JUST RETURN INPUT ARRAY (FASTER)
      return iClusterIds;

    final int[] readableClusterIds = new int[listOfReadableIds.size()];
    int index = 0;
    for (int clusterId : listOfReadableIds) {
      readableClusterIds[index++] = clusterId;
    }

    return readableClusterIds;
  }

  @Override
  public OClusterSelectionStrategy getClusterSelection() {
    acquireSchemaReadLock();
    try {
      return clusterSelection;
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public OClass setClusterSelection(final OClusterSelectionStrategy clusterSelection) {
    return setClusterSelection(clusterSelection.getName());
  }

  public String getCustom(final String iName) {
    acquireSchemaReadLock();
    try {
      if (customFields == null)
        return null;

      return customFields.get(iName);
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Map<String, String> getCustomInternal() {
    acquireSchemaReadLock();
    try {
      if (customFields != null)
        return Collections.unmodifiableMap(customFields);
      return null;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public void removeCustom(final String name) {
    setCustom(name, null);
  }

  public Set<String> getCustomKeys() {
    acquireSchemaReadLock();
    try {
      if (customFields != null)
        return Collections.unmodifiableSet(customFields.keySet());
      return new HashSet<String>();
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public boolean hasClusterId(final int clusterId) {
    return Arrays.binarySearch(clusterIds, clusterId) >= 0;
  }

  @Override
  public boolean hasPolymorphicClusterId(final int clusterId) {
    return Arrays.binarySearch(polymorphicClusterIds, clusterId) >= 0;
  }

  @Override
  @Deprecated
  public OClass getSuperClass() {
    acquireSchemaReadLock();
    try {
      return superClasses.isEmpty() ? null : superClasses.get(0);
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  @Deprecated
  public OClass setSuperClass(OClass iSuperClass) {
    setSuperClasses(iSuperClass != null ? Arrays.asList(iSuperClass) : Collections.EMPTY_LIST);
    return this;
  }

  public String getName() {
    acquireSchemaReadLock();
    try {
      return name;
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public List<OClass> getSuperClasses() {
    acquireSchemaReadLock();
    try {
      return Collections.unmodifiableList((List<? extends OClass>) superClasses);
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public boolean hasSuperClasses() {
    acquireSchemaReadLock();
    try {
      return !superClasses.isEmpty();
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public List<String> getSuperClassesNames() {
    acquireSchemaReadLock();
    try {
      List<String> superClassesNames = new ArrayList<String>(superClasses.size());
      for (OClassImpl superClass : superClasses) {
        superClassesNames.add(superClass.getName());
      }
      return superClassesNames;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public OClass setSuperClassesByNames(List<String> classNames) {
    if (classNames == null)
      classNames = Collections.EMPTY_LIST;

    final List<OClass> classes = new ArrayList<OClass>(classNames.size());
    final OSchema schema = getDatabase().getMetadata().getSchema();
    for (String className : classNames) {
      classes.add(schema.getClass(decodeClassName(className)));
    }
    return setSuperClasses(classes);
  }

  protected abstract void setSuperClassesInternal(final List<? extends OClass> classes);

  public long getSize() {
    acquireSchemaReadLock();
    try {
      long size = 0;
      for (int clusterId : clusterIds)
        size += getDatabase().getClusterRecordSizeById(clusterId);

      return size;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public String getShortName() {
    acquireSchemaReadLock();
    try {
      return shortName;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public String getDescription() {
    acquireSchemaReadLock();
    try {
      return description;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public String getStreamableName() {
    acquireSchemaReadLock();
    try {
      return shortName != null ? shortName : name;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Collection<OProperty> declaredProperties() {
    acquireSchemaReadLock();
    try {
      return Collections.unmodifiableCollection(properties.values());
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Map<String, OProperty> propertiesMap() {
    getDatabase().checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_READ);

    acquireSchemaReadLock();
    try {
      final Map<String, OProperty> props = new HashMap<String, OProperty>(20);
      propertiesMap(props);
      return props;
    } finally {
      releaseSchemaReadLock();
    }
  }

  private void propertiesMap(Map<String, OProperty> propertiesMap) {
    for (OProperty p : properties.values()) {
      String propName = p.getName();
      if (!propertiesMap.containsKey(propName))
        propertiesMap.put(propName, p);
    }
    for (OClassImpl superClass : superClasses) {
      superClass.propertiesMap(propertiesMap);
    }
  }

  public Collection<OProperty> properties() {
    getDatabase().checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_READ);

    acquireSchemaReadLock();
    try {
      final Collection<OProperty> props = new ArrayList<OProperty>();
      properties(props);
      return props;
    } finally {
      releaseSchemaReadLock();
    }
  }

  private void properties(Collection<OProperty> properties) {
    properties.addAll(this.properties.values());
    for (OClassImpl superClass : superClasses) {
      superClass.properties(properties);
    }
  }

  public void getIndexedProperties(Collection<OProperty> indexedProperties) {
    for (OProperty p : properties.values())
      if (areIndexed(p.getName()))
        indexedProperties.add(p);
    for (OClassImpl superClass : superClasses) {
      superClass.getIndexedProperties(indexedProperties);
    }
  }

  @Override
  public Collection<OProperty> getIndexedProperties() {
    getDatabase().checkSecurity(ORule.ResourceGeneric.SCHEMA, ORole.PERMISSION_READ);

    acquireSchemaReadLock();
    try {
      Collection<OProperty> indexedProps = new HashSet<OProperty>();
      getIndexedProperties(indexedProps);
      return indexedProps;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public OProperty getProperty(String propertyName) {
    acquireSchemaReadLock();
    try {

      OProperty p = properties.get(propertyName);
      if (p != null)
        return p;
      for (int i = 0; i < superClasses.size() && p == null; i++) {
        p = superClasses.get(i).getProperty(propertyName);
      }
      return p;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public OProperty createProperty(final String iPropertyName, final OType iType) {
    return addProperty(iPropertyName, iType, null, null, false);
  }

  public OProperty createProperty(final String iPropertyName, final OType iType, final OClass iLinkedClass) {
    return addProperty(iPropertyName, iType, null, iLinkedClass, false);
  }

  public OProperty createProperty(final String iPropertyName, final OType iType, final OClass iLinkedClass, final boolean unsafe) {
    return addProperty(iPropertyName, iType, null, iLinkedClass, unsafe);
  }

  public OProperty createProperty(final String iPropertyName, final OType iType, final OType iLinkedType) {
    return addProperty(iPropertyName, iType, iLinkedType, null, false);
  }

  public OProperty createProperty(final String iPropertyName, final OType iType, final OType iLinkedType, final boolean unsafe) {
    return addProperty(iPropertyName, iType, iLinkedType, null, unsafe);
  }

  @Override
  public boolean existsProperty(String propertyName) {
    acquireSchemaReadLock();
    try {
      boolean result = properties.containsKey(propertyName);
      if (result)
        return true;
      for (OClassImpl superClass : superClasses) {
        result = superClass.existsProperty(propertyName);
        if (result)
          return true;
      }
      return false;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public void fromStream() {
    subclasses = null;
    superClasses.clear();

    name = document.field("name");
    if (document.containsField("shortName"))
      shortName = document.field("shortName");
    else
      shortName = null;
    if (document.containsField("description"))
      description = document.field("description");
    else
      description = null;
    defaultClusterId = document.field("defaultClusterId");
    if (document.containsField("strictMode"))
      strictMode = document.field("strictMode");
    else
      strictMode = false;

    if (document.containsField("abstract"))
      abstractClass = document.field("abstract");
    else
      abstractClass = false;

    if (document.field("overSize") != null)
      overSize = document.field("overSize");
    else
      overSize = 0f;

    final Object cc = document.field("clusterIds");
    if (cc instanceof Collection<?>) {
      final Collection<Integer> coll = document.field("clusterIds");
      clusterIds = new int[coll.size()];
      int i = 0;
      for (final Integer item : coll)
        clusterIds[i++] = item;
    } else
      clusterIds = (int[]) cc;
    Arrays.sort(clusterIds);

    if (clusterIds.length == 1 && clusterIds[0] == -1)
      setPolymorphicClusterIds(OCommonConst.EMPTY_INT_ARRAY);
    else
      setPolymorphicClusterIds(clusterIds);

    // READ PROPERTIES
    OPropertyImpl prop;

    final Map<String, OProperty> newProperties = new HashMap<String, OProperty>();
    final Collection<ODocument> storedProperties = document.field("properties");

    if (storedProperties != null)
      for (OIdentifiable id : storedProperties) {
        ODocument p = id.getRecord();
        String name = p.field("name");
        //To lower case ?
        if (properties.containsKey(name)) {
          prop = (OPropertyImpl) properties.get(name);
          prop.fromStream(p);
        } else {
          prop = createPropertyInstance(p);
          prop.fromStream();

        }

        newProperties.put(prop.getName(), prop);
      }

    properties.clear();
    properties.putAll(newProperties);
    customFields = document.field("customFields", OType.EMBEDDEDMAP);
    clusterSelection = owner.getClusterSelectionFactory().getStrategy((String) document.field("clusterSelection"));
  }

  protected abstract OPropertyImpl createPropertyInstance(ODocument p);

  public ODocument toStream() {
    document.setInternalStatus(ORecordElement.STATUS.UNMARSHALLING);

    try {
      document.field("name", name);
      document.field("shortName", shortName);
      document.field("description", description);
      document.field("defaultClusterId", defaultClusterId);
      document.field("clusterIds", clusterIds);
      document.field("clusterSelection", clusterSelection.getName());
      document.field("overSize", overSize);
      document.field("strictMode", strictMode);
      document.field("abstract", abstractClass);

      final Set<ODocument> props = new LinkedHashSet<ODocument>();
      for (final OProperty p : properties.values()) {
        props.add(((OPropertyImpl) p).toStream());
      }
      document.field("properties", props, OType.EMBEDDEDSET);

      if (superClasses.isEmpty()) {
        // Single super class is deprecated!
        document.field("superClass", null, OType.STRING);
        document.field("superClasses", null, OType.EMBEDDEDLIST);
      } else {
        // Single super class is deprecated!
        document.field("superClass", superClasses.get(0).getName(), OType.STRING);
        List<String> superClassesNames = new ArrayList<String>();
        for (OClassImpl superClass : superClasses) {
          superClassesNames.add(superClass.getName());
        }
        document.field("superClasses", superClassesNames, OType.EMBEDDEDLIST);
      }

      document.field("customFields", customFields != null && customFields.size() > 0 ? customFields : null, OType.EMBEDDEDMAP);

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

    return document;
  }

  @Override
  public int getClusterForNewInstance(final ODocument doc) {
    acquireSchemaReadLock();
    try {
      return clusterSelection.getCluster(this, doc);
    } finally {
      releaseSchemaReadLock();
    }
  }

  public int getDefaultClusterId() {
    acquireSchemaReadLock();
    try {
      return defaultClusterId;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public int[] getClusterIds() {
    acquireSchemaReadLock();
    try {
      return clusterIds;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public int[] getPolymorphicClusterIds() {
    acquireSchemaReadLock();
    try {
      return Arrays.copyOf(polymorphicClusterIds, polymorphicClusterIds.length);
    } finally {
      releaseSchemaReadLock();
    }
  }

  private void setPolymorphicClusterIds(final int[] iClusterIds) {
    Set<Integer> set = new TreeSet<Integer>();
    for (int iClusterId : iClusterIds) {
      set.add(iClusterId);
    }
    polymorphicClusterIds = new int[set.size()];
    int i = 0;
    for (Integer clusterId : set) {
      polymorphicClusterIds[i] = clusterId;
      i++;
    }
  }

  public void renameProperty(final String iOldName, final String iNewName) {
    final OProperty p = properties.remove(iOldName);
    if (p != null)
      properties.put(iNewName, p);
  }

  public static OClass addClusters(final OClass cls, final int iClusters) {
    final String clusterBase = cls.getName().toLowerCase(Locale.ENGLISH) + "_";
    for (int i = 0; i < iClusters; ++i) {
      cls.addCluster(clusterBase + i);
    }
    return cls;
  }

  protected void truncateClusterInternal(final String clusterName, final ODatabaseDocumentInternal database) {
    final OCluster cluster = database.getStorage().getClusterByName(clusterName);

    if (cluster == null) {
      throw new ODatabaseException("Cluster with name " + clusterName + " does not exist");
    }

    try {
      database.checkForClusterPermissions(clusterName);
      cluster.truncate();
    } catch (IOException e) {
      throw OException.wrapException(new ODatabaseException("Error during truncate of cluster " + clusterName), e);
    }

    for (OIndex index : getIndexes()) {
      index.rebuild();
    }
  }

  public Collection<OClass> getSubclasses() {
    acquireSchemaReadLock();
    try {
      if (subclasses == null || subclasses.size() == 0)
        return Collections.emptyList();

      return Collections.unmodifiableCollection(subclasses);
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Collection<OClass> getAllSubclasses() {
    acquireSchemaReadLock();
    try {
      final Set<OClass> set = new HashSet<OClass>();
      if (subclasses != null) {
        set.addAll(subclasses);

        for (OClass c : subclasses)
          set.addAll(c.getAllSubclasses());
      }
      return set;
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Deprecated
  public Collection<OClass> getBaseClasses() {
    return getSubclasses();
  }

  @Deprecated
  public Collection<OClass> getAllBaseClasses() {
    return getAllSubclasses();
  }

  @Override
  public Collection<OClass> getAllSuperClasses() {
    Set<OClass> ret = new HashSet<OClass>();
    getAllSuperClasses(ret);
    return ret;
  }

  private void getAllSuperClasses(Set<OClass> set) {
    set.addAll(superClasses);
    for (OClassImpl superClass : superClasses) {
      superClass.getAllSuperClasses(set);
    }
  }

  protected abstract OClass removeBaseClassInternal(final OClass baseClass);

  public float getOverSize() {
    acquireSchemaReadLock();
    try {
      if (overSize > 0)
        // CUSTOM OVERSIZE SET
        return overSize;

      // NO OVERSIZE by default
      float maxOverSize = 0;
      float thisOverSize;
      for (OClassImpl superClass : superClasses) {
        thisOverSize = superClass.getOverSize();
        if (thisOverSize > maxOverSize)
          maxOverSize = thisOverSize;
      }
      return maxOverSize;
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public float getClassOverSize() {
    acquireSchemaReadLock();
    try {
      return overSize;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public boolean isAbstract() {
    acquireSchemaReadLock();
    try {
      return abstractClass;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public boolean isStrictMode() {
    acquireSchemaReadLock();
    try {
      return strictMode;
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public String toString() {
    acquireSchemaReadLock();
    try {
      return name;
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public boolean equals(Object obj) {
    acquireSchemaReadLock();
    try {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!OClass.class.isAssignableFrom(obj.getClass()))
        return false;
      final OClass other = (OClass) obj;
      if (name == null) {
        if (other.getName() != null)
          return false;
      } else if (!name.equals(other.getName()))
        return false;

      return true;
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public int hashCode() {
    int sh = hashCode;
    if (sh != 0)
      return sh;

    acquireSchemaReadLock();
    try {
      sh = hashCode;
      if (sh != 0)
        return sh;

      calculateHashCode();
      return hashCode;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public int compareTo(final OClass o) {
    acquireSchemaReadLock();
    try {
      return name.compareTo(o.getName());
    } finally {
      releaseSchemaReadLock();
    }
  }

  public long count() {
    return count(true);
  }

  public long count(final boolean isPolymorphic) {
    acquireSchemaReadLock();
    try {
      return getDatabase().countClass(getName(), isPolymorphic);
    } finally {
      releaseSchemaReadLock();
    }
  }

  /**
   * Truncates all the clusters the class uses.
   */
  public void truncate() throws IOException {
    ODatabaseDocumentInternal db = getDatabase();
    db.checkSecurity(ORule.ResourceGeneric.CLASS, ORole.PERMISSION_UPDATE);

    if (isSubClassOf(OSecurityShared.RESTRICTED_CLASSNAME)) {
      throw new OSecurityException(
          "Class '" + getName() + "' cannot be truncated because has record level security enabled (extends '"
              + OSecurityShared.RESTRICTED_CLASSNAME + "')");
    }

    final OStorage storage = db.getStorage();
    acquireSchemaReadLock();
    try {

      for (int id : clusterIds) {
        OCluster cl = storage.getClusterById(id);
        db.checkForClusterPermissions(cl.getName());
        cl.truncate();
      }
      for (OIndex<?> index : getClassIndexes())
        index.clear();

      Set<OIndex<?>> superclassIndexes = new HashSet<OIndex<?>>();
      superclassIndexes.addAll(getIndexes());
      superclassIndexes.removeAll(getClassIndexes());
      for (OIndex index : superclassIndexes) {
        index.rebuild();
      }
    } finally {
      releaseSchemaReadLock();
    }
  }

  /**
   * Check if the current instance extends specified schema class.
   *
   * @param iClassName of class that should be checked
   *
   * @return Returns true if the current instance extends the passed schema class (iClass)
   *
   * @see #isSuperClassOf(OClass)
   */
  public boolean isSubClassOf(final String iClassName) {
    acquireSchemaReadLock();
    try {
      if (iClassName == null)
        return false;

      if (iClassName.equalsIgnoreCase(getName()) || iClassName.equalsIgnoreCase(getShortName()))
        return true;
      for (OClassImpl superClass : superClasses) {
        if (superClass.isSubClassOf(iClassName))
          return true;
      }
      return false;
    } finally {
      releaseSchemaReadLock();
    }
  }

  /**
   * Check if the current instance extends specified schema class.
   *
   * @param clazz to check
   *
   * @return true if the current instance extends the passed schema class (iClass)
   *
   * @see #isSuperClassOf(OClass)
   */
  public boolean isSubClassOf(final OClass clazz) {
    acquireSchemaReadLock();
    try {
      if (clazz == null)
        return false;
      if (equals(clazz))
        return true;
      for (OClassImpl superClass : superClasses) {
        if (superClass.isSubClassOf(clazz))
          return true;
      }
      return false;
    } finally {
      releaseSchemaReadLock();
    }
  }

  /**
   * Returns true if the passed schema class (iClass) extends the current instance.
   *
   * @param clazz to check
   *
   * @return Returns true if the passed schema class extends the current instance
   *
   * @see #isSubClassOf(OClass)
   */
  public boolean isSuperClassOf(final OClass clazz) {
    return clazz != null && clazz.isSubClassOf(this);
  }

  public Object get(final ATTRIBUTES iAttribute) {
    if (iAttribute == null)
      throw new IllegalArgumentException("attribute is null");

    switch (iAttribute) {
    case NAME:
      return getName();
    case SHORTNAME:
      return getShortName();
    case SUPERCLASS:
      return getSuperClass();
    case SUPERCLASSES:
      return getSuperClasses();
    case OVERSIZE:
      return getOverSize();
    case STRICTMODE:
      return isStrictMode();
    case ABSTRACT:
      return isAbstract();
    case CLUSTERSELECTION:
      return getClusterSelection();
    case CUSTOM:
      return getCustomInternal();
    case DESCRIPTION:
      return getDescription();
    }

    throw new IllegalArgumentException("Cannot find attribute '" + iAttribute + "'");
  }

  public OClass set(final ATTRIBUTES attribute, final Object iValue) {
    if (attribute == null)
      throw new IllegalArgumentException("attribute is null");

    final String stringValue = iValue != null ? iValue.toString() : null;
    final boolean isNull = stringValue == null || stringValue.equalsIgnoreCase("NULL");

    switch (attribute) {
    case NAME:
      setName(decodeClassName(stringValue));
      break;
    case SHORTNAME:
      setShortName(decodeClassName(stringValue));
      break;
    case SUPERCLASS:
      if (stringValue == null)
        throw new IllegalArgumentException("Superclass is null");

      if (stringValue.startsWith("+")) {
        addSuperClass(getDatabase().getMetadata().getSchema().getClass(decodeClassName(stringValue.substring(1))));
      } else if (stringValue.startsWith("-")) {
        removeSuperClass(getDatabase().getMetadata().getSchema().getClass(decodeClassName(stringValue.substring(1))));
      } else {
        setSuperClass(getDatabase().getMetadata().getSchema().getClass(decodeClassName(stringValue)));
      }
      break;
    case SUPERCLASSES:
      setSuperClassesByNames(stringValue != null ? Arrays.asList(stringValue.split(",\\s*")) : null);
      break;
    case OVERSIZE:
      setOverSize(Float.parseFloat(stringValue));
      break;
    case STRICTMODE:
      setStrictMode(Boolean.parseBoolean(stringValue));
      break;
    case ABSTRACT:
      setAbstract(Boolean.parseBoolean(stringValue));
      break;
    case ADDCLUSTER: {
      addCluster(stringValue);
      break;
    }
    case REMOVECLUSTER:
      int clId = owner.getClusterId(getDatabase(), stringValue);
      if (clId == NOT_EXISTENT_CLUSTER_ID)
        throw new IllegalArgumentException("Cluster id '" + stringValue + "' cannot be removed");
      removeClusterId(clId);
      break;
    case CLUSTERSELECTION:
      setClusterSelection(stringValue);
      break;
    case CUSTOM:
      int indx = stringValue != null ? stringValue.indexOf('=') : -1;
      if (indx < 0) {
        if (isNull || "clear".equalsIgnoreCase(stringValue)) {
          clearCustom();
        } else
          throw new IllegalArgumentException("Syntax error: expected <name> = <value> or clear, instead found: " + iValue);
      } else {
        String customName = stringValue.substring(0, indx).trim();
        String customValue = stringValue.substring(indx + 1).trim();
        if (isQuoted(customValue)) {
          customValue = removeQuotes(customValue);
        }
        if (customValue.isEmpty())
          removeCustom(customName);
        else
          setCustom(customName, customValue);
      }
      break;
    case DESCRIPTION:
      setDescription(stringValue);
      break;
    case ENCRYPTION:
      setEncryption(stringValue);
      break;
    }
    return this;
  }

  private String removeQuotes(String s) {
    s = s.trim();
    return s.substring(1, s.length() - 1);
  }

  private boolean isQuoted(String s) {
    s = s.trim();
    if (s.startsWith("\"") && s.endsWith("\""))
      return true;
    if (s.startsWith("'") && s.endsWith("'"))
      return true;
    if (s.startsWith("`") && s.endsWith("`"))
      return true;

    return false;
  }

  public abstract OClassImpl setEncryption(final String iValue);

  protected void setEncryptionInternal(ODatabaseDocumentInternal database, final String iValue) {
    for (int cl : getClusterIds()) {
      final OCluster c = database.getStorage().getClusterById(cl);
      if (c != null)
        try {
          c.set(OCluster.ATTRIBUTES.ENCRYPTION, iValue);
        } catch (IOException e) {
          OLogManager.instance().error(this, "Can not set value of encryption parameter to '%s'", e, iValue);
        }
    }
  }

  public OIndex<?> createIndex(final String iName, final INDEX_TYPE iType, final String... fields) {
    return createIndex(iName, iType.name(), fields);
  }

  public OIndex<?> createIndex(final String iName, final String iType, final String... fields) {
    return createIndex(iName, iType, null, null, fields);
  }

  public OIndex<?> createIndex(final String iName, final INDEX_TYPE iType, final OProgressListener iProgressListener,
      final String... fields) {
    return createIndex(iName, iType.name(), iProgressListener, null, fields);
  }

  public OIndex<?> createIndex(String iName, String iType, OProgressListener iProgressListener, ODocument metadata,
      String... fields) {
    return createIndex(iName, iType, iProgressListener, metadata, null, fields);
  }

  public OIndex<?> createIndex(final String name, String type, final OProgressListener progressListener, ODocument metadata,
      String algorithm, final String... fields) {
    if (type == null)
      throw new IllegalArgumentException("Index type is null");

    type = type.toUpperCase(Locale.ENGLISH);

    if (fields.length == 0) {
      throw new OIndexException("List of fields to index cannot be empty.");
    }

    final String localName = this.name;
    final int[] localPolymorphicClusterIds = polymorphicClusterIds;

    for (final String fieldToIndex : fields) {
      final String fieldName = decodeClassName(OIndexDefinitionFactory.extractFieldName(fieldToIndex));

      if (!fieldName.equals("@rid") && !existsProperty(fieldName))
        throw new OIndexException(
            "Index with name '" + name + "' cannot be created on class '" + localName + "' because the field '" + fieldName
                + "' is absent in class definition");
    }

    final OIndexDefinition indexDefinition = OIndexDefinitionFactory
        .createIndexDefinition(this, Arrays.asList(fields), extractFieldTypes(fields), null, type, algorithm);

    return getDatabase().getMetadata().getIndexManager()
        .createIndex(name, type, indexDefinition, localPolymorphicClusterIds, progressListener, metadata, algorithm);
  }

  public boolean areIndexed(final String... fields) {
    return areIndexed(Arrays.asList(fields));
  }

  public boolean areIndexed(final Collection<String> fields) {
    final OIndexManager indexManager = getDatabase().getMetadata().getIndexManager();

    acquireSchemaReadLock();
    try {
      final boolean currentClassResult = indexManager.areIndexed(name, fields);

      if (currentClassResult)
        return true;
      for (OClassImpl superClass : superClasses) {
        if (superClass.areIndexed(fields))
          return true;
      }
      return false;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Set<OIndex<?>> getInvolvedIndexes(final String... fields) {
    return getInvolvedIndexes(Arrays.asList(fields));
  }

  public Set<OIndex<?>> getInvolvedIndexes(final Collection<String> fields) {
    acquireSchemaReadLock();
    try {
      final Set<OIndex<?>> result = new HashSet<OIndex<?>>(getClassInvolvedIndexes(fields));

      for (OClassImpl superClass : superClasses) {
        result.addAll(superClass.getInvolvedIndexes(fields));
      }

      return result;
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Set<OIndex<?>> getClassInvolvedIndexes(final Collection<String> fields) {

    final OIndexManager indexManager = getDatabase().getMetadata().getIndexManager();

    acquireSchemaReadLock();
    try {
      return indexManager.getClassInvolvedIndexes(name, fields);
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Set<OIndex<?>> getClassInvolvedIndexes(final String... fields) {
    return getClassInvolvedIndexes(Arrays.asList(fields));
  }

  public OIndex<?> getClassIndex(final String name) {
    acquireSchemaReadLock();
    try {
      return getDatabase().getMetadata().getIndexManager().getClassIndex(this.name, name);
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Set<OIndex<?>> getClassIndexes() {
    acquireSchemaReadLock();
    try {
      final OIndexManager idxManager = getDatabase().getMetadata().getIndexManager();
      if (idxManager == null)
        return new HashSet<OIndex<?>>();

      return idxManager.getClassIndexes(name);
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public void getClassIndexes(final Collection<OIndex<?>> indexes) {
    acquireSchemaReadLock();
    try {
      final OIndexManager idxManager = getDatabase().getMetadata().getIndexManager();
      if (idxManager == null)
        return;

      idxManager.getClassIndexes(name, indexes);
    } finally {
      releaseSchemaReadLock();
    }
  }

  @Override
  public OIndex<?> getAutoShardingIndex() {
    final ODatabaseDocumentInternal db = getDatabase();
    return db != null ? db.getMetadata().getIndexManager().getClassAutoShardingIndex(name) : null;
  }

  @Override
  public boolean isEdgeType() {
    return isSubClassOf(EDGE_CLASS_NAME);
  }

  @Override
  public boolean isVertexType() {
    return isSubClassOf(VERTEX_CLASS_NAME);
  }

  public void onPostIndexManagement() {
    final OIndex<?> autoShardingIndex = getAutoShardingIndex();
    if (autoShardingIndex != null) {
      if (!getDatabase().getStorage().isRemote()) {
        // OVERRIDE CLUSTER SELECTION
        acquireSchemaWriteLock();
        try {
          this.clusterSelection = new OAutoShardingClusterSelectionStrategy(this, autoShardingIndex);
        } finally {
          releaseSchemaWriteLock();
        }
      }
    } else if (clusterSelection instanceof OAutoShardingClusterSelectionStrategy) {
      // REMOVE AUTO SHARDING CLUSTER SELECTION
      acquireSchemaWriteLock();
      try {
        this.clusterSelection = owner.getClusterSelectionFactory().newInstanceOfDefaultClass();
      } finally {
        releaseSchemaWriteLock();
      }
    }
  }

  @Override
  public void getIndexes(final Collection<OIndex<?>> indexes) {
    acquireSchemaReadLock();
    try {
      getClassIndexes(indexes);
      for (OClass superClass : superClasses) {
        superClass.getIndexes(indexes);
      }
    } finally {
      releaseSchemaReadLock();
    }
  }

  public Set<OIndex<?>> getIndexes() {
    final Set<OIndex<?>> indexes = new HashSet<OIndex<?>>();
    getIndexes(indexes);
    return indexes;
  }

  public void acquireSchemaReadLock() {
    owner.acquireSchemaReadLock();
  }

  public void releaseSchemaReadLock() {
    owner.releaseSchemaReadLock();
  }

  public void acquireSchemaWriteLock() {
    owner.acquireSchemaWriteLock(getDatabase());
  }

  public void releaseSchemaWriteLock() {
    releaseSchemaWriteLock(true);
  }

  public void releaseSchemaWriteLock(final boolean iSave) {
    calculateHashCode();
    owner.releaseSchemaWriteLock(getDatabase(), iSave);
  }

  public void checkEmbedded() {
    owner.checkEmbedded();
  }

  public void setClusterSelectionInternal(final OClusterSelectionStrategy iClusterSelection) {
    // AVOID TO CHECK THIS IN LOCK TO AVOID RE-GENERATION OF IMMUTABLE SCHEMAS
    if (this.clusterSelection.getClass().equals(iClusterSelection.getClass()))
      // NO CHANGES
      return;

    // DON'T GET THE SCHEMA LOCK BECAUSE THIS CHANGE IS USED ONLY TO WRAP THE SELECTION STRATEGY
    checkEmbedded();
    this.clusterSelection = iClusterSelection;

  }

  public void fireDatabaseMigration(final ODatabaseDocument database, final String propertyName, final OType type) {
    final boolean strictSQL = ((ODatabaseInternal) database).getStorage().getConfiguration().isStrictSql();

    database.query(new OSQLAsynchQuery<Object>(
        "select from " + getEscapedName(name, strictSQL) + " where " + getEscapedName(propertyName, strictSQL) + ".type() <> \""
            + type.name() + "\"", new OCommandResultListener() {

      @Override
      public boolean result(Object iRecord) {
        final ODocument record = ((OIdentifiable) iRecord).getRecord();
        record.field(propertyName, record.field(propertyName), type);
        database.save(record);
        return true;
      }

      @Override
      public void end() {
      }

      @Override
      public Object getResult() {
        return null;
      }
    }));
  }

  public void firePropertyNameMigration(final ODatabaseDocument database, final String propertyName, final String newPropertyName,
      final OType type) {
    final boolean strictSQL = ((ODatabaseInternal) database).getStorage().getConfiguration().isStrictSql();

    database.query(new OSQLAsynchQuery<Object>(
        "select from " + getEscapedName(name, strictSQL) + " where " + getEscapedName(propertyName, strictSQL) + " is not null ",
        new OCommandResultListener() {

          @Override
          public boolean result(Object iRecord) {
            final ODocument record = ((OIdentifiable) iRecord).getRecord();
            record.setFieldType(propertyName, type);
            record.field(newPropertyName, record.field(propertyName), type);
            database.save(record);
            return true;
          }

          @Override
          public void end() {
          }

          @Override
          public Object getResult() {
            return null;
          }
        }));

  }

  public void checkPersistentPropertyType(final ODatabaseInternal<ORecord> database, final String propertyName, final OType type) {
    if (OType.ANY.equals(type)) {
      return;
    }
    final boolean strictSQL = database.getStorage().getConfiguration().isStrictSql();

    final StringBuilder builder = new StringBuilder(256);
    builder.append("select count(*) as count from ");
    builder.append(getEscapedName(name, strictSQL));
    builder.append(" where ");
    builder.append(getEscapedName(propertyName, strictSQL));
    builder.append(".type() not in [");

    final Iterator<OType> cur = type.getCastable().iterator();
    while (cur.hasNext()) {
      builder.append('"').append(cur.next().name()).append('"');
      if (cur.hasNext())
        builder.append(",");
    }
    builder.append("] and ").append(getEscapedName(propertyName, strictSQL)).append(" is not null ");
    if (type.isMultiValue())
      builder.append(" and ").append(getEscapedName(propertyName, strictSQL)).append(".size() <> 0 limit 1");

    try (final OResultSet res = database.command(builder.toString())) {
      if (((Long) res.next().getProperty("count")) > 0)
        throw new OSchemaException("The database contains some schema-less data in the property '" + name + "." + propertyName
            + "' that is not compatible with the type " + type + ". Fix those records and change the schema again");
    }
  }

  protected String getEscapedName(final String iName, final boolean iStrictSQL) {
    if (iStrictSQL)
      // ESCAPE NAME
      return "`" + iName + "`";
    return iName;
  }

  public OSchemaShared getOwner() {
    return owner;
  }

  private void calculateHashCode() {
    int result = super.hashCode();
    result = 31 * result + (name != null ? name.hashCode() : 0);
    hashCode = result;
  }

  protected void renameCluster(String oldName, String newName) {
    oldName = oldName.toLowerCase(Locale.ENGLISH);
    newName = newName.toLowerCase(Locale.ENGLISH);

    final ODatabaseDocumentInternal database = getDatabase();
    final OStorage storage = database.getStorage();

    if (storage.getClusterIdByName(newName) != -1)
      return;

    final int clusterId = storage.getClusterIdByName(oldName);
    if (clusterId == -1)
      return;

    if (!hasClusterId(clusterId))
      return;

    database.command("alter cluster `" + oldName + "` NAME \"" + newName + "\"").close();
  }

  protected void addPolymorphicClusterId(int clusterId) {
    if (Arrays.binarySearch(polymorphicClusterIds, clusterId) >= 0)
      return;

    polymorphicClusterIds = OArrays.copyOf(polymorphicClusterIds, polymorphicClusterIds.length + 1);
    polymorphicClusterIds[polymorphicClusterIds.length - 1] = clusterId;
    Arrays.sort(polymorphicClusterIds);

    addClusterIdToIndexes(clusterId);

    for (OClassImpl superClass : superClasses) {
      superClass.addPolymorphicClusterId(clusterId);
    }
  }

  protected abstract OProperty addProperty(final String propertyName, final OType type, final OType linkedType,
      final OClass linkedClass, final boolean unsafe);

  protected void validatePropertyName(final String propertyName) {
  }

  private int getClusterId(final String stringValue) {
    int clId;
    if (!stringValue.isEmpty() && Character.isDigit(stringValue.charAt(0)))
      try {
        clId = Integer.parseInt(stringValue);
      } catch (NumberFormatException ignore) {
        clId = getDatabase().getClusterIdByName(stringValue);
      }
    else
      clId = getDatabase().getClusterIdByName(stringValue);

    return clId;
  }

  private void addClusterIdToIndexes(int iId) {
    ODatabaseDocumentInternal database = ODatabaseRecordThreadLocal.instance().getIfDefined();
    if (database != null && database.getStorage().getUnderlying() instanceof OAbstractPaginatedStorage) {
      final String clusterName = getDatabase().getClusterNameById(iId);
      final List<String> indexesToAdd = new ArrayList<String>();

      for (OIndex<?> index : getIndexes())
        indexesToAdd.add(index.getName());

      final OIndexManager indexManager = getDatabase().getMetadata().getIndexManager();
      for (String indexName : indexesToAdd)
        indexManager.addClusterToIndex(clusterName, indexName);
    }
  }

  /**
   * Adds a base class to the current one. It adds also the base class cluster ids to the polymorphic cluster ids array.
   *
   * @param iBaseClass The base class to add.
   */
  protected OClass addBaseClass(final OClassImpl iBaseClass) {
    checkRecursion(iBaseClass);

    if (subclasses == null)
      subclasses = new ArrayList<OClass>();

    if (subclasses.contains(iBaseClass))
      return this;

    subclasses.add(iBaseClass);
    addPolymorphicClusterIdsWithInheritance(iBaseClass);
    return this;
  }

  protected void checkParametersConflict(final OClass baseClass) {
    final Collection<OProperty> baseClassProperties = baseClass.properties();
    for (OProperty property : baseClassProperties) {
      OProperty thisProperty = getProperty(property.getName());
      if (thisProperty != null && !thisProperty.getType().equals(property.getType())) {
        throw new OSchemaException(
            "Cannot add base class '" + baseClass.getName() + "', because of property conflict: '" + thisProperty + "' vs '"
                + property + "'");
      }
    }
  }

  public static void checkParametersConflict(List<OClass> classes) {
    final Map<String, OProperty> comulative = new HashMap<String, OProperty>();
    final Map<String, OProperty> properties = new HashMap<String, OProperty>();

    for (OClass superClass : classes) {
      if (superClass == null)
        continue;
      OClassImpl impl;

      if (superClass instanceof OClassAbstractDelegate)
        impl = (OClassImpl) ((OClassAbstractDelegate) superClass).delegate;
      else
        impl = (OClassImpl) superClass;
      impl.propertiesMap(properties);
      for (Map.Entry<String, OProperty> entry : properties.entrySet()) {
        if (comulative.containsKey(entry.getKey())) {
          final String property = entry.getKey();
          final OProperty existingProperty = comulative.get(property);
          if (!existingProperty.getType().equals(entry.getValue().getType())) {
            throw new OSchemaException("Properties conflict detected: '" + existingProperty + "] vs [" + entry.getValue() + "]");
          }
        }
      }

      comulative.putAll(properties);
      properties.clear();
    }
  }

  private void checkRecursion(final OClass baseClass) {
    if (isSubClassOf(baseClass)) {
      throw new OSchemaException("Cannot add base class '" + baseClass.getName() + "', because of recursion");
    }
  }

  protected void removePolymorphicClusterIds(final OClassImpl iBaseClass) {
    for (final int clusterId : iBaseClass.polymorphicClusterIds)
      removePolymorphicClusterId(clusterId);
  }

  protected void removePolymorphicClusterId(final int clusterId) {
    final int index = Arrays.binarySearch(polymorphicClusterIds, clusterId);
    if (index < 0)
      return;

    if (index < polymorphicClusterIds.length - 1)
      System.arraycopy(polymorphicClusterIds, index + 1, polymorphicClusterIds, index, polymorphicClusterIds.length - (index + 1));

    polymorphicClusterIds = Arrays.copyOf(polymorphicClusterIds, polymorphicClusterIds.length - 1);

    removeClusterFromIndexes(clusterId);
    for (OClassImpl superClass : superClasses) {
      superClass.removePolymorphicClusterId(clusterId);
    }
  }

  private void removeClusterFromIndexes(final int iId) {
    if (getDatabase().getStorage().getUnderlying() instanceof OAbstractPaginatedStorage) {
      final String clusterName = getDatabase().getClusterNameById(iId);
      final List<String> indexesToRemove = new ArrayList<String>();

      for (final OIndex<?> index : getIndexes())
        indexesToRemove.add(index.getName());

      final OIndexManager indexManager = getDatabase().getMetadata().getIndexManager();
      for (final String indexName : indexesToRemove)
        indexManager.removeClusterFromIndex(clusterName, indexName);
    }
  }

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

  /**
   * Add different cluster id to the "polymorphic cluster ids" array.
   */
  protected void addPolymorphicClusterIds(final OClassImpl iBaseClass) {
    Set<Integer> clusters = new TreeSet<Integer>();

    for (int clusterId : polymorphicClusterIds) {
      clusters.add(clusterId);
    }
    for (int clusterId : iBaseClass.polymorphicClusterIds) {
      if (clusters.add(clusterId)) {
        try {
          addClusterIdToIndexes(clusterId);
        } catch (RuntimeException e) {
          OLogManager.instance().warn(this, "Error adding clusterId '%d' to index of class '%s'", e, clusterId, getName());
          clusters.remove(clusterId);
        }
      }
    }
    polymorphicClusterIds = new int[clusters.size()];
    int i = 0;
    for (Integer cluster : clusters) {
      polymorphicClusterIds[i] = cluster;
      i++;
    }
  }

  private void addPolymorphicClusterIdsWithInheritance(final OClassImpl iBaseClass) {
    addPolymorphicClusterIds(iBaseClass);
    for (OClassImpl superClass : superClasses) {
      superClass.addPolymorphicClusterIdsWithInheritance(iBaseClass);
    }
  }

  public List<OType> extractFieldTypes(final String[] fieldNames) {
    final List<OType> types = new ArrayList<OType>(fieldNames.length);

    for (String fieldName : fieldNames) {
      if (!fieldName.equals("@rid"))
        types.add(getProperty(decodeClassName(OIndexDefinitionFactory.extractFieldName(fieldName))).getType());
      else
        types.add(OType.LINK);
    }
    return types;
  }

  protected OClass setClusterIds(final int[] iClusterIds) {
    clusterIds = iClusterIds;
    Arrays.sort(clusterIds);

    return this;
  }

  public static String decodeClassName(String s) {
    if (s == null) {
      return null;
    }
    s = s.trim();
    if (s.startsWith("`") && s.endsWith("`")) {
      return s.substring(1, s.length() - 1);
    }
    return s;
  }

  public void fromStream(ODocument document) {
    this.document = document;
    fromStream();
  }

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

    try {
      document.field("name", name);
      document.field("shortName", shortName);
      document.field("description", description);
      document.field("defaultClusterId", defaultClusterId);
      document.field("clusterIds", clusterIds);
      document.field("clusterSelection", clusterSelection.getName());
      document.field("overSize", overSize);
      document.field("strictMode", strictMode);
      document.field("abstract", abstractClass);

      final Set<ODocument> props = new LinkedHashSet<ODocument>();
      for (final OProperty p : properties.values()) {
        props.add(((OPropertyImpl) p).toNetworkStream());
      }
      document.field("properties", props, OType.EMBEDDEDSET);

      if (superClasses.isEmpty()) {
        // Single super class is deprecated!
        document.field("superClass", null, OType.STRING);
        document.field("superClasses", null, OType.EMBEDDEDLIST);
      } else {
        // Single super class is deprecated!
        document.field("superClass", superClasses.get(0).getName(), OType.STRING);
        List<String> superClassesNames = new ArrayList<String>();
        for (OClassImpl superClass : superClasses) {
          superClassesNames.add(superClass.getName());
        }
        document.field("superClasses", superClassesNames, OType.EMBEDDEDLIST);
      }

      document.field("customFields", customFields != null && customFields.size() > 0 ? customFields : null, OType.EMBEDDEDMAP);

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

    return document;
  }
}


/**
 * 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://www.orientdb.com
 */
package com.orientechnologies.spatial.operator;

import com.orientechnologies.lucene.operator.OLuceneOperatorUtil;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.record.binary.ODocumentSerializer;
import com.orientechnologies.orient.core.sql.OIndexSearchResult;
import com.orientechnologies.orient.core.sql.OSQLEngine;
import com.orientechnologies.orient.core.sql.filter.OSQLFilterCondition;
import com.orientechnologies.orient.core.sql.functions.OSQLFunction;
import com.orientechnologies.orient.core.sql.operator.OIndexReuseType;
import com.orientechnologies.orient.core.sql.operator.OQueryTargetOperator;
import com.orientechnologies.spatial.shape.OShapeBuilder;
import com.orientechnologies.spatial.shape.OShapeFactory;

import java.util.List;

/**
 * Created by Enrico Risa on 12/08/15.
 */
public abstract class OLuceneSpatialOperator extends OQueryTargetOperator {

  protected OShapeBuilder factory;

  protected OLuceneSpatialOperator(String iKeyword, int iPrecedence, boolean iLogical) {
    super(iKeyword, iPrecedence, iLogical);
    factory = OShapeFactory.INSTANCE;
  }

  @Override
  public OIndexSearchResult getOIndexSearchResult(OClass iSchemaClass, OSQLFilterCondition iCondition,
      List<OIndexSearchResult> iIndexSearchResults, OCommandContext context) {
    return OLuceneOperatorUtil.buildOIndexSearchResult(iSchemaClass, iCondition, iIndexSearchResults, context);
  }

  // TODO HANDLE EVALUATE RECORD
  @Override
  public Object evaluateRecord(OIdentifiable iRecord, ODocument iCurrentResult, OSQLFilterCondition iCondition, Object iLeft,
      Object iRight, OCommandContext iContext, final ODocumentSerializer serializer) {

    OSQLFunction function = OSQLEngine.getInstance().getFunction(keyword);
    return function.execute(this, iRecord, iCurrentResult, new Object[] { iLeft, iCondition.getRight() }, iContext);
  }

  @Override
  public OIndexReuseType getIndexReuseType(Object iLeft, Object iRight) {
    return OIndexReuseType.INDEX_OPERATOR;
  }

  @Override
  public ORID getBeginRidRange(Object iLeft, Object iRight) {
    return null;
  }

  @Override
  public ORID getEndRidRange(Object iLeft, Object iRight) {
    return null;
  }

}


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

import com.orientechnologies.common.collection.OLazyIterator;
import com.orientechnologies.common.collection.OMultiCollectionIterator;
import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.core.db.ODatabase;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
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.OSerializationException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.OMetadataInternal;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.record.impl.ODocumentInternal;
import com.orientechnologies.orient.core.serialization.ODocumentSerializable;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;
import com.orientechnologies.orient.core.serialization.serializer.string.OStringBuilderSerializable;
import com.orientechnologies.orient.core.serialization.serializer.string.OStringSerializerEmbedded;
import com.orientechnologies.orient.core.storage.OStorageProxy;

import java.util.*;
import java.util.Map.Entry;

@SuppressWarnings({ "unchecked", "serial" })
public abstract class ORecordSerializerCSVAbstract extends ORecordSerializerStringAbstract {
  public static final char    FIELD_VALUE_SEPARATOR = ':';

  /**
   * Serialize the link.
   *
   * @param buffer
   * @param iParentRecord
   * @param iLinked       Can be an instance of ORID or a Record<?>
   *
   * @return
   */
  private static OIdentifiable linkToStream(final StringBuilder buffer, final ODocument iParentRecord, Object iLinked) {
    if (iLinked == null)
      // NULL REFERENCE
      return null;

    OIdentifiable resultRid = null;
    ORID rid;

    if (iLinked instanceof ORID) {
      // JUST THE REFERENCE
      rid = (ORID) iLinked;

      assert rid.getIdentity().isValid() || (ODatabaseRecordThreadLocal.instance().get().getStorage() instanceof OStorageProxy) :
          "Impossible to serialize invalid link " + rid.getIdentity();
      resultRid = rid;
    } else {
      if (iLinked instanceof String)
        iLinked = new ORecordId((String) iLinked);

      if (!(iLinked instanceof OIdentifiable))
        throw new IllegalArgumentException(
            "Invalid object received. Expected a OIdentifiable but received type=" + iLinked.getClass().getName() + " and value="
                + iLinked);

      // RECORD
      ORecord iLinkedRecord = ((OIdentifiable) iLinked).getRecord();
      rid = iLinkedRecord.getIdentity();

      assert rid.getIdentity().isValid() || (ODatabaseRecordThreadLocal.instance().get().getStorage() instanceof OStorageProxy) :
          "Impossible to serialize invalid link " + rid.getIdentity();

      final ODatabaseDocument database = ODatabaseRecordThreadLocal.instance().get();
      if (iParentRecord != null) {
        if (!database.isRetainRecords())
          // REPLACE CURRENT RECORD WITH ITS ID: THIS SAVES A LOT OF MEMORY
          resultRid = iLinkedRecord.getIdentity();
      }
    }

    if (rid.isValid())
      rid.toString(buffer);

    return resultRid;
  }

  public Object fieldFromStream(final ORecord iSourceRecord, final OType iType, OClass iLinkedClass, OType iLinkedType,
      final String iName, final String iValue) {

    if (iValue == null)
      return null;

    switch (iType) {
    case EMBEDDEDLIST:
    case EMBEDDEDSET:
      return embeddedCollectionFromStream((ODocument) iSourceRecord, iType, iLinkedClass, iLinkedType, iValue);

    case LINKSET:
    case LINKLIST: {
      if (iValue.length() == 0)
        return null;

      // REMOVE BEGIN & END COLLECTIONS CHARACTERS IF IT'S A COLLECTION
      final String value = iValue.startsWith("[") || iValue.startsWith("<") ? iValue.substring(1, iValue.length() - 1) : iValue;

      if (iType == OType.LINKLIST) {
        return unserializeList((ODocument) iSourceRecord, value);
      } else {
        return unserializeSet((ODocument) iSourceRecord, value);
      }
    }

    case LINKMAP: {
      if (iValue.length() == 0)
        return null;

      // REMOVE BEGIN & END MAP CHARACTERS
      String value = iValue.substring(1, iValue.length() - 1);

      @SuppressWarnings("rawtypes")
      final Map map = new ORecordLazyMap((ODocument) iSourceRecord, ODocument.RECORD_TYPE);

      if (value.length() == 0)
        return map;

      final List<String> items = OStringSerializerHelper.smartSplit(value, OStringSerializerHelper.RECORD_SEPARATOR, true, false);

      // EMBEDDED LITERALS
      for (String item : items) {
        if (item != null && !item.isEmpty()) {
          final List<String> entry = OStringSerializerHelper.smartSplit(item, OStringSerializerHelper.ENTRY_SEPARATOR);
          if (!entry.isEmpty()) {
            String mapValue = entry.get(1);
            if (mapValue != null && !mapValue.isEmpty())
              mapValue = mapValue.substring(1);
            map.put(fieldTypeFromStream((ODocument) iSourceRecord, OType.STRING, entry.get(0)), new ORecordId(mapValue));
          }

        }
      }
      return map;
    }

    case EMBEDDEDMAP:
      return embeddedMapFromStream((ODocument) iSourceRecord, iLinkedType, iValue, iName);

    case LINK:
      if (iValue.length() > 1) {
        int pos = iValue.indexOf(OStringSerializerHelper.CLASS_SEPARATOR);
        if (pos > -1)
          ((OMetadataInternal) ODatabaseRecordThreadLocal.instance().get().getMetadata()).getImmutableSchemaSnapshot()
              .getClass(iValue.substring(1, pos));
        else
          pos = 0;

        final String linkAsString = iValue.substring(pos + 1);
        try {
          return new ORecordId(linkAsString);
        } catch (IllegalArgumentException e) {
          OLogManager.instance()
              .error(this, "Error on unmarshalling field '%s' of record '%s': value '%s' is not a link", e, iName, iSourceRecord,
                  linkAsString);
          return new ORecordId();
        }
      } else
        return null;

    case EMBEDDED:
      if (iValue.length() > 2) {
        // REMOVE BEGIN & END EMBEDDED CHARACTERS
        final String value = iValue.substring(1, iValue.length() - 1);

        final Object embeddedObject = OStringSerializerEmbedded.INSTANCE.fromStream(value);
        if (embeddedObject instanceof ODocument)
          ODocumentInternal.addOwner((ODocument) embeddedObject, iSourceRecord);

        // RECORD
        return embeddedObject;
      } else
        return null;
    case LINKBAG:
      final String value =
          iValue.charAt(0) == OStringSerializerHelper.BAG_BEGIN ? iValue.substring(1, iValue.length() - 1) : iValue;
      return ORidBag.fromStream(value);
    default:
      return fieldTypeFromStream((ODocument) iSourceRecord, iType, iValue);
    }
  }

  public Map<String, Object> embeddedMapFromStream(final ODocument iSourceDocument, final OType iLinkedType, final String iValue,
      final String iName) {
    if (iValue.length() == 0)
      return null;

    // REMOVE BEGIN & END MAP CHARACTERS
    String value = iValue.substring(1, iValue.length() - 1);

    @SuppressWarnings("rawtypes")
    Map map;
    if (iLinkedType == OType.LINK || iLinkedType == OType.EMBEDDED)
      map = new ORecordLazyMap(iSourceDocument, ODocument.RECORD_TYPE);
    else
      map = new OTrackedMap<Object>(iSourceDocument);

    if (value.length() == 0)
      return map;

    final List<String> items = OStringSerializerHelper.smartSplit(value, OStringSerializerHelper.RECORD_SEPARATOR, true, false);

    // EMBEDDED LITERALS

    if (map instanceof ORecordElement)
      ((ORecordElement) map).setInternalStatus(STATUS.UNMARSHALLING);

    for (String item : items) {
      if (item != null && !item.isEmpty()) {
        final List<String> entries = OStringSerializerHelper.smartSplit(item, OStringSerializerHelper.ENTRY_SEPARATOR, true, false);
        if (!entries.isEmpty()) {
          final Object mapValueObject;
          if (entries.size() > 1) {
            String mapValue = entries.get(1);

            final OType linkedType;

            if (iLinkedType == null)
              if (!mapValue.isEmpty()) {
                linkedType = getType(mapValue);
                if ((iName == null || iSourceDocument.fieldType(iName) == null
                    || iSourceDocument.fieldType(iName) != OType.EMBEDDEDMAP) && isConvertToLinkedMap(map, linkedType)) {
                  // CONVERT IT TO A LAZY MAP
                  map = new ORecordLazyMap(iSourceDocument, ODocument.RECORD_TYPE);
                  ((ORecordElement) map).setInternalStatus(STATUS.UNMARSHALLING);
                } else if (map instanceof ORecordLazyMap && linkedType != OType.LINK) {
                  map = new OTrackedMap<Object>(iSourceDocument, map, null);
                }
              } else
                linkedType = OType.EMBEDDED;
            else
              linkedType = iLinkedType;

            if (linkedType == OType.EMBEDDED && mapValue.length() >= 2)
              mapValue = mapValue.substring(1, mapValue.length() - 1);

            mapValueObject = fieldTypeFromStream(iSourceDocument, linkedType, mapValue);

            if (mapValueObject != null && mapValueObject instanceof ODocument)
              ODocumentInternal.addOwner((ODocument) mapValueObject, iSourceDocument);
          } else
            mapValueObject = null;

          final Object key = fieldTypeFromStream(iSourceDocument, OType.STRING, entries.get(0));
          try {
            map.put(key, mapValueObject);
          } catch (ClassCastException e) {
            throw OException.wrapException(new OSerializationException(
                "Cannot load map because the type was not the expected: key=" + key + "(type " + key.getClass().toString()
                    + "), value=" + mapValueObject + "(type " + key.getClass() + ")"), e);
          }
        }

      }
    }

    if (map instanceof ORecordElement)
      ((ORecordElement) map).setInternalStatus(STATUS.LOADED);

    return map;
  }

  public void fieldToStream(final ODocument iRecord, final StringBuilder iOutput, final OType iType, final OClass iLinkedClass,
      final OType iLinkedType, final String iName, final Object iValue, final boolean iSaveOnlyDirty) {
    if (iValue == null)
      return;

    final long timer = PROFILER.startChrono();

    switch (iType) {

    case LINK: {
      if (!(iValue instanceof OIdentifiable))
        throw new OSerializationException(
            "Found an unexpected type during marshalling of a LINK where a OIdentifiable (ORID or any Record) was expected. The string representation of the object is: "
                + iValue);

      if (!((OIdentifiable) iValue).getIdentity().isValid() && iValue instanceof ODocument && ((ODocument) iValue).isEmbedded()) {
        // WRONG: IT'S EMBEDDED!
        fieldToStream(iRecord, iOutput, OType.EMBEDDED, iLinkedClass, iLinkedType, iName, iValue, iSaveOnlyDirty);
      } else {
        final Object link = linkToStream(iOutput, iRecord, iValue);
        if (link != null)
          // OVERWRITE CONTENT
          iRecord.field(iName, link);
        PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.link2string"), "Serialize link to string", timer);
      }
      break;
    }

    case LINKLIST: {
      iOutput.append(OStringSerializerHelper.LIST_BEGIN);
      final ORecordLazyList coll;
      final Iterator<OIdentifiable> it;
      if (iValue instanceof OMultiCollectionIterator<?>) {
        final OMultiCollectionIterator<OIdentifiable> iterator = (OMultiCollectionIterator<OIdentifiable>) iValue;
        iterator.reset();
        it = iterator;
        coll = null;
      } else if (!(iValue instanceof ORecordLazyList)) {
        // FIRST TIME: CONVERT THE ENTIRE COLLECTION
        coll = new ORecordLazyList(iRecord);

        if (iValue.getClass().isArray()) {
          Iterable<Object> iterab = OMultiValue.getMultiValueIterable(iValue, false);
          for (Object i : iterab) {
            coll.add((OIdentifiable) i);
          }
        } else {
          coll.addAll((Collection<? extends OIdentifiable>) iValue);
          ((Collection<? extends OIdentifiable>) iValue).clear();
        }

        iRecord.field(iName, coll);
        it = coll.rawIterator();
      } else {
        // LAZY LIST
        coll = (ORecordLazyList) iValue;
        it = coll.rawIterator();
      }

      if (it != null && it.hasNext()) {
        final StringBuilder buffer = new StringBuilder(128);
        for (int items = 0; it.hasNext(); items++) {
          if (items > 0)
            buffer.append(OStringSerializerHelper.RECORD_SEPARATOR);

          final OIdentifiable item = it.next();

          final OIdentifiable newRid = linkToStream(buffer, iRecord, item);
          if (newRid != null)
            ((OLazyIterator<OIdentifiable>) it).update(newRid);
        }

        if (coll != null)
          coll.convertRecords2Links();

        iOutput.append(buffer);

      }

      iOutput.append(OStringSerializerHelper.LIST_END);
      PROFILER
          .stopChrono(PROFILER.getProcessMetric("serializer.record.string.linkList2string"), "Serialize linklist to string", timer);
      break;
    }

    case LINKSET: {
      if (!(iValue instanceof OStringBuilderSerializable)) {
        if (iValue instanceof OAutoConvertToRecord)
          ((OAutoConvertToRecord) iValue).setAutoConvertToRecord(false);

        final Collection<OIdentifiable> coll;

        // FIRST TIME: CONVERT THE ENTIRE COLLECTION
        if (!(iValue instanceof ORecordLazySet)) {
          final ORecordLazySet set = new ORecordLazySet(iRecord);
          set.addAll((Collection<OIdentifiable>) iValue);
          iRecord.field(iName, set);
          coll = set;
        } else
          coll = (Collection<OIdentifiable>) iValue;

        serializeSet(coll, iOutput);

      } else {
        // LAZY SET
        final OStringBuilderSerializable coll = (OStringBuilderSerializable) iValue;
        coll.toStream(iOutput);
      }

      PROFILER
          .stopChrono(PROFILER.getProcessMetric("serializer.record.string.linkSet2string"), "Serialize linkset to string", timer);
      break;
    }

    case LINKMAP: {
      iOutput.append(OStringSerializerHelper.MAP_BEGIN);

      Map<Object, Object> map = (Map<Object, Object>) iValue;

      boolean invalidMap = false;
      int items = 0;
      for (Map.Entry<Object, Object> entry : map.entrySet()) {
        if (items++ > 0)
          iOutput.append(OStringSerializerHelper.RECORD_SEPARATOR);

        fieldTypeToString(iOutput, OType.STRING, entry.getKey());
        iOutput.append(OStringSerializerHelper.ENTRY_SEPARATOR);
        final Object link = linkToStream(iOutput, iRecord, entry.getValue());

        if (link != null && !invalidMap)
          // IDENTITY IS CHANGED, RE-SET INTO THE COLLECTION TO RECOMPUTE THE HASH
          invalidMap = true;
      }

      if (invalidMap) {
        final ORecordLazyMap newMap = new ORecordLazyMap(iRecord, ODocument.RECORD_TYPE);

        // REPLACE ALL CHANGED ITEMS
        for (Map.Entry<Object, Object> entry : map.entrySet()) {
          newMap.put(entry.getKey(), (OIdentifiable) entry.getValue());
        }
        map.clear();
        iRecord.field(iName, newMap);
      }

      iOutput.append(OStringSerializerHelper.MAP_END);
      PROFILER
          .stopChrono(PROFILER.getProcessMetric("serializer.record.string.linkMap2string"), "Serialize linkmap to string", timer);
      break;
    }

    case EMBEDDED:
      if (iValue instanceof ORecord) {
        iOutput.append(OStringSerializerHelper.EMBEDDED_BEGIN);
        toString((ORecord) iValue, iOutput, null, false, true);
        iOutput.append(OStringSerializerHelper.EMBEDDED_END);
      } else if (iValue instanceof ODocumentSerializable) {
        final ODocument doc = ((ODocumentSerializable) iValue).toDocument();
        doc.field(ODocumentSerializable.CLASS_NAME, iValue.getClass().getName());

        iOutput.append(OStringSerializerHelper.EMBEDDED_BEGIN);
        toString(doc, iOutput, null, false, true);
        iOutput.append(OStringSerializerHelper.EMBEDDED_END);

      } else if (iValue != null)
        iOutput.append(iValue.toString());
      PROFILER
          .stopChrono(PROFILER.getProcessMetric("serializer.record.string.embed2string"), "Serialize embedded to string", timer);
      break;

    case EMBEDDEDLIST:
      embeddedCollectionToStream(null, iOutput, iLinkedClass, iLinkedType, iValue, iSaveOnlyDirty,
          false);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.embedList2string"),
          "Serialize embeddedlist to string", timer);
      break;

    case EMBEDDEDSET:
      embeddedCollectionToStream(null, iOutput, iLinkedClass, iLinkedType, iValue, iSaveOnlyDirty,
          true);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.embedSet2string"), "Serialize embeddedset to string",
          timer);
      break;

    case EMBEDDEDMAP: {
      embeddedMapToStream(null, iOutput, iLinkedClass, iLinkedType, iValue, iSaveOnlyDirty);
      PROFILER.stopChrono(PROFILER.getProcessMetric("serializer.record.string.embedMap2string"), "Serialize embeddedmap to string",
          timer);
      break;
    }

    case LINKBAG: {
      iOutput.append(OStringSerializerHelper.BAG_BEGIN);
      ((ORidBag) iValue).toStream(iOutput);
      iOutput.append(OStringSerializerHelper.BAG_END);
      break;
    }

    default:
      fieldTypeToString(iOutput, iType, iValue);
    }
  }

  public void embeddedMapToStream(ODatabase<?> iDatabase, final StringBuilder iOutput, final OClass iLinkedClass, OType iLinkedType,
      final Object iValue, final boolean iSaveOnlyDirty) {
    iOutput.append(OStringSerializerHelper.MAP_BEGIN);

    if (iValue != null) {
      int items = 0;
      // EMBEDDED OBJECTS
      for (Entry<String, Object> o : ((Map<String, Object>) iValue).entrySet()) {
        if (items > 0)
          iOutput.append(OStringSerializerHelper.RECORD_SEPARATOR);

        if (o != null) {
          fieldTypeToString(iOutput, OType.STRING, o.getKey());
          iOutput.append(OStringSerializerHelper.ENTRY_SEPARATOR);

          if (o.getValue() instanceof ODocument && ((ODocument) o.getValue()).getIdentity().isValid()) {
            fieldTypeToString(iOutput, OType.LINK, o.getValue());
          } else if (o.getValue() instanceof ORecord || o.getValue() instanceof ODocumentSerializable) {
            final ODocument record;
            if (o.getValue() instanceof ODocument)
              record = (ODocument) o.getValue();
            else if (o.getValue() instanceof ODocumentSerializable) {
              record = ((ODocumentSerializable) o.getValue()).toDocument();
              record.field(ODocumentSerializable.CLASS_NAME, o.getValue().getClass().getName());
            } else {
              record = null;
            }
            iOutput.append(OStringSerializerHelper.EMBEDDED_BEGIN);
            toString(record, iOutput, null, false, true);
            iOutput.append(OStringSerializerHelper.EMBEDDED_END);
          } else if (o.getValue() instanceof Set<?>) {
            // SUB SET
            fieldTypeToString(iOutput, OType.EMBEDDEDSET, o.getValue());
          } else if (o.getValue() instanceof Collection<?>) {
            // SUB LIST
            fieldTypeToString(iOutput, OType.EMBEDDEDLIST, o.getValue());
          } else if (o.getValue() instanceof Map<?, ?>) {
            // SUB MAP
            fieldTypeToString(iOutput, OType.EMBEDDEDMAP, o.getValue());
          } else {
            // EMBEDDED LITERALS
            if (iLinkedType == null && o.getValue() != null) {
              fieldTypeToString(iOutput, OType.getTypeByClass(o.getValue().getClass()), o.getValue());
            } else {
              fieldTypeToString(iOutput, iLinkedType, o.getValue());
            }
          }
        }
        items++;
      }
    }

    iOutput.append(OStringSerializerHelper.MAP_END);
  }

  public Object embeddedCollectionFromStream(final ODocument iDocument, final OType iType, OClass iLinkedClass,
      final OType iLinkedType, final String iValue) {
    if (iValue.length() == 0)
      return null;

    // REMOVE BEGIN & END COLLECTIONS CHARACTERS IF IT'S A COLLECTION
    final String value =
        iValue.charAt(0) == OStringSerializerHelper.LIST_BEGIN || iValue.charAt(0) == OStringSerializerHelper.SET_BEGIN ?
            iValue.substring(1, iValue.length() - 1) :
            iValue;

    Collection<?> coll;
    if (iLinkedType == OType.LINK) {
      if (iDocument != null)
        coll = (iType == OType.EMBEDDEDLIST ? unserializeList(iDocument, value) : unserializeSet(iDocument, value));
      else {
        if (iType == OType.EMBEDDEDLIST)
          coll = unserializeList(iDocument, value);
        else {
          return unserializeSet(iDocument, value);
        }
      }
    } else
      coll = iType == OType.EMBEDDEDLIST ? new OTrackedList<Object>(iDocument) : new OTrackedSet<Object>(iDocument);

    if (value.length() == 0)
      return coll;

    OType linkedType;

    if (coll instanceof ORecordElement)
      ((ORecordElement) coll).setInternalStatus(STATUS.UNMARSHALLING);

    final List<String> items = OStringSerializerHelper.smartSplit(value, OStringSerializerHelper.RECORD_SEPARATOR, true, false);
    for (String item : items) {
      Object objectToAdd = null;
      linkedType = null;

      if (item.equals("null"))
        // NULL VALUE
        objectToAdd = null;
      else if (item.length() > 2 && item.charAt(0) == OStringSerializerHelper.EMBEDDED_BEGIN) {
        // REMOVE EMBEDDED BEGIN/END CHARS
        item = item.substring(1, item.length() - 1);

        if (!item.isEmpty()) {
          // EMBEDDED RECORD, EXTRACT THE CLASS NAME IF DIFFERENT BY THE PASSED (SUB-CLASS OR IT WAS PASSED NULL)
          iLinkedClass = OStringSerializerHelper.getRecordClassName(item, iLinkedClass);

          if (iLinkedClass != null) {
            ODocument doc = new ODocument();
            objectToAdd = fromString(item, doc, null);
            ODocumentInternal.fillClassNameIfNeeded(doc, iLinkedClass.getName());
          } else
            // EMBEDDED OBJECT
            objectToAdd = fieldTypeFromStream(iDocument, OType.EMBEDDED, item);
        }
      } else {
        if (linkedType == null) {
          final char begin = item.length() > 0 ? item.charAt(0) : OStringSerializerHelper.LINK;

          // AUTO-DETERMINE LINKED TYPE
          if (begin == OStringSerializerHelper.LINK)
            linkedType = OType.LINK;
          else
            linkedType = getType(item);

          if (linkedType == null)
            throw new IllegalArgumentException(
                "Linked type cannot be null. Probably the serialized type has not stored the type along with data");
        }

        if (iLinkedType == OType.CUSTOM)
          item = item.substring(1, item.length() - 1);

        objectToAdd = fieldTypeFromStream(iDocument, linkedType, item);
      }

      if (objectToAdd != null && objectToAdd instanceof ODocument && coll instanceof ORecordElement)
        ODocumentInternal.addOwner((ODocument) objectToAdd, (ORecordElement) coll);

      ((Collection<Object>) coll).add(objectToAdd);
    }

    if (coll instanceof ORecordElement)
      ((ORecordElement) coll).setInternalStatus(STATUS.LOADED);

    return coll;
  }

  public StringBuilder embeddedCollectionToStream(ODatabase<?> iDatabase, final StringBuilder iOutput, final OClass iLinkedClass,
      final OType iLinkedType, final Object iValue, final boolean iSaveOnlyDirty, final boolean iSet) {
    iOutput.append(iSet ? OStringSerializerHelper.SET_BEGIN : OStringSerializerHelper.LIST_BEGIN);

    final Iterator<Object> iterator = OMultiValue.getMultiValueIterator(iValue, false);

    OType linkedType = iLinkedType;

    for (int i = 0; iterator.hasNext(); ++i) {
      final Object o = iterator.next();

      if (i > 0)
        iOutput.append(OStringSerializerHelper.RECORD_SEPARATOR);

      if (o == null) {
        iOutput.append("null");
        continue;
      }

      OIdentifiable id = null;
      ODocument doc = null;

      final OClass linkedClass;
      if (!(o instanceof OIdentifiable)) {
        if (iLinkedType == null)
          linkedType = OType.getTypeByClass(o.getClass());

        linkedClass = iLinkedClass;
      } else {
        id = (OIdentifiable) o;

        if (iLinkedType == null)
          // AUTO-DETERMINE LINKED TYPE
          if (id.getIdentity().isValid())
            linkedType = OType.LINK;
          else
            linkedType = OType.EMBEDDED;

        if (id instanceof ODocument) {
          doc = (ODocument) id;

          if (doc.hasOwners())
            linkedType = OType.EMBEDDED;

          assert linkedType == OType.EMBEDDED || id.getIdentity().isValid() || (ODatabaseRecordThreadLocal.instance().get()
              .getStorage() instanceof OStorageProxy) : "Impossible to serialize invalid link " + id.getIdentity();

          linkedClass = ODocumentInternal.getImmutableSchemaClass(doc);
        } else
          linkedClass = null;
      }

      if (id != null && linkedType != OType.LINK)
        iOutput.append(OStringSerializerHelper.EMBEDDED_BEGIN);

      if (linkedType == OType.EMBEDDED && o instanceof OIdentifiable)
        toString((ORecord) ((OIdentifiable) o).getRecord(), iOutput, null);
      else if (linkedType != OType.LINK && (linkedClass != null || doc != null)) {
        toString(doc, iOutput, null, false, true);
      } else {
        // EMBEDDED LITERALS
        if (iLinkedType == null) {
          if (o != null)
            linkedType = OType.getTypeByClass(o.getClass());
        } else if (iLinkedType == OType.CUSTOM)
          iOutput.append(OStringSerializerHelper.CUSTOM_TYPE);
        fieldTypeToString(iOutput, linkedType, o);
      }

      if (id != null && linkedType != OType.LINK)
        iOutput.append(OStringSerializerHelper.EMBEDDED_END);
    }

    iOutput.append(iSet ? OStringSerializerHelper.SET_END : OStringSerializerHelper.LIST_END);
    return iOutput;
  }

  protected boolean isConvertToLinkedMap(Map<?, ?> map, final OType linkedType) {
    boolean convert = (linkedType == OType.LINK && !(map instanceof ORecordLazyMap));
    if (convert) {
      for (Object value : map.values())
        if (!(value instanceof OIdentifiable))
          return false;
    }
    return convert;
  }

  private void serializeSet(final Collection<OIdentifiable> coll, final StringBuilder iOutput) {
    iOutput.append(OStringSerializerHelper.SET_BEGIN);
    int i = 0;
    for (OIdentifiable rid : coll) {
      if (i++ > 0)
        iOutput.append(',');

      iOutput.append(rid.getIdentity().toString());
    }
    iOutput.append(OStringSerializerHelper.SET_END);
  }

  private ORecordLazyList unserializeList(final ODocument iSourceRecord, final String value) {
    final ORecordLazyList coll = new ORecordLazyList(iSourceRecord);
    final List<String> items = OStringSerializerHelper.smartSplit(value, OStringSerializerHelper.RECORD_SEPARATOR);
    for (String item : items) {
      if (item.length() == 0)
        coll.add(new ORecordId());
      else {
        if (item.startsWith("#"))
          coll.add(new ORecordId(item));
        else {
          final ORecord doc = fromString(item);
          if (doc instanceof ODocument)
            ODocumentInternal.addOwner((ODocument) doc, iSourceRecord);

          coll.add(doc);
        }
      }
    }
    return coll;
  }

  private ORecordLazySet unserializeSet(final ODocument iSourceRecord, final String value) {
    final ORecordLazySet coll = new ORecordLazySet(iSourceRecord);
    final List<String> items = OStringSerializerHelper.smartSplit(value, OStringSerializerHelper.RECORD_SEPARATOR);
    for (String item : items) {
      if (item.length() == 0)
        coll.add(new ORecordId());
      else {
        if (item.startsWith("#"))
          coll.add(new ORecordId(item));
        else {
          final ORecord doc = fromString(item);
          if (doc instanceof ODocument)
            ODocumentInternal.addOwner((ODocument) doc, iSourceRecord);

          coll.add(doc);
        }
      }
    }
    return coll;
  }
}


package com.orientechnologies.lucene.engine;

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.lucene.exception.OLuceneIndexException;
import com.orientechnologies.orient.core.command.OCommandContext;
import com.orientechnologies.orient.core.index.OIndexDefinition;
import com.orientechnologies.orient.core.record.impl.ODocument;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by frank on 04/05/2017.
 */
public class OLuceneIndexEngineUtils {

  public static void sendTotalHits(String indexName, OCommandContext context, long totalHits) {
    if (context != null) {

      if (context.getVariable("totalHits") == null) {
        context.setVariable("totalHits", totalHits);
      } else {
        context.setVariable("totalHits", null);
      }
      context.setVariable((indexName + ".totalHits").replace(".", "_"), totalHits);
    }

  }

  public static void sendLookupTime(String indexName, OCommandContext context, final TopDocs docs, final Integer limit,
      long startFetching) {
    if (context != null) {

      final long finalTime = System.currentTimeMillis() - startFetching;
      context.setVariable((indexName + ".lookupTime").replace(".", "_"), new HashMap<String, Object>() {
        {
          put("limit", limit);
          put("totalTime", finalTime);
          put("totalHits", docs.totalHits);
          put("returnedHits", docs.scoreDocs.length);
          if (!Float.isNaN(docs.getMaxScore())) {
            put("maxScore", docs.getMaxScore());
          }

        }
      });
    }
  }

  public static Document retrieveIndexMetadata(IndexWriter writer) {

    IndexReader reader = null;
    IndexSearcher searcher = null;
    try {
      reader = DirectoryReader.open(writer);

      searcher = new IndexSearcher(reader);

      final TopDocs topDocs = searcher.search(new TermQuery(new Term("_CLASS", "JSON_METADATA")), 1);

      final Document metaDoc = searcher.doc(topDocs.scoreDocs[0].doc);
      return metaDoc;
    } catch (IOException e) {
//      OLogManager.instance().error(OLuceneIndexEngineAbstract.class, "Error while retrieving index metadata", e);
      throw OException.wrapException(new OLuceneIndexException("unable to retrieve metadata document from index"), e);
    } finally {
      if (reader != null)
        try {
          reader.close();
        } catch (IOException e) {
          OLogManager.instance().error(OLuceneIndexEngineAbstract.class, "Error while retrieving index metadata", e);

        }

    }

  }

  public static List<SortField> buildSortFields(ODocument metadata) {
    List<Map<String, Object>> sortConf = Optional.ofNullable(metadata.<List<Map<String, Object>>>getProperty("sort"))
        .orElse(Collections.emptyList());

    final List<SortField> fields = sortConf.stream()
        .map(d -> buildSortField(d))
        .collect(Collectors.toList());

    return fields;
  }

  /**
   * Builds {@link SortField} from a configuration {@link ODocument}
   *
   * @param conf
   *
   * @return
   */
  public static SortField buildSortField(ODocument conf) {

    return buildSortField(conf.toMap());
  }

  /**
   * Builds a {@link SortField} from a configuration map. The map can contains up to three fields: field (name), reverse
   * (true/false) and type {@link SortField.Type}.
   *
   * @param conf
   *
   * @return
   */
  public static SortField buildSortField(Map<String, Object> conf) {

    final String field = Optional.ofNullable((String) conf.get("field")).orElse(null);
    final String type = Optional.ofNullable(((String) conf.get("type")).toUpperCase()).orElse(SortField.Type.STRING.name());
    final Boolean reverse = Optional.ofNullable((Boolean) conf.get("reverse")).orElse(false);

    SortField sortField = new SortField(field, SortField.Type.valueOf(type), reverse);

    return sortField;
  }

  public static ODocument getMetadataFromIndex(IndexWriter writer) {

    final Document metaDoc = retrieveIndexMetadata(writer);

    return new ODocument().fromJSON(metaDoc.get("_META_JSON"));

  }

  public static OIndexDefinition getIndexDefinitionFromIndex(IndexWriter writer) {

    final Document metaDoc = retrieveIndexMetadata(writer);

    final ODocument defAsJson = new ODocument().fromJSON(metaDoc.get("_DEF_JSON"));

    final String defClassName = metaDoc.get("_DEF_CLASS_NAME");

    try {
      final Class<?> indexDefClass = Class.forName(defClassName);
      OIndexDefinition indexDefinition = (OIndexDefinition) indexDefClass.getDeclaredConstructor().newInstance();
      indexDefinition.fromStream(defAsJson);
      return indexDefinition;

    } catch (ClassNotFoundException |
        NoSuchMethodException |
        InvocationTargetException |
        InstantiationException |
        IllegalAccessException e) {
      throw OException.wrapException(new OLuceneIndexException("Error during deserialization of index definition"), e);
    }
  }

}


/*
 *
 *  *  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 java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;

import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.record.ORecordAbstract;
import com.orientechnologies.orient.core.serialization.OMemoryStream;

/**
 * The rawest representation of a record. It's schema less. Use this if you need to store Strings or byte[] without matter about the
 * content. Useful also to store multimedia contents and binary files. The object can be reused across calls to the database by
 * using the reset() at every re-use.
 */
@SuppressWarnings({ "unchecked" })
public class ORecordBytes extends ORecordAbstract implements OBlob {
  private static final long   serialVersionUID = 1L;

  private static final byte[] EMPTY_SOURCE     = new byte[] {};

  public ORecordBytes() {
    setup();
  }

  public ORecordBytes(final ODatabaseDocumentInternal iDatabase) {
    setup();
    ODatabaseRecordThreadLocal.instance().set(iDatabase);
  }

  public ORecordBytes(final ODatabaseDocumentInternal iDatabase, final byte[] iSource) {
    this(iSource);
    ODatabaseRecordThreadLocal.instance().set(iDatabase);
  }

  public ORecordBytes(final byte[] iSource) {
    super(iSource);
    _dirty = true;
    _contentChanged = true;
    setup();
  }

  public ORecordBytes(final ORID iRecordId) {
    _recordId = (ORecordId) iRecordId;
    setup();
  }

  public ORecordBytes reset(final byte[] iSource) {
    reset();
    _source = iSource;
    return this;
  }

  @Override
  public ORecordBytes copy() {
    return (ORecordBytes) copyTo(new ORecordBytes());
  }

  @Override
  public ORecordBytes fromStream(final byte[] iRecordBuffer) {
    _source = iRecordBuffer;
    _status = ORecordElement.STATUS.LOADED;
    return this;
  }

  @Override
  public ORecordAbstract clear() {
    clearSource();
    return super.clear();
  }

  @Override
  public byte[] toStream() {
    return _source;
  }

  public byte getRecordType() {
    return RECORD_TYPE;
  }

  @Override
  protected void setup() {
    super.setup();
  }

  /**
   * Reads the input stream in memory. This is less efficient than {@link #fromInputStream(InputStream, int)} because allocation is
   * made multiple times. If you already know the input size use {@link #fromInputStream(InputStream, int)}.
   *
   * @param in
   *          Input Stream, use buffered input stream wrapper to speed up reading
   * @return Buffer read from the stream. It's also the internal buffer size in bytes
   * @throws IOException
   */
  public int fromInputStream(final InputStream in) throws IOException {
    final OMemoryStream out = new OMemoryStream();
    try {
      final byte[] buffer = new byte[OMemoryStream.DEF_SIZE];
      int readBytesCount;
      while (true) {
        readBytesCount = in.read(buffer, 0, buffer.length);
        if (readBytesCount == -1) {
          break;
        }
        out.write(buffer, 0, readBytesCount);
      }
      out.flush();
      _source = out.toByteArray();
    } finally {
      out.close();
    }
    _size = _source.length;
    return _size;
  }

  /**
   * Reads the input stream in memory specifying the maximum bytes to read. This is more efficient than
   * {@link #fromInputStream(InputStream)} because allocation is made only once.
   *
   * @param in
   *          Input Stream, use buffered input stream wrapper to speed up reading
   * @param maxSize
   *          Maximum size to read
   * @return Buffer count of bytes that are read from the stream. It's also the internal buffer size in bytes
   * @throws IOException
   *           if an I/O error occurs.
   */
  public int fromInputStream(final InputStream in, final int maxSize) throws IOException {
    final byte[] buffer = new byte[maxSize];
    int totalBytesCount = 0;
    int readBytesCount;
    while (totalBytesCount < maxSize) {
      readBytesCount = in.read(buffer, totalBytesCount, buffer.length - totalBytesCount);
      if (readBytesCount == -1) {
        break;
      }
      totalBytesCount += readBytesCount;
    }

    if (totalBytesCount == 0) {
      _source = EMPTY_SOURCE;
      _size = 0;
    } else if (totalBytesCount == maxSize) {
      _source = buffer;
      _size = maxSize;
    } else {
      _source = Arrays.copyOf(buffer, totalBytesCount);
      _size = totalBytesCount;
    }
    return _size;
  }

  public void toOutputStream(final OutputStream out) throws IOException {
    checkForLoading();

    if (_source.length > 0) {
      out.write(_source);
    }
  }
}