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

import com.orientechnologies.common.concur.lock.OLockException;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.exception.OSystemException;
import com.orientechnologies.common.io.OIOException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.orient.client.remote.OStorageRemoteNodeSession;
import com.orientechnologies.orient.client.remote.OStorageRemoteSession;
import com.orientechnologies.orient.client.remote.message.OError37Response;
import com.orientechnologies.orient.core.OConstants;
import com.orientechnologies.orient.core.config.OContextConfiguration;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.serialization.OMemoryInputStream;
import com.orientechnologies.orient.enterprise.channel.OSocketFactory;
import com.orientechnologies.orient.enterprise.channel.binary.OChannelBinary;
import com.orientechnologies.orient.enterprise.channel.binary.OChannelBinaryProtocol;
import com.orientechnologies.orient.enterprise.channel.binary.ONetworkProtocolException;
import com.orientechnologies.orient.enterprise.channel.binary.OResponseProcessingException;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.Map;

public class OChannelBinaryAsynchClient extends OChannelBinary {
  private         int    socketTimeout;                                               // IN MS
  protected final short  srvProtocolVersion;
  private         String serverURL;
  private         byte   currentStatus;
  private         int    currentSessionId;
  private         byte   currentMessage;

  public OChannelBinaryAsynchClient(final String remoteHost, final int remotePort, final String iDatabaseName,
      final OContextConfiguration iConfig, final int iProtocolVersion) throws IOException {
    super(OSocketFactory.instance(iConfig).createSocket(), iConfig);
    try {

      serverURL = remoteHost + ":" + remotePort;
      if (iDatabaseName != null)
        serverURL += "/" + iDatabaseName;
      socketTimeout = iConfig.getValueAsInteger(OGlobalConfiguration.NETWORK_SOCKET_TIMEOUT);

      try {
        socket.connect(new InetSocketAddress(remoteHost, remotePort), getSocketTimeout());
        setReadResponseTimeout();
        connected();
      } catch (java.net.SocketTimeoutException e) {
        throw new IOException("Cannot connect to host " + remoteHost + ":" + remotePort, e);
      }
      try {
        if (socketBufferSize > 0) {
          inStream = new BufferedInputStream(socket.getInputStream(), socketBufferSize);
          outStream = new BufferedOutputStream(socket.getOutputStream(), socketBufferSize);
        } else {
          inStream = new BufferedInputStream(socket.getInputStream());
          outStream = new BufferedOutputStream(socket.getOutputStream());
        }

        in = new DataInputStream(inStream);
        out = new DataOutputStream(outStream);

        srvProtocolVersion = readShort();

        writeByte(OChannelBinaryProtocol.REQUEST_HANDSHAKE);
        writeShort((short) iProtocolVersion);
        writeString("Java Client");
        writeString(OConstants.getVersion());
        writeByte(OChannelBinaryProtocol.ENCODING_DEFAULT);
        writeByte(OChannelBinaryProtocol.ERROR_MESSAGE_JAVA);
        flush();
      } catch (IOException e) {
        throw new ONetworkProtocolException(
            "Cannot read protocol version from remote server " + socket.getRemoteSocketAddress() + ": " + e);
      }

      if (srvProtocolVersion != iProtocolVersion) {
        OLogManager.instance().warn(this,
            "The Client driver version is different than Server version: client=" + iProtocolVersion + ", server="
                + srvProtocolVersion
                + ". You could not use the full features of the newer version. Assure to have the same versions on both");
      }

    } catch (RuntimeException e) {
      if (socket.isConnected())
        socket.close();
      throw e;
    }
  }

  @SuppressWarnings("unchecked")
  private static RuntimeException createException(final String iClassName, final String iMessage, final Exception iPrevious) {
    RuntimeException rootException = null;
    Constructor<?> c = null;
    try {
      final Class<RuntimeException> excClass = (Class<RuntimeException>) Class.forName(iClassName);
      if (iPrevious != null) {
        try {
          c = excClass.getConstructor(String.class, Throwable.class);
        } catch (NoSuchMethodException e) {
          c = excClass.getConstructor(String.class, Exception.class);
        }
      }

      if (c == null)
        c = excClass.getConstructor(String.class);

    } catch (Exception e) {
      // UNABLE TO REPRODUCE THE SAME SERVER-SIDE EXCEPTION: THROW AN SYSTEM EXCEPTION
      rootException = OException.wrapException(new OSystemException(iMessage), iPrevious);
    }

    if (c != null)
      try {
        final Exception cause;
        if (c.getParameterTypes().length > 1)
          cause = (Exception) c.newInstance(iMessage, iPrevious);
        else
          cause = (Exception) c.newInstance(iMessage);

        rootException = OException.wrapException(new OSystemException("Data processing exception"), cause);
      } catch (InstantiationException ignored) {
      } catch (IllegalAccessException ignored) {
      } catch (InvocationTargetException ignored) {
      }

    return rootException;
  }

  public byte[] beginResponse(final int iRequesterId, final boolean token) throws IOException {
    return beginResponse(iRequesterId, timeout, token);
  }

  public byte[] beginResponse(final int iRequesterId, final long iTimeout, final boolean token) throws IOException {
    try {
      // WAIT FOR THE RESPONSE
      if (iTimeout <= 0)
        acquireReadLock();

      if (!isConnected()) {
        releaseReadLock();
        throw new IOException("Channel is closed");
      }

      try {
        setWaitResponseTimeout();
        currentStatus = readByte();
        currentSessionId = readInt();

        if (debug)
          OLogManager.instance()
              .debug(this, "%s - Read response: %d-%d", socket.getLocalAddress(), (int) currentStatus, currentSessionId);

      } finally {
        setReadResponseTimeout();
      }

      assert (currentSessionId == iRequesterId);

      if (debug)
        OLogManager.instance().debug(this, "%s - Session %d handle response", socket.getLocalAddress(), iRequesterId);
      byte[] tokenBytes;
      if (token)
        tokenBytes = this.readBytes();
      else
        tokenBytes = null;

      currentMessage = readByte();
      handleStatus(currentStatus, currentSessionId);
      return tokenBytes;
    } catch (OLockException e) {
      Thread.currentThread().interrupt();
      // NEVER HAPPENS?
      OLogManager.instance().error(this, "Unexpected error on reading response from channel", e);
    }
    return null;
  }

  public void endResponse() throws IOException {
    // WAKE UP ALL THE WAITING THREADS
    try {
      releaseReadLock();
    } catch (IllegalMonitorStateException e) {
      // IGNORE IT
      OLogManager.instance().debug(this, "Error on unlocking network channel after reading response");
    }

  }

  public void endRequest() throws IOException {
    flush();
    releaseWriteLock();
  }

  @Override
  public void close() {
    try {
      super.close();
    } catch (Exception e) {
      // IGNORE IT
    }
  }

  @Override
  public void clearInput() throws IOException {
    acquireReadLock();
    try {
      super.clearInput();
    } finally {
      releaseReadLock();
    }
  }

  /**
   * Tells if the channel is connected.
   *
   * @return true if it's connected, otherwise false.
   */
  public boolean isConnected() {
    final Socket s = socket;
    return s != null && !s.isClosed() && s.isConnected() && !s.isInputShutdown() && !s.isOutputShutdown();
  }

  /**
   * Gets the major supported protocol version
   */
  public short getSrvProtocolVersion() {
    return srvProtocolVersion;
  }

  public String getServerURL() {
    return serverURL;
  }

  public boolean tryLock() {
    return getLockWrite().tryAcquireLock();
  }

  public void unlock() {
    getLockWrite().unlock();
  }

  public interface ExceptionHandler {
    void onException(Throwable ex);
  }

  public int handleStatus(final byte iResult, final int iClientTxId, ExceptionHandler exceptionHandler) throws IOException {
    if (iResult == OChannelBinaryProtocol.RESPONSE_STATUS_OK || iResult == OChannelBinaryProtocol.PUSH_DATA) {
      return iClientTxId;
    } else if (iResult == OChannelBinaryProtocol.RESPONSE_STATUS_ERROR) {

      OError37Response response = new OError37Response();
      response.read(this, null);
      byte[] serializedException = response.getVerbose();
      Exception previous = null;
      if (serializedException != null && serializedException.length > 0) {
        Throwable deserializeException = deserializeException(serializedException);
        exceptionHandler.onException(deserializeException);
      }

      for (Map.Entry<String, String> entry : response.getMessages().entrySet()) {
        previous = createException(entry.getKey(), entry.getValue(), previous);
      }

      if (previous != null) {
        exceptionHandler.onException(new RuntimeException(previous));
      } else
        exceptionHandler.onException(new ONetworkProtocolException("Network response error"));

    } else {
      // PROTOCOL ERROR
      // close();
      exceptionHandler.onException(new ONetworkProtocolException("Error on reading response from the server"));
    }

    return iClientTxId;
  }

  public int handleStatus(final byte iResult, final int iClientTxId) throws IOException {
    return handleStatus(iResult, iClientTxId, this::handleException);
  }

  private void setReadResponseTimeout() throws SocketException {
    final Socket s = socket;
    if (s != null && s.isConnected() && !s.isClosed())
      s.setSoTimeout(getSocketTimeout());
  }

  private Throwable deserializeException(final byte[] serializedException) throws IOException {
    final OMemoryInputStream inputStream = new OMemoryInputStream(serializedException);
    final ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);

    Object throwable = null;
    try {
      throwable = objectInputStream.readObject();
    } catch (ClassNotFoundException e) {
      OLogManager.instance().error(this, "Error during exception deserialization", e);
      throw new IOException("Error during exception deserialization: " + e.toString(), e);
    }

    objectInputStream.close();
    return (Throwable) throwable;
  }

  public void handleException(Throwable throwable) {
    if (throwable instanceof OException) {
      try {
        final Class<? extends OException> cls = (Class<? extends OException>) throwable.getClass();
        final Constructor<? extends OException> constructor;
        constructor = cls.getConstructor(cls);
        final OException proxyInstance = constructor.newInstance(throwable);
        proxyInstance.addSuppressed((Exception) throwable);
        throw proxyInstance;

      } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
        OLogManager.instance().error(this, "Error during exception deserialization", e);
      }
    }

    if (throwable instanceof RuntimeException) {
      throw (RuntimeException) throwable;
    }
    if (throwable instanceof Throwable) {
      throw new OResponseProcessingException("Exception during response processing", (Throwable) throwable);
    } else {
      // WRAP IT
      OLogManager.instance().error(this,
          "Error during exception serialization, serialized exception is not Throwable, exception type is " + (throwable != null ?
              throwable.getClass().getName() :
              "null"), null);
    }

  }

  public void beginRequest(final byte iCommand, final OStorageRemoteSession session) throws IOException {
    final OStorageRemoteNodeSession nodeSession = session.getServerSession(getServerURL());
    beginRequest(iCommand, nodeSession);
  }

  public void beginRequest(byte iCommand, OStorageRemoteNodeSession nodeSession) throws IOException {
    if (nodeSession == null)
      throw new OIOException("Invalid session for URL '" + getServerURL() + "'");

    writeByte(iCommand);
    writeInt(nodeSession.getSessionId());
    writeBytes(nodeSession.getToken());
  }

  public int getSocketTimeout() {
    return socketTimeout;
  }

  public void setSocketTimeout(int socketTimeout) {
    this.socketTimeout = socketTimeout;
  }

}


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

import com.orientechnologies.common.io.OIOUtils;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.metadata.function.OFunction;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * SQL CREATE FUNCTION command.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 * @author Claudio Tesoriero
 */
public class OCommandExecutorSQLCreateFunction extends OCommandExecutorSQLAbstract {
  public static final String NAME = "CREATE FUNCTION";
  private String name;
  private String code;
  private String language;
  private boolean      idempotent = false;
  private List<String> parameters = null;

  @SuppressWarnings("unchecked")
  public OCommandExecutorSQLCreateFunction parse(final OCommandRequest iRequest) {
    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;

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

      init((OCommandRequestText) iRequest);

      parserRequiredKeyword("CREATE");
      parserRequiredKeyword("FUNCTION");

      name = parserNextWord(false);
      code = OIOUtils.getStringContent(parserNextWord(false));

      String temp = parseOptionalWord(true);
      while (temp != null) {
        if (temp.equals("IDEMPOTENT")) {
          parserNextWord(false);
          idempotent = Boolean.parseBoolean(parserGetLastWord());
        } else if (temp.equals("LANGUAGE")) {
          parserNextWord(false);
          language = parserGetLastWord();
        } else if (temp.equals("PARAMETERS")) {
          parserNextWord(false);
          parameters = new ArrayList<String>();
          OStringSerializerHelper.getCollection(parserGetLastWord(), 0, parameters);
          if (parameters.size() == 0)
            throw new OCommandExecutionException("Syntax Error. Missing function parameter(s): " + getSyntax());
        }

        temp = parserOptionalWord(true);
        if (parserIsEnded())
          break;
      }
    } finally {
      textRequest.setText(originalQuery);
    }

    return this;
  }

  @Override
  public long getDistributedTimeout() {
    return getDatabase().getConfiguration().getValueAsLong(OGlobalConfiguration.DISTRIBUTED_COMMAND_QUICK_TASK_SYNCH_TIMEOUT);
  }

  /**
   * Execute the command and return the ODocument object created.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    if (name == null)
      throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet");
    if (name.isEmpty())
      throw new OCommandExecutionException("Syntax Error. You must specify a function name: " + getSyntax());
    if (code == null || code.isEmpty())
      throw new OCommandExecutionException("Syntax Error. You must specify the function code: " + getSyntax());

    ODatabaseDocument database = getDatabase();
    final OFunction f = database.getMetadata().getFunctionLibrary().createFunction(name);
    f.setCode(code);
    f.setIdempotent(idempotent);
    if (parameters != null)
      f.setParameters(parameters);
    if (language != null)
      f.setLanguage(language);
    f.save();
    return f.getId();
  }

  @Override
  public String getSyntax() {
    return "CREATE FUNCTION <name> <code> [PARAMETERS [<comma-separated list of parameters' name>]] [IDEMPOTENT true|false] [LANGUAGE <language>]";
  }

}


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

package com.orientechnologies.common.util;

import java.nio.ByteBuffer;

/**
 * This class is utility class for split primitive types to separate byte buffers and vice versa. This class is used because we use
 * many byte buffers for mmap and there is situation when we need to write value on border of two buffers.
 *
 * @author Artem Loginov (logart2007@gmail.com)
 * @since 5/25/12 6:37 AM
 */
public class OByteBufferUtils {
  public static final int  SIZE_OF_SHORT        = 2;
  public static final int  SIZE_OF_INT          = 4;
  public static final int  SIZE_OF_LONG         = 8;
  private static final int SIZE_OF_BYTE_IN_BITS = 8;
  private static final int MASK                 = 0x000000FF;

  /**
   * Merge short value from two byte buffer. First byte of short will be extracted from first byte buffer and second from second
   * one.
   *
   * @param buffer
   *          to read first part of value
   * @param buffer1
   *          to read second part of value
   * @return merged value
   */
  public static short mergeShortFromBuffers(final ByteBuffer buffer, final ByteBuffer buffer1) {
    short result = 0;
    result = (short) (result | (buffer.get() & MASK));
    result = (short) (result << SIZE_OF_BYTE_IN_BITS);
    result = (short) (result | (buffer1.get() & MASK));
    return result;
  }

  /**
   * Merge int value from two byte buffer. First bytes of int will be extracted from first byte buffer and second from second one.
   * How many bytes will be read from first buffer determines based on buffer.remaining() value
   *
   * @param buffer
   *          to read first part of value
   * @param buffer1
   *          to read second part of value
   * @return merged value
   */
  public static int mergeIntFromBuffers(final ByteBuffer buffer, final ByteBuffer buffer1) {
    int result = 0;
    final int remaining = buffer.remaining();
    for (int i = 0; i < remaining; ++i) {
      result = result | (buffer.get() & MASK);
      result = result << SIZE_OF_BYTE_IN_BITS;
    }
    for (int i = 0; i < SIZE_OF_INT - remaining - 1; ++i) {
      result = result | (buffer1.get() & MASK);
      result = result << SIZE_OF_BYTE_IN_BITS;
    }
    result = result | (buffer1.get() & MASK);
    return result;
  }

  /**
   * Merge long value from two byte buffer. First bytes of long will be extracted from first byte buffer and second from second one.
   * How many bytes will be read from first buffer determines based on buffer.remaining() value
   *
   * @param buffer
   *          to read first part of value
   * @param buffer1
   *          to read second part of value
   * @return merged value
   */
  public static long mergeLongFromBuffers(final ByteBuffer buffer, final ByteBuffer buffer1) {
    long result = 0;
    final int remaining = buffer.remaining();
    for (int i = 0; i < remaining; ++i) {
      result = result | (MASK & buffer.get());
      result = result << SIZE_OF_BYTE_IN_BITS;
    }
    for (int i = 0; i < SIZE_OF_LONG - remaining - 1; ++i) {
      result = result | (MASK & buffer1.get());
      result = result << SIZE_OF_BYTE_IN_BITS;
    }
    result = result | (MASK & buffer1.get());
    return result;
  }

  /**
   * Split short value into two byte buffer. First byte of short will be written to first byte buffer and second to second one.
   *
   * @param buffer
   *          to write first part of value
   * @param buffer1
   *          to write second part of value
   */
  public static void splitShortToBuffers(final ByteBuffer buffer, final ByteBuffer buffer1, final short iValue) {
    buffer.put((byte) (MASK & (iValue >>> SIZE_OF_BYTE_IN_BITS)));
    buffer1.put((byte) (MASK & iValue));
  }

  /**
   * Split int value into two byte buffer. First byte of int will be written to first byte buffer and second to second one. How many
   * bytes will be written to first buffer determines based on buffer.remaining() value
   *
   * @param buffer
   *          to write first part of value
   * @param buffer1
   *          to write second part of value
   */
  public static void splitIntToBuffers(final ByteBuffer buffer, final ByteBuffer buffer1, final int iValue) {
    final int remaining = buffer.remaining();
    int i;
    for (i = 0; i < remaining; ++i) {
      buffer.put((byte) (MASK & (iValue >>> SIZE_OF_BYTE_IN_BITS * (SIZE_OF_INT - i - 1))));
    }
    for (int j = 0; j < SIZE_OF_INT - remaining; ++j) {
      buffer1.put((byte) (MASK & (iValue >>> SIZE_OF_BYTE_IN_BITS * (SIZE_OF_INT - i - j - 1))));
    }
  }

  /**
   * Split long value into two byte buffer. First byte of long will be written to first byte buffer and second to second one. How
   * many bytes will be written to first buffer determines based on buffer.remaining() value
   *
   * @param buffer
   *          to write first part of value
   * @param buffer1
   *          to write second part of value
   */
  public static void splitLongToBuffers(final ByteBuffer buffer, final ByteBuffer buffer1, final long iValue) {
    final int remaining = buffer.remaining();
    int i;
    for (i = 0; i < remaining; ++i) {
      buffer.put((byte) (iValue >> SIZE_OF_BYTE_IN_BITS * (SIZE_OF_LONG - i - 1)));
    }
    for (int j = 0; j < SIZE_OF_LONG - remaining; ++j) {
      buffer1.put((byte) (iValue >> SIZE_OF_BYTE_IN_BITS * (SIZE_OF_LONG - i - j - 1)));
    }
  }
}


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

import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OClassEmbedded;
import com.orientechnologies.orient.core.metadata.schema.OPropertyImpl;
import com.orientechnologies.orient.core.metadata.schema.OType;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * SQL CREATE PROPERTY command: Creates a new property in the target class.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 * @author Michael MacFadden
 */
@SuppressWarnings("unchecked")
public class OCommandExecutorSQLCreateProperty extends OCommandExecutorSQLAbstract implements OCommandDistributedReplicateRequest {
  public static final String KEYWORD_CREATE   = "CREATE";
  public static final String KEYWORD_PROPERTY = "PROPERTY";

  public static final String KEYWORD_MANDATORY = "MANDATORY";
  public static final String KEYWORD_READONLY  = "READONLY";
  public static final String KEYWORD_NOTNULL   = "NOTNULL";
  public static final String KEYWORD_MIN       = "MIN";
  public static final String KEYWORD_MAX       = "MAX";
  public static final String KEYWORD_DEFAULT   = "DEFAULT";

  private String className;
  private String fieldName;

  private boolean ifNotExists = false;

  private OType  type;
  private String linked;

  private boolean readonly  = false;
  private boolean mandatory = false;
  private boolean notnull   = false;

  private String max          = null;
  private String min          = null;
  private String defaultValue = null;

  private boolean unsafe = false;

  public OCommandExecutorSQLCreateProperty parse(final OCommandRequest iRequest) {

    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;

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

      init((OCommandRequestText) iRequest);

      final StringBuilder word = new StringBuilder();

      int oldPos = 0;
      int pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_CREATE))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_CREATE + " not found", parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_PROPERTY))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_PROPERTY + " not found", parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserTextUpperCase, oldPos, word, false);
      if (pos == -1)
        throw new OCommandSQLParsingException("Expected <class>.<property>", parserText, oldPos);

      String[] parts = split(word);
      if (parts.length != 2)
        throw new OCommandSQLParsingException("Expected <class>.<property>", parserText, oldPos);

      className = decodeClassName(parts[0]);
      if (className == null)
        throw new OCommandSQLParsingException("Class not found", parserText, oldPos);
      fieldName = decodeClassName(parts[1]);

      oldPos = pos;
      pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1)
        throw new OCommandSQLParsingException("Missed property type", parserText, oldPos);
      if ("IF".equalsIgnoreCase(word.toString())) {
        oldPos = pos;
        pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
        if (pos == -1)
          throw new OCommandSQLParsingException("Missed property type", parserText, oldPos);
        if (!"NOT".equalsIgnoreCase(word.toString())) {
          throw new OCommandSQLParsingException("Expected NOT EXISTS after IF", parserText, oldPos);
        }
        oldPos = pos;
        pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
        if (pos == -1)
          throw new OCommandSQLParsingException("Missed property type", parserText, oldPos);
        if (!"EXISTS".equalsIgnoreCase(word.toString())) {
          throw new OCommandSQLParsingException("Expected EXISTS after IF NOT", parserText, oldPos);
        }
        this.ifNotExists = true;

        oldPos = pos;
        pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      }

      type = OType.valueOf(word.toString());

      // Use a REGEX for the rest because we know exactly what we are looking for.
      // If we are in strict mode, the parser took care of strict matching.
      String rest = parserTextUpperCase.substring(pos).trim();
      String pattern = "(`[^`]*`|[^\\(]\\S*)?\\s*(\\(.*\\))?\\s*(UNSAFE)?";

      Pattern r = Pattern.compile(pattern);
      Matcher m = r.matcher(rest.toUpperCase(Locale.ENGLISH).trim());

      if (m.matches()) {
        // Linked Type / Class
        if (m.group(1) != null) {
          if (m.group(1).equalsIgnoreCase("UNSAFE")) {
            this.unsafe = true;
          } else {
            linked = m.group(1);
            if (linked.startsWith("`") && linked.endsWith("`") && linked.length() > 1) {
              linked = linked.substring(1, linked.length() - 1);
            }
          }
        }

        // Attributes
        if (m.group(2) != null) {
          String raw = m.group(2);
          String atts = raw.substring(1, raw.length() - 1);
          processAtts(atts);
        }

        // UNSAFE
        if (m.group(3) != null) {
          this.unsafe = true;
        }
      } else {
        // Syntax Error
      }
    } finally {
      textRequest.setText(originalQuery);
    }
    return this;
  }

  private void processAtts(String atts) {
    String[] split = atts.split(",");
    for (String attDef : split) {
      String[] parts = attDef.trim().split("\\s+");
      if (parts.length > 2) {
        onInvalidAttributeDefinition(attDef);
      }

      String att = parts[0].trim();
      if (att.equals(KEYWORD_MANDATORY)) {
        this.mandatory = getOptionalBoolean(parts);
      } else if (att.equals(KEYWORD_READONLY)) {
        this.readonly = getOptionalBoolean(parts);
      } else if (att.equals(KEYWORD_NOTNULL)) {
        this.notnull = getOptionalBoolean(parts);
      } else if (att.equals(KEYWORD_MIN)) {
        this.min = getRequiredValue(attDef, parts);
      } else if (att.equals(KEYWORD_MAX)) {
        this.max = getRequiredValue(attDef, parts);
      } else if (att.equals(KEYWORD_DEFAULT)) {
        this.defaultValue = getRequiredValue(attDef, parts);
      } else {
        onInvalidAttributeDefinition(attDef);
      }
    }
  }

  private void onInvalidAttributeDefinition(String attDef) {
    throw new OCommandSQLParsingException("Invalid attribute definition: '" + attDef + "'");
  }

  private boolean getOptionalBoolean(String[] parts) {
    if (parts.length < 2) {
      return true;
    }

    String trimmed = parts[1].trim();
    if (trimmed.length() == 0) {
      return true;
    }

    return Boolean.parseBoolean(trimmed);
  }

  private String getRequiredValue(String attDef, String[] parts) {
    if (parts.length < 2) {
      onInvalidAttributeDefinition(attDef);
    }

    String trimmed = parts[1].trim();
    if (trimmed.length() == 0) {
      onInvalidAttributeDefinition(attDef);
    }

    return trimmed;
  }

  private String[] split(StringBuilder word) {
    List<String> result = new ArrayList<String>();
    StringBuilder builder = new StringBuilder();
    boolean quoted = false;
    for (char c : word.toString().toCharArray()) {
      if (!quoted) {
        if (c == '`') {
          quoted = true;
        } else if (c == '.') {
          String nextToken = builder.toString().trim();
          if (nextToken.length() > 0) {
            result.add(nextToken);
          }
          builder = new StringBuilder();
        } else {
          builder.append(c);
        }
      } else {
        if (c == '`') {
          quoted = false;
        } else {
          builder.append(c);
        }
      }
    }
    String nextToken = builder.toString().trim();
    if (nextToken.length() > 0) {
      result.add(nextToken);
    }
    return result.toArray(new String[] {});
  }

  @Override
  public long getDistributedTimeout() {
    return getDatabase().getConfiguration().getValueAsLong(OGlobalConfiguration.DISTRIBUTED_COMMAND_QUICK_TASK_SYNCH_TIMEOUT);
  }

  /**
   * Execute the CREATE PROPERTY.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    if (type == null)
      throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet");

    final ODatabaseDocument database = getDatabase();
    final OClassEmbedded sourceClass = (OClassEmbedded) database.getMetadata().getSchema().getClass(className);
    if (sourceClass == null)
      throw new OCommandExecutionException("Source class '" + className + "' not found");

    OPropertyImpl prop = (OPropertyImpl) sourceClass.getProperty(fieldName);

    if (prop != null) {
      if (ifNotExists) {
        return sourceClass.properties().size();
      }
      throw new OCommandExecutionException(
          "Property '" + className + "." + fieldName + "' already exists. Remove it before to retry.");
    }

    // CREATE THE PROPERTY
    OClass linkedClass = null;
    OType linkedType = null;
    if (linked != null) {
      // FIRST SEARCH BETWEEN CLASSES
      linkedClass = database.getMetadata().getSchema().getClass(linked);

      if (linkedClass == null)
        // NOT FOUND: SEARCH BETWEEN TYPES
        linkedType = OType.valueOf(linked.toUpperCase(Locale.ENGLISH));
    }

    // CREATE IT LOCALLY
    OPropertyImpl internalProp = sourceClass.addPropertyInternal(fieldName, type, linkedType, linkedClass, unsafe);
    if (readonly) {
      internalProp.setReadonly(true);
    }

    if (mandatory) {
      internalProp.setMandatory(true);
    }

    if (notnull) {
      internalProp.setNotNull(true);
    }

    if (max != null) {
      internalProp.setMax(max);
    }

    if (min != null) {
      internalProp.setMin(min);
    }

    if (defaultValue != null) {
      internalProp.setDefaultValue(defaultValue);
    }

    return sourceClass.properties().size();
  }

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

  @Override
  public String getUndoCommand() {
    return "drop property " + className + "." + fieldName;
  }

  @Override
  public String getSyntax() {
    return "CREATE PROPERTY <class>.<property> [IF NOT EXISTS] <type> [<linked-type>|<linked-class>] "
        + "[(mandatory <true|false>, notnull <true|false>, <true|false>, default <value>, min <value>, max <value>)] " + "[UNSAFE]";
  }
}


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

import com.orientechnologies.common.exception.OException;
import com.orientechnologies.orient.core.command.OCommandDistributedReplicateRequest;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.config.OStorageClusterConfiguration;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.storage.OCluster;
import com.orientechnologies.orient.core.storage.OCluster.ATTRIBUTES;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * SQL ALTER PROPERTY command: Changes an attribute of an existent property in the target class.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
@SuppressWarnings("unchecked")
public class OCommandExecutorSQLAlterCluster extends OCommandExecutorSQLAbstract implements OCommandDistributedReplicateRequest {
  public static final String KEYWORD_ALTER   = "ALTER";
  public static final String KEYWORD_CLUSTER = "CLUSTER";

  protected String clusterName;
  protected int clusterId = -1;
  protected ATTRIBUTES attribute;
  protected String     value;

  public OCommandExecutorSQLAlterCluster parse(final OCommandRequest iRequest) {
    final OCommandRequestText textRequest = (OCommandRequestText) iRequest;

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

      final ODatabaseDocument database = getDatabase();

      init((OCommandRequestText) iRequest);

      StringBuilder word = new StringBuilder();

      int oldPos = 0;
      int pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_ALTER))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_ALTER + " not found. Use " + getSyntax(), parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1 || !word.toString().equals(KEYWORD_CLUSTER))
        throw new OCommandSQLParsingException("Keyword " + KEYWORD_CLUSTER + " not found. Use " + getSyntax(), parserText, oldPos);

      oldPos = pos;
      pos = nextWord(parserText, parserTextUpperCase, oldPos, word, false);
      if (pos == -1)
        throw new OCommandSQLParsingException("Expected <cluster-name>. Use " + getSyntax(), parserText, oldPos);

      clusterName = word.toString();
      clusterName = decodeClassName(clusterName);

      final Pattern p = Pattern.compile("([0-9]*)");
      final Matcher m = p.matcher(clusterName);
      if (m.matches())
        clusterId = Integer.parseInt(clusterName);

      oldPos = pos;
      pos = nextWord(parserText, parserTextUpperCase, oldPos, word, true);
      if (pos == -1)
        throw new OCommandSQLParsingException("Missing cluster attribute to change. Use " + getSyntax(), parserText, oldPos);

      final String attributeAsString = word.toString();

      try {
        attribute = OCluster.ATTRIBUTES.valueOf(attributeAsString.toUpperCase(Locale.ENGLISH));
      } catch (IllegalArgumentException e) {
        throw OException.wrapException(new OCommandSQLParsingException(
            "Unknown class attribute '" + attributeAsString + "'. Supported attributes are: " + Arrays
                .toString(OCluster.ATTRIBUTES.values()), parserText, oldPos), e);
      }

      value = parserText.substring(pos + 1).trim();

      value = decodeClassName(value);

      if (attribute == ATTRIBUTES.NAME) {
        value = value.replaceAll(" ", ""); //no spaces in cluster names
      }

      if (value.length() == 0)
        throw new OCommandSQLParsingException(
            "Missing property value to change for attribute '" + attribute + "'. Use " + getSyntax(), parserText, oldPos);

      if (value.equalsIgnoreCase("null"))
        value = null;
    } finally {
      textRequest.setText(originalQuery);
    }

    return this;
  }

  /**
   * Execute the ALTER CLASS.
   */
  public Object execute(final Map<Object, Object> iArgs) {
    if (attribute == null)
      throw new OCommandExecutionException("Cannot execute the command because it has not been parsed yet");

    final List<OCluster> clusters = getClusters();

    if (clusters.isEmpty())
      throw new OCommandExecutionException("Cluster '" + clusterName + "' not found");

    Object result = null;

    for (OCluster cluster : getClusters()) {
      if (clusterId > -1 && clusterName.equals(String.valueOf(clusterId))) {
        clusterName = cluster.getName();
      } else {
        clusterId = cluster.getId();
      }

      try {
        if (attribute == ATTRIBUTES.STATUS && OStorageClusterConfiguration.STATUS.OFFLINE.toString().equalsIgnoreCase(value))
          // REMOVE CACHE OF COMMAND RESULTS IF ACTIVE
          getDatabase().getMetadata().getCommandCache().invalidateResultsOfCluster(clusterName);

        if (attribute == ATTRIBUTES.NAME)
          // REMOVE CACHE OF COMMAND RESULTS IF ACTIVE
          getDatabase().getMetadata().getCommandCache().invalidateResultsOfCluster(clusterName);

        result = cluster.set(attribute, value);
      } catch (IOException ioe) {
        throw OException.wrapException(new OCommandExecutionException("Error altering cluster '" + clusterName + "'"), ioe);
      }
    }

    return result;
  }

  @Override
  public long getDistributedTimeout() {
    return getDatabase().getConfiguration().getValueAsLong(OGlobalConfiguration.DISTRIBUTED_COMMAND_QUICK_TASK_SYNCH_TIMEOUT);
  }

  protected List<OCluster> getClusters() {
    final ODatabaseDocumentInternal database = getDatabase();

    final List<OCluster> result = new ArrayList<OCluster>();

    if (clusterName.endsWith("*")) {
      final String toMatch = clusterName.substring(0, clusterName.length() - 1).toLowerCase(Locale.ENGLISH);
      for (String cl : database.getStorage().getClusterNames()) {
        if (cl.startsWith(toMatch))
          result.add(database.getStorage().getClusterByName(cl));
      }
    } else {
      if (clusterId > -1) {
        result.add(database.getStorage().getClusterById(clusterId));
      } else {
        result.add(database.getStorage().getClusterById(database.getStorage().getClusterIdByName(clusterName)));
      }
    }

    return result;
  }

  public String getSyntax() {
    return "ALTER CLUSTER <cluster-name>|<cluster-id> <attribute-name> <attribute-value>";
  }

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


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

import com.orientechnologies.common.listener.OProgressListener;
import com.orientechnologies.common.serialization.types.OBinarySerializer;
import com.orientechnologies.common.types.OModifiableBoolean;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OInvalidIndexEngineIdException;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;
import com.orientechnologies.orient.core.storage.impl.local.OAbstractPaginatedStorage;
import com.orientechnologies.orient.core.storage.ridbag.sbtree.OIndexRIDContainer;
import com.orientechnologies.orient.core.storage.ridbag.sbtree.OMixedIndexRIDContainer;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Fast index for full-text searches.
 *
 * @author Luca Garulli
 */
public class OIndexFullText extends OIndexMultiValues {

  private static final String  CONFIG_STOP_WORDS      = "stopWords";
  private static final String  CONFIG_SEPARATOR_CHARS = "separatorChars";
  private static final String  CONFIG_IGNORE_CHARS    = "ignoreChars";
  private static final String  CONFIG_INDEX_RADIX     = "indexRadix";
  private static final String  CONFIG_MIN_WORD_LEN    = "minWordLength";
  private static final boolean DEF_INDEX_RADIX        = true;
  private static final String  DEF_SEPARATOR_CHARS    = " \r\n\t:;,.|+*/\\=!?[]()";
  private static final String  DEF_IGNORE_CHARS       = "'\"";
  private static final String  DEF_STOP_WORDS         =
      "the in a at as and or for his her " + "him this that what which while " + "up with be was were is";
  private              boolean indexRadix;
  private              String  separatorChars;
  private              String  ignoreChars;
  private              int     minWordLength;

  private Set<String> stopWords;

  public OIndexFullText(String name, String typeId, String algorithm, int version, OAbstractPaginatedStorage storage,
      String valueContainerAlgorithm, ODocument metadata, int binaryFormatVersion) {
    super(name, typeId, algorithm, version, storage, valueContainerAlgorithm, metadata, binaryFormatVersion);
    acquireExclusiveLock();
    try {
      config();
      configWithMetadata(metadata);
    } finally {
      releaseExclusiveLock();
    }
  }

  /**
   * Indexes a value and save the index. Splits the value in single words and index each one. Save of the index is responsibility of
   * the caller.
   */
  @Override
  public OIndexFullText put(Object key, final OIdentifiable singleValue) {
    if (key == null) {
      return this;
    }

    key = getCollatingValue(key);

    final Set<String> words = splitIntoWords(key.toString());

    // FOREACH WORD CREATE THE LINK TO THE CURRENT DOCUMENT
    for (final String word : words) {
      acquireSharedLock();
      try {
        if (apiVersion == 0) {
          doPutV0(singleValue, word);
        } else if (apiVersion == 1) {
          doPutV1(singleValue, word);
        } else {
          throw new IllegalStateException("Invalid API version, " + apiVersion);
        }

      } finally {
        releaseSharedLock();
      }
    }

    return this;
  }

  private void doPutV0(OIdentifiable singleValue, String word) {
    Set<OIdentifiable> refs;
    while (true) {
      try {
        //noinspection unchecked
        refs = (Set<OIdentifiable>) storage.getIndexValue(indexId, word);
        break;
      } catch (OInvalidIndexEngineIdException ignore) {
        doReloadIndexEngine();
      }
    }

    final Set<OIdentifiable> refsc = refs;

    // SAVE THE INDEX ENTRY
    while (true) {
      try {
        storage.updateIndexEntry(indexId, word, (oldValue, bonsayFileId) -> {
          Set<OIdentifiable> result;

          if (refsc == null) {
            // WORD NOT EXISTS: CREATE THE KEYWORD CONTAINER THE FIRST TIME THE WORD IS FOUND
            if (ODefaultIndexFactory.SBTREE_BONSAI_VALUE_CONTAINER.equals(valueContainerAlgorithm)) {
              if (binaryFormatVersion >= 13) {
                result = new OMixedIndexRIDContainer(getName(), bonsayFileId);
              } else {
                result = new OIndexRIDContainer(getName(), true, bonsayFileId);
              }
            } else {
              throw new IllegalStateException("MBRBTreeContainer is not supported any more");
            }
          } else {
            result = refsc;
          }

          // ADD THE CURRENT DOCUMENT AS REF FOR THAT WORD
          result.add(singleValue);

          return OIndexUpdateAction.changed(result);
        });

        break;
      } catch (OInvalidIndexEngineIdException ignore) {
        doReloadIndexEngine();
      }
    }
  }

  private void doPutV1(OIdentifiable singleValue, String word) {
    while (true) {
      try {
        storage.putRidIndexEntry(indexId, word, singleValue.getIdentity());
        break;
      } catch (OInvalidIndexEngineIdException ignore) {
        doReloadIndexEngine();
      }
    }
  }

  /**
   * Splits passed in key on several words and remove records with keys equals to any item of split result and values equals to
   * passed in value.
   *
   * @param key   Key to remove.
   * @param value Value to remove.
   *
   * @return true if at least one record is removed.
   */
  @Override
  public boolean remove(Object key, final OIdentifiable value) {
    if (key == null) {
      return false;
    }

    key = getCollatingValue(key);

    final Set<String> words = splitIntoWords(key.toString());
    final OModifiableBoolean removed = new OModifiableBoolean(false);

    for (final String word : words) {
      acquireSharedLock();
      try {
        if (apiVersion == 0) {
          removeV0(value, removed, word);
        } else if (apiVersion == 1) {
          removeV1(value, removed, word);
        } else {
          throw new IllegalStateException("Invalid API version, " + apiVersion);
        }
      } finally {
        releaseSharedLock();
      }
    }

    return removed.getValue();
  }

  private void removeV0(OIdentifiable value, OModifiableBoolean removed, String word) {
    Set<OIdentifiable> recs;
    while (true) {
      try {
        //noinspection unchecked
        recs = (Set<OIdentifiable>) storage.getIndexValue(indexId, word);
        break;
      } catch (OInvalidIndexEngineIdException ignore) {
        doReloadIndexEngine();
      }
    }

    if (recs != null && !recs.isEmpty()) {
      while (true) {
        try {
          storage.updateIndexEntry(indexId, word, new EntityRemover(value, removed));
          break;
        } catch (OInvalidIndexEngineIdException ignore) {
          doReloadIndexEngine();
        }

      }
    }
  }

  private void removeV1(OIdentifiable value, OModifiableBoolean removed, String word) {
    while (true) {
      try {
        final boolean rm = storage.removeRidIndexEntry(indexId, word, value.getIdentity());
        removed.setValue(rm);
        break;
      } catch (OInvalidIndexEngineIdException ignore) {
        doReloadIndexEngine();
      }
    }
  }

  @Override
  public OIndexInternal<?> create(OIndexDefinition indexDefinition, String clusterIndexName, Set<String> clustersToIndex,
      boolean rebuild, OProgressListener progressListener, OBinarySerializer valueSerializer) {

    if (indexDefinition.getFields().size() > 1) {
      throw new OIndexException(type + " indexes cannot be used as composite ones.");
    }

    return super.create(indexDefinition, clusterIndexName, clustersToIndex, rebuild, progressListener, valueSerializer);
  }

  @Override
  public OIndexMultiValues create(String name, OIndexDefinition indexDefinition, String clusterIndexName,
      Set<String> clustersToIndex, boolean rebuild, OProgressListener progressListener) {
    if (indexDefinition.getFields().size() > 1) {
      throw new OIndexException(type + " indexes cannot be used as composite ones.");
    }
    return super.create(name, indexDefinition, clusterIndexName, clustersToIndex, rebuild, progressListener);
  }

  @Override
  public ODocument updateConfiguration() {
    super.updateConfiguration();
    return ((FullTextIndexConfiguration) configuration)
        .updateFullTextIndexConfiguration(separatorChars, ignoreChars, stopWords, minWordLength, indexRadix);
  }

  @Override
  protected IndexConfiguration indexConfigurationInstance(ODocument document) {
    return new FullTextIndexConfiguration(document);
  }

  public boolean canBeUsedInEqualityOperators() {
    return false;
  }

  public boolean supportsOrderedIterations() {
    return false;
  }

  private void configWithMetadata(ODocument metadata) {
    if (metadata != null) {
      if (metadata.containsField(CONFIG_IGNORE_CHARS)) {
        ignoreChars = metadata.field(CONFIG_IGNORE_CHARS);
      }

      if (metadata.containsField(CONFIG_INDEX_RADIX)) {
        indexRadix = metadata.field(CONFIG_INDEX_RADIX);
      }

      if (metadata.containsField(CONFIG_SEPARATOR_CHARS)) {
        separatorChars = metadata.field(CONFIG_SEPARATOR_CHARS);
      }

      if (metadata.containsField(CONFIG_MIN_WORD_LEN)) {
        minWordLength = metadata.field(CONFIG_MIN_WORD_LEN);
      }

      if (metadata.containsField(CONFIG_STOP_WORDS)) {
        stopWords = new HashSet<>(metadata.field(CONFIG_STOP_WORDS));
      }
    }

  }

  private void config() {
    ignoreChars = DEF_IGNORE_CHARS;
    indexRadix = DEF_INDEX_RADIX;
    separatorChars = DEF_SEPARATOR_CHARS;
    minWordLength = 3;
    stopWords = new HashSet<>(OStringSerializerHelper.split(DEF_STOP_WORDS, ' '));
  }

  private Set<String> splitIntoWords(final String iKey) {
    final Set<String> result = new HashSet<>();

    final List<String> words = new ArrayList<>();
    OStringSerializerHelper.split(words, iKey, 0, -1, separatorChars);

    final StringBuilder buffer = new StringBuilder(64);
    // FOREACH WORD CREATE THE LINK TO THE CURRENT DOCUMENT

    char c;
    boolean ignore;
    for (String word : words) {
      buffer.setLength(0);

      for (int i = 0; i < word.length(); ++i) {
        c = word.charAt(i);
        ignore = false;
        for (int k = 0; k < ignoreChars.length(); ++k) {
          if (c == ignoreChars.charAt(k)) {
            ignore = true;
            break;
          }
        }

        if (!ignore) {
          buffer.append(c);
        }
      }

      int length = buffer.length();

      while (length >= minWordLength) {
        buffer.setLength(length);
        word = buffer.toString();

        // CHECK IF IT'S A STOP WORD
        if (!stopWords.contains(word))
          // ADD THE WORD TO THE RESULT SET
        {
          result.add(word);
        }

        if (indexRadix) {
          length--;
        } else {
          break;
        }
      }
    }

    return result;
  }

  private static class EntityRemover implements OIndexKeyUpdater<Object> {
    private final OIdentifiable      value;
    private final OModifiableBoolean removed;

    EntityRemover(OIdentifiable value, OModifiableBoolean removed) {
      this.value = value;
      this.removed = removed;
    }

    @Override
    public OIndexUpdateAction<Object> update(Object old, AtomicLong bonsayFileId) {
      @SuppressWarnings("unchecked")
      Set<OIdentifiable> recs = (Set<OIdentifiable>) old;
      if (recs.remove(value)) {
        removed.setValue(true);

        if (recs.isEmpty()) {
          //noinspection unchecked
          return OIndexUpdateAction.remove();
        } else {
          return OIndexUpdateAction.changed(recs);
        }

      }

      return OIndexUpdateAction.changed(recs);
    }
  }

  private final class FullTextIndexConfiguration extends IndexConfiguration {
    FullTextIndexConfiguration(ODocument document) {
      super(document);
    }

    synchronized ODocument updateFullTextIndexConfiguration(String separatorChars, String ignoreChars, Set<String> stopWords,
        int minWordLength, boolean indexRadix) {
      document.field(CONFIG_SEPARATOR_CHARS, separatorChars);
      document.field(CONFIG_IGNORE_CHARS, ignoreChars);
      document.field(CONFIG_STOP_WORDS, stopWords);
      document.field(CONFIG_MIN_WORD_LEN, minWordLength);
      document.field(CONFIG_INDEX_RADIX, indexRadix);

      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://orientdb.com
 *
 */
package com.orientechnologies.orient.core.sql.query;

import com.orientechnologies.common.util.OCommonConst;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.db.ODatabaseDocumentInternal;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OQueryParsingException;
import com.orientechnologies.orient.core.exception.OSerializationException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.metadata.OMetadataInternal;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.query.OQueryAbstract;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.OMemoryStream;
import com.orientechnologies.orient.core.serialization.serializer.record.ORecordSerializer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * SQL query implementation.
 *
 * @param <T> Record type to return.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
@SuppressWarnings("serial")
public abstract class OSQLQuery<T> extends OQueryAbstract<T> implements OCommandRequestText {
  protected String text;

  public OSQLQuery() {
  }

  public OSQLQuery(final String iText) {
    text = iText.trim();
  }

  /**
   * Delegates to the OQueryExecutor the query execution.
   */
  @SuppressWarnings("unchecked")
  public List<T> run(final Object... iArgs) {
    final ODatabaseDocumentInternal database = ODatabaseRecordThreadLocal.instance().get();
    if (database == null)
      throw new OQueryParsingException("No database configured");

    ((OMetadataInternal) database.getMetadata()).makeThreadLocalSchemaSnapshot();
    try {
      setParameters(iArgs);
      Object o = database.getStorage().command(this);
      if (o instanceof List) {
        return (List<T>) o;
      } else {
        return (List<T>) Collections.singletonList(o);
      }

    } finally {
      ((OMetadataInternal) database.getMetadata()).clearThreadLocalSchemaSnapshot();
    }
  }

  /**
   * Returns only the first record if any.
   */
  public T runFirst(final Object... iArgs) {
    setLimit(1);
    final List<T> result = execute(iArgs);
    return result != null && !result.isEmpty() ? result.get(0) : null;
  }

  public String getText() {
    return text;
  }

  public OCommandRequestText setText(final String iText) {
    text = iText;
    return this;
  }

  @Override
  public String toString() {
    return "sql." + text;
  }

  public OCommandRequestText fromStream(final byte[] iStream, ORecordSerializer serializer) throws OSerializationException {
    final OMemoryStream buffer = new OMemoryStream(iStream);

    queryFromStream(buffer, serializer);

    return this;
  }

  public byte[] toStream() throws OSerializationException {
    return queryToStream().toByteArray();
  }

  protected OMemoryStream queryToStream() {
    final OMemoryStream buffer = new OMemoryStream();

    buffer.setUtf8(text); // TEXT AS STRING
    buffer.set(limit); // LIMIT AS INTEGER
    buffer.setUtf8(fetchPlan != null ? fetchPlan : "");

    buffer.set(serializeQueryParameters(parameters));

    return buffer;
  }

  protected void queryFromStream(final OMemoryStream buffer, ORecordSerializer serializer) {
    text = buffer.getAsString();
    limit = buffer.getAsInteger();

    setFetchPlan(buffer.getAsString());

    final byte[] paramBuffer = buffer.getAsByteArray();
    parameters = deserializeQueryParameters(paramBuffer, serializer);
  }

  protected Map<Object, Object> deserializeQueryParameters(final byte[] paramBuffer, ORecordSerializer serializer) {
    if (paramBuffer == null || paramBuffer.length == 0)
      return Collections.emptyMap();

    final ODocument param = new ODocument();

    serializer.fromStream(paramBuffer, param, null);
    param.setFieldType("params", OType.EMBEDDEDMAP);
    final Map<String, Object> params = param.rawField("params");

    final Map<Object, Object> result = new HashMap<Object, Object>();
    for (Entry<String, Object> p : params.entrySet()) {
      if (Character.isDigit(p.getKey().charAt(0)))
        result.put(Integer.parseInt(p.getKey()), p.getValue());
      else
        result.put(p.getKey(), p.getValue());
    }
    return result;
  }

  protected byte[] serializeQueryParameters(final Map<Object, Object> params) {
    if (params == null || params.size() == 0)
      // NO PARAMETER, JUST SEND 0
      return OCommonConst.EMPTY_BYTE_ARRAY;

    final ODocument param = new ODocument();
    param.field("params", convertToRIDsIfPossible(params));
    return param.toStream();
  }

  @SuppressWarnings("unchecked")
  private Map<Object, Object> convertToRIDsIfPossible(final Map<Object, Object> params) {
    final Map<Object, Object> newParams = new HashMap<Object, Object>(params.size());

    for (Entry<Object, Object> entry : params.entrySet()) {
      final Object value = entry.getValue();

      if (value instanceof Set<?> && !((Set<?>) value).isEmpty() && ((Set<?>) value).iterator().next() instanceof ORecord) {
        // CONVERT RECORDS AS RIDS
        final Set<ORID> newSet = new HashSet<ORID>();
        for (ORecord rec : (Set<ORecord>) value) {
          newSet.add(rec.getIdentity());
        }
        newParams.put(entry.getKey(), newSet);

      } else if (value instanceof List<?> && !((List<?>) value).isEmpty() && ((List<?>) value).get(0) instanceof ORecord) {
        // CONVERT RECORDS AS RIDS
        final List<ORID> newList = new ArrayList<ORID>();
        for (ORecord rec : (List<ORecord>) value) {
          newList.add(rec.getIdentity());
        }
        newParams.put(entry.getKey(), newList);

      } else if (value instanceof Map<?, ?> && !((Map<?, ?>) value).isEmpty() && ((Map<?, ?>) value).values().iterator()
          .next() instanceof ORecord) {
        // CONVERT RECORDS AS RIDS
        final Map<Object, ORID> newMap = new HashMap<Object, ORID>();
        for (Entry<?, ORecord> mapEntry : ((Map<?, ORecord>) value).entrySet()) {
          newMap.put(mapEntry.getKey(), mapEntry.getValue().getIdentity());
        }
        newParams.put(entry.getKey(), newMap);
      } else if (value instanceof OIdentifiable) {
        newParams.put(entry.getKey(), ((OIdentifiable) value).getIdentity());
      } else
        newParams.put(entry.getKey(), value);
    }

    return newParams;
  }
}


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

package com.orientechnologies.orient.core.storage.index.sbtree.local;

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

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/**
 * @author Andrey Lomakin (a.lomakin-at-orientdb.com)
 * @since 8/7/13
 */
public class OPrefixBTreeBucket<V> extends ODurablePage {
  private static final int BUCKET_PREFIX_OFFSET = NEXT_FREE_POSITION;

  private int freePointerOffset;
  private int sizeOffset;
  private int isLeafOffset;

  private int treeSizeOffset;

  private int positionsArrayOffset;

  private final boolean isLeaf;

  private final OBinarySerializer<String> keySerializer;
  private final OBinarySerializer<V>      valueSerializer;

  private final Comparator<? super Object> comparator = ODefaultComparator.INSTANCE;

  private final OEncryption encryption;
  private       String      bucketPrefix;

  OPrefixBTreeBucket(OCacheEntry cacheEntry, boolean isLeaf, OBinarySerializer<String> keySerializer,
      OBinarySerializer<V> valueSerializer, OEncryption encryption, String bucketPrefix) {
    super(cacheEntry);

    this.isLeaf = isLeaf;
    this.keySerializer = keySerializer;
    this.valueSerializer = valueSerializer;
    this.encryption = encryption;

    final int bucketPrefixSize;

    if (encryption == null) {
      final byte[] serializedPrefix = keySerializer.serializeNativeAsWhole(bucketPrefix);
      setBinaryValue(BUCKET_PREFIX_OFFSET, serializedPrefix);
      bucketPrefixSize = serializedPrefix.length;
    } else {
      final byte[] serializedPrefix = keySerializer.serializeNativeAsWhole(bucketPrefix);
      final byte[] encryptedPrefix = encryption.encrypt(serializedPrefix);
      setIntValue(BUCKET_PREFIX_OFFSET, encryptedPrefix.length);
      setBinaryValue(BUCKET_PREFIX_OFFSET + OIntegerSerializer.INT_SIZE, encryptedPrefix);
      bucketPrefixSize = encryptedPrefix.length + OIntegerSerializer.INT_SIZE;
    }

    this.bucketPrefix = bucketPrefix;

    calculateOffsets(bucketPrefixSize);

    setIntValue(freePointerOffset, MAX_PAGE_SIZE_BYTES);
    setIntValue(sizeOffset, 0);

    setByteValue(isLeafOffset, (byte) (isLeaf ? 1 : 0));

    setLongValue(treeSizeOffset, 0);
  }

  private void calculateOffsets(int bucketPrefixSize) {
    freePointerOffset = bucketPrefixSize + BUCKET_PREFIX_OFFSET + OIntegerSerializer.INT_SIZE;
    sizeOffset = freePointerOffset + OIntegerSerializer.INT_SIZE;
    isLeafOffset = sizeOffset + OIntegerSerializer.INT_SIZE;
    treeSizeOffset = isLeafOffset + OByteSerializer.BYTE_SIZE;
    positionsArrayOffset = treeSizeOffset + OLongSerializer.LONG_SIZE;
  }

  public OPrefixBTreeBucket(OCacheEntry cacheEntry, OBinarySerializer<String> keySerializer, OBinarySerializer<V> valueSerializer,
      OEncryption encryption) {
    super(cacheEntry);
    this.encryption = encryption;

    final int commonPrefixSize;
    if (encryption == null) {
      commonPrefixSize = getObjectSizeInDirectMemory(keySerializer, BUCKET_PREFIX_OFFSET);
      bucketPrefix = deserializeFromDirectMemory(keySerializer, BUCKET_PREFIX_OFFSET);
    } else {
      final int encryptedSize = getIntValue(BUCKET_PREFIX_OFFSET);
      final byte[] encryptedKey = getBinaryValue(BUCKET_PREFIX_OFFSET + OIntegerSerializer.INT_SIZE, encryptedSize);
      final byte[] serializedPrefix = encryption.decrypt(encryptedKey);

      bucketPrefix = keySerializer.deserializeNativeObject(serializedPrefix, 0);
      commonPrefixSize = encryptedSize + OIntegerSerializer.INT_SIZE;
    }

    calculateOffsets(commonPrefixSize);

    this.isLeaf = getByteValue(isLeafOffset) > 0;
    this.keySerializer = keySerializer;
    this.valueSerializer = valueSerializer;

  }

  void setTreeSize(long size) {
    setLongValue(treeSizeOffset, size);
  }

  long getTreeSize() {
    return getLongValue(treeSizeOffset);
  }

  public boolean isEmpty() {
    return size() == 0;
  }

  public String getBucketPrefix() {
    return bucketPrefix;
  }

  public int find(String key) {
    final int size = size();

    int low = 0;
    int high = size - 1;

    if (key.length() < bucketPrefix.length()) {
      return -1;
    }

    if (!key.startsWith(bucketPrefix)) {
      if (key.compareTo(bucketPrefix) > 0) {
        return -(size + 1);
      }

      return -1;
    }

    key = key.substring(bucketPrefix.length());

    while (low <= high) {
      int mid = (low + high) >>> 1;
      String midVal = getKeyWithoutPrefix(mid);
      int cmp = comparator.compare(midVal, key);

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

    return -(low + 1); // key not found.
  }

  public void remove(final int entryIndex) {
    final int entryPosition = getIntValue(positionsArrayOffset + entryIndex * OIntegerSerializer.INT_SIZE);
    final int keySize;
    if (encryption == null) {
      keySize = getObjectSizeInDirectMemory(keySerializer, entryPosition);
    } else {
      keySize = getIntValue(entryPosition) + OIntegerSerializer.INT_SIZE;
    }

    final int entrySize;
    if (isLeaf) {
      if (valueSerializer.isFixedLength()) {
        entrySize = keySize + valueSerializer.getFixedLength() + OByteSerializer.BYTE_SIZE;
      } else {
        assert getByteValue(entryPosition + keySize) == 0;
        entrySize = keySize + getObjectSizeInDirectMemory(valueSerializer, entryPosition + OByteSerializer.BYTE_SIZE + keySize)
            + OByteSerializer.BYTE_SIZE;
      }
    } else {
      throw new IllegalStateException("Remove is applies to leaf buckets only");
    }
    int size = getIntValue(sizeOffset);
    if (entryIndex < size - 1) {
      moveData(positionsArrayOffset + (entryIndex + 1) * OIntegerSerializer.INT_SIZE,
          positionsArrayOffset + entryIndex * OIntegerSerializer.INT_SIZE, (size - entryIndex - 1) * OIntegerSerializer.INT_SIZE);
    }

    size--;
    setIntValue(sizeOffset, size);

    final int freePointer = getIntValue(freePointerOffset);
    if (size > 0 && entryPosition > freePointer) {
      moveData(freePointer, freePointer + entrySize, entryPosition - freePointer);
    }

    setIntValue(freePointerOffset, freePointer + entrySize);

    int currentPositionOffset = positionsArrayOffset;

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

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

  public SBTreeEntry<V> getEntry(int entryIndex) {
    int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + positionsArrayOffset);

    if (isLeaf) {
      String key;
      if (encryption == null) {
        key = deserializeFromDirectMemory(keySerializer, entryPosition);

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

        final byte[] encryptedKey = getBinaryValue(entryPosition, encryptionSize);
        entryPosition += encryptedKey.length;

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

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

      boolean isLinkValue = getByteValue(entryPosition) > 0;
      V value;

      assert !isLinkValue;
      value = deserializeFromDirectMemory(valueSerializer, entryPosition + OByteSerializer.BYTE_SIZE);

      return new SBTreeEntry<>(-1, -1, bucketPrefix + key, new OSBTreeValue<>(false, -1, value));
    } else {
      int leftChild = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE;

      int rightChild = getIntValue(entryPosition);
      entryPosition += OIntegerSerializer.INT_SIZE;

      String key;

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

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

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

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

      return new SBTreeEntry<>(leftChild, rightChild, bucketPrefix + key, null);
    }
  }

  public int getLeft(int entryIndex) {
    assert !isLeaf;

    final int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + positionsArrayOffset);
    return getIntValue(entryPosition);
  }

  public int getRight(int entryIndex) {
    assert !isLeaf;

    final int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + positionsArrayOffset);
    return getIntValue(entryPosition + OIntegerSerializer.INT_SIZE);
  }

  /**
   * Obtains the value stored under the given entry index in this bucket.
   *
   * @param entryIndex the value entry index.
   *
   * @return the obtained value.
   */
  public OSBTreeValue<V> getValue(int entryIndex) {
    assert isLeaf;

    int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + positionsArrayOffset);

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

    boolean isLinkValue = getByteValue(entryPosition) > 0;
    long link = -1;
    V value = null;

    if (isLinkValue)
      link = deserializeFromDirectMemory(OLongSerializer.INSTANCE, entryPosition + OByteSerializer.BYTE_SIZE);
    else
      value = deserializeFromDirectMemory(valueSerializer, entryPosition + OByteSerializer.BYTE_SIZE);

    return new OSBTreeValue<>(link >= 0, link, value);
  }

  byte[] getRawValue(final int entryIndex) {
    assert isLeaf;

    int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + positionsArrayOffset);

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

    assert getByteValue(entryPosition) == 0;

    final int valueSize = getObjectSizeInDirectMemory(valueSerializer, entryPosition + OByteSerializer.BYTE_SIZE);

    return getBinaryValue(entryPosition + OByteSerializer.BYTE_SIZE, valueSize);
  }

  String getKeyWithoutPrefix(int index) {
    int entryPosition = getIntValue(index * OIntegerSerializer.INT_SIZE + positionsArrayOffset);

    if (!isLeaf) {
      entryPosition += 2 * OIntegerSerializer.INT_SIZE;
    }

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

      final byte[] encryptedKey = getBinaryValue(entryPosition, encryptedSize);
      final byte[] serializedKey = encryption.decrypt(encryptedKey);
      return keySerializer.deserializeNativeObject(serializedKey, 0);
    }
  }

  public boolean isLeaf() {
    return isLeaf;
  }

  void addAllNoPrefix(final List<byte[]> rawEntries) {
    for (int i = 0; i < rawEntries.size(); i++) {
      appendRawEntry(i, rawEntries.get(i));
    }

    setIntValue(sizeOffset, rawEntries.size());
  }

  void addAllWithPrefix(List<SBTreeEntry<V>> entries, String bucketPrefix) {
    final int bucketPrefixSize;

    if (encryption == null) {
      final byte[] serializedPrefix = keySerializer.serializeNativeAsWhole(bucketPrefix);
      setBinaryValue(BUCKET_PREFIX_OFFSET, serializedPrefix);
      bucketPrefixSize = serializedPrefix.length;
    } else {
      final byte[] serializedPrefix = keySerializer.serializeNativeAsWhole(bucketPrefix);
      final byte[] encryptedPrefix = encryption.encrypt(serializedPrefix);
      setIntValue(BUCKET_PREFIX_OFFSET, encryptedPrefix.length);
      setBinaryValue(BUCKET_PREFIX_OFFSET + OIntegerSerializer.INT_SIZE, encryptedPrefix);
      bucketPrefixSize = encryptedPrefix.length + OIntegerSerializer.INT_SIZE;
    }

    calculateOffsets(bucketPrefixSize);

    setIntValue(freePointerOffset, MAX_PAGE_SIZE_BYTES);
    setIntValue(sizeOffset, 0);

    setByteValue(isLeafOffset, (byte) (isLeaf ? 1 : 0));

    setLongValue(treeSizeOffset, 0);

    this.bucketPrefix = bucketPrefix;

    for (int i = 0; i < entries.size(); i++) {
      final SBTreeEntry<V> entry = entries.get(i);
      addEntry(i, entry, false);
    }
  }

  void shrinkWithPrefix(final int newSize, final String bucketPrefix) {
    List<SBTreeEntry<V>> treeEntries = new ArrayList<>(newSize);

    for (int i = 0; i < newSize; i++) {
      treeEntries.add(getEntry(i));
    }

    final long treeSize = getLongValue(treeSizeOffset);

    final int bucketPrefixSize;

    if (encryption == null) {
      final byte[] serializedPrefix = keySerializer.serializeNativeAsWhole(bucketPrefix);
      setBinaryValue(BUCKET_PREFIX_OFFSET, serializedPrefix);
      bucketPrefixSize = serializedPrefix.length;
    } else {
      final byte[] serializedPrefix = keySerializer.serializeNativeAsWhole(bucketPrefix);
      final byte[] encryptedPrefix = encryption.encrypt(serializedPrefix);
      setIntValue(BUCKET_PREFIX_OFFSET, encryptedPrefix.length);
      setBinaryValue(BUCKET_PREFIX_OFFSET + OIntegerSerializer.INT_SIZE, encryptedPrefix);
      bucketPrefixSize = encryptedPrefix.length + OIntegerSerializer.INT_SIZE;
    }

    calculateOffsets(bucketPrefixSize);

    this.bucketPrefix = bucketPrefix;

    setIntValue(freePointerOffset, MAX_PAGE_SIZE_BYTES);
    setIntValue(sizeOffset, 0);

    setByteValue(isLeafOffset, (byte) (isLeaf ? 1 : 0));

    setLongValue(treeSizeOffset, treeSize);

    int index = 0;
    for (SBTreeEntry<V> entry : treeEntries) {
      addEntry(index, entry, false);
      index++;
    }
  }

  byte[] getRawEntry(final int entryIndex) {
    int entryPosition = getIntValue(entryIndex * OIntegerSerializer.INT_SIZE + positionsArrayOffset);
    final int startEntryPosition = entryPosition;

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

      entryPosition += keySize;

      assert getByteValue(entryPosition) == 0;

      final int valueSize = getObjectSizeInDirectMemory(valueSerializer, entryPosition + OByteSerializer.BYTE_SIZE);

      return getBinaryValue(startEntryPosition, keySize + valueSize + OByteSerializer.BYTE_SIZE);
    } else {
      entryPosition += 2 * OIntegerSerializer.INT_SIZE;

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

      return getBinaryValue(startEntryPosition, keySize + 2 * OIntegerSerializer.INT_SIZE);
    }
  }

  private void appendRawEntry(final int index, final byte[] rawEntry) {
    int freePointer = getIntValue(freePointerOffset);
    freePointer -= rawEntry.length;

    setIntValue(freePointerOffset, freePointer);
    setIntValue(positionsArrayOffset + index * OIntegerSerializer.INT_SIZE, freePointer);

    setBinaryValue(freePointer, rawEntry);
  }

  public boolean addEntry(int index, SBTreeEntry<V> treeEntry, boolean updateNeighbors) {
    assert treeEntry.key.startsWith(bucketPrefix);
    final String key = treeEntry.key.substring(bucketPrefix.length());

    final byte[] serializedKey = keySerializer.serializeNativeAsWhole(key, OType.STRING);
    final int keySize;
    byte[] encryptedKey = null;

    if (encryption == null) {
      keySize = serializedKey.length;
    } else {
      encryptedKey = encryption.encrypt(serializedKey);
      keySize = encryptedKey.length + OIntegerSerializer.INT_SIZE;
    }

    int valueSize = 0;
    int entrySize = keySize;

    if (isLeaf) {
      if (valueSerializer.isFixedLength()) {
        valueSize = valueSerializer.getFixedLength();
      } else {
        valueSize = valueSerializer.getObjectSize(treeEntry.value.getValue());
      }

      entrySize += valueSize + OByteSerializer.BYTE_SIZE;
    } else {
      entrySize += 2 * OIntegerSerializer.INT_SIZE;
    }

    int size = size();
    int freePointer = getIntValue(freePointerOffset);

    if (freePointer - entrySize < (size + 1) * OIntegerSerializer.INT_SIZE + positionsArrayOffset) {
      return false;
    }

    if (index <= size - 1) {
      moveData(positionsArrayOffset + index * OIntegerSerializer.INT_SIZE,
          positionsArrayOffset + (index + 1) * OIntegerSerializer.INT_SIZE, (size - index) * OIntegerSerializer.INT_SIZE);
    }

    freePointer -= entrySize;

    setIntValue(freePointerOffset, freePointer);
    setIntValue(positionsArrayOffset + index * OIntegerSerializer.INT_SIZE, freePointer);
    setIntValue(sizeOffset, size + 1);

    if (isLeaf) {
      if (encryption == null) {
        freePointer += setBinaryValue(freePointer, serializedKey);
      } else {
        setIntValue(freePointer, encryptedKey.length);
        freePointer += OIntegerSerializer.INT_SIZE;

        freePointer += setBinaryValue(freePointer, encryptedKey);
      }

      freePointer += setByteValue(freePointer, treeEntry.value.isLink() ? (byte) 1 : (byte) 0);

      byte[] serializedValue = valueSerializer.serializeNativeAsWhole(treeEntry.value.getValue());

      setBinaryValue(freePointer, serializedValue);
    } else {
      freePointer += setIntValue(freePointer, treeEntry.leftChild);
      freePointer += setIntValue(freePointer, treeEntry.rightChild);

      if (encryption == null) {
        setBinaryValue(freePointer, serializedKey);
      } else {
        setIntValue(freePointer, encryptedKey.length);
        freePointer += OIntegerSerializer.INT_SIZE;

        setBinaryValue(freePointer, encryptedKey);
      }

      size++;

      if (updateNeighbors && size > 1) {
        if (index < size - 1) {
          final int nextEntryPosition = getIntValue(positionsArrayOffset + (index + 1) * OIntegerSerializer.INT_SIZE);
          setIntValue(nextEntryPosition, treeEntry.rightChild);
        }

        if (index > 0) {
          final int prevEntryPosition = getIntValue(positionsArrayOffset + (index - 1) * OIntegerSerializer.INT_SIZE);
          setIntValue(prevEntryPosition + OIntegerSerializer.INT_SIZE, treeEntry.leftChild);
        }
      }
    }

    return true;
  }

  void updateValue(final int index, final byte[] value) {
    int entryPosition = getIntValue(index * OIntegerSerializer.INT_SIZE + positionsArrayOffset);

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

    assert getByteValue(entryPosition) == 0;

    entryPosition += OByteSerializer.BYTE_SIZE;

    setBinaryValue(entryPosition, value);
  }

  public static final class SBTreeEntry<V> implements Comparable<SBTreeEntry<V>> {
    private final Comparator<? super String> comparator = ODefaultComparator.INSTANCE;

    final        int             leftChild;
    final        int             rightChild;
    public       String          key;
    public final OSBTreeValue<V> value;

    public SBTreeEntry(int leftChild, int rightChild, String key, OSBTreeValue<V> value) {
      this.leftChild = leftChild;
      this.rightChild = rightChild;
      this.key = key;
      this.value = value;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o)
        return true;
      if (o == null || getClass() != o.getClass())
        return false;
      SBTreeEntry<?> that = (SBTreeEntry<?>) o;
      return leftChild == that.leftChild && rightChild == that.rightChild && Objects.equals(comparator, that.comparator) && Objects
          .equals(key, that.key) && Objects.equals(value, that.value);
    }

    @Override
    public int hashCode() {
      return Objects.hash(comparator, leftChild, rightChild, key, value);
    }

    @Override
    public String toString() {
      return "SBTreeEntry{" + "leftChild=" + leftChild + ", rightChild=" + rightChild + ", key=" + key + ", value=" + value + '}';
    }

    @Override
    public int compareTo(SBTreeEntry<V> other) {
      return comparator.compare(key, other.key);
    }
  }
}