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

import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.util.OCallable;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.record.OElement;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.OJSONWriter;
import com.orientechnologies.orient.core.sql.executor.OResult;
import com.orientechnologies.orient.server.OClientConnection;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.net.Socket;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.zip.GZIPOutputStream;

/**
 * Maintains information about current HTTP response.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class OHttpResponse {
  public static final  String  JSON_FORMAT   = "type,indent:-1,rid,version,attribSameRow,class,keepTypes,alwaysFetchEmbeddedDocuments";
  public static final  char[]  URL_SEPARATOR = { '/' };
  private static final Charset utf8          = Charset.forName("utf8");

  public final  String       httpVersion;
  private final OutputStream out;
  public        String       headers;
  public        String[]     additionalHeaders;
  public        String       characterSet;
  public        String       contentType;
  public        String       serverInfo;

  public String sessionId;
  public String callbackFunction;
  public String contentEncoding;
  public String staticEncoding;
  public boolean sendStarted = false;
  public String content;
  public int    code;
  public boolean keepAlive         = true;
  public boolean jsonErrorResponse = true;
  public OClientConnection connection;
  private boolean streaming = OGlobalConfiguration.NETWORK_HTTP_STREAMING.getValueAsBoolean();

  public OHttpResponse(final OutputStream iOutStream, final String iHttpVersion, final String[] iAdditionalHeaders,
      final String iResponseCharSet, final String iServerInfo, final String iSessionId, final String iCallbackFunction,
      final boolean iKeepAlive, OClientConnection connection) {
    streaming = connection.getProtocol().getServer().getContextConfiguration()
        .getValueAsBoolean(OGlobalConfiguration.NETWORK_HTTP_STREAMING);
    out = iOutStream;
    httpVersion = iHttpVersion;
    additionalHeaders = iAdditionalHeaders;
    characterSet = iResponseCharSet;
    serverInfo = iServerInfo;
    sessionId = iSessionId;
    callbackFunction = iCallbackFunction;
    keepAlive = iKeepAlive;
    this.connection = connection;
  }

  public void send(final int iCode, final String iReason, final String iContentType, final Object iContent, final String iHeaders)
      throws IOException {
    if (sendStarted) {
      // AVOID TO SEND RESPONSE TWICE
      return;
    }
    sendStarted = true;

    if (callbackFunction != null) {
      content = callbackFunction + "(" + iContent + ")";
      contentType = "text/javascript";
    } else {
      if (content == null || content.length() == 0) {
        content = iContent != null ? iContent.toString() : null;
      }
      if (contentType == null || contentType.length() == 0) {
        contentType = iContentType;
      }
    }

    final boolean empty = content == null || content.length() == 0;

    if (this.code > 0) {
      writeStatus(this.code, iReason);
    } else {
      writeStatus(empty && iCode == 200 ? 204 : iCode, iReason);
    }
    writeHeaders(contentType, keepAlive);

    if (iHeaders != null) {
      writeLine(iHeaders);
    }

    if (sessionId != null)
      writeLine("Set-Cookie: " + OHttpUtils.OSESSIONID + "=" + sessionId + "; Path=/; HttpOnly");

    byte[] binaryContent = null;
    if (!empty) {
      if (contentEncoding != null && contentEncoding.equals(OHttpUtils.CONTENT_ACCEPT_GZIP_ENCODED)) {
        binaryContent = compress(content);
      } else {
        binaryContent = content.getBytes(utf8);
      }
    }

    writeLine(OHttpUtils.HEADER_CONTENT_LENGTH + (empty ? 0 : binaryContent.length));

    writeLine(null);

    if (binaryContent != null) {
      out.write(binaryContent);
    }

    flush();
  }

  public void writeStatus(final int iStatus, final String iReason) throws IOException {
    writeLine(httpVersion + " " + iStatus + " " + iReason);
  }

  public void writeHeaders(final String iContentType) throws IOException {
    writeHeaders(iContentType, true);
  }

  public void writeHeaders(final String iContentType, final boolean iKeepAlive) throws IOException {
    if (headers != null) {
      writeLine(headers);
    }

    // Set up a date formatter that prints the date in the Http-date format as
    // per RFC 7231, section 7.1.1.1
    SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
    sdf.setTimeZone(TimeZone.getTimeZone("UTC"));

    writeLine("Date: " + sdf.format(new Date()));
    writeLine("Content-Type: " + iContentType + "; charset=" + characterSet);
    writeLine("Server: " + serverInfo);
    writeLine("Connection: " + (iKeepAlive ? "Keep-Alive" : "close"));

    // SET CONTENT ENCDOING
    if (contentEncoding != null && contentEncoding.length() > 0) {
      writeLine("Content-Encoding: " + contentEncoding);
    }

    // INCLUDE COMMON CUSTOM HEADERS
    if (additionalHeaders != null) {
      for (String h : additionalHeaders) {
        writeLine(h);
      }
    }
  }

  public void writeLine(final String iContent) throws IOException {
    writeContent(iContent);
    out.write(OHttpUtils.EOL);
  }

  public void writeContent(final String iContent) throws IOException {
    if (iContent != null) {
      out.write(iContent.getBytes(utf8));
    }
  }

  public void writeResult(Object iResult) throws InterruptedException, IOException {
    writeResult(iResult, null, null, null);
  }

  public void writeResult(Object iResult, final String iFormat, final String iAccept) throws InterruptedException, IOException {
    writeResult(iResult, iFormat, iAccept, null);
  }

  public void writeResult(Object iResult, final String iFormat, final String iAccept,
      final Map<String, Object> iAdditionalProperties) throws InterruptedException, IOException {
    writeResult(iResult, iFormat, iAccept, iAdditionalProperties, null);
  }

  public void writeResult(Object iResult, final String iFormat, final String iAccept,
      final Map<String, Object> iAdditionalProperties, final String mode) throws InterruptedException, IOException {
    if (iResult == null) {
      send(OHttpUtils.STATUS_OK_NOCONTENT_CODE, "", OHttpUtils.CONTENT_TEXT_PLAIN, null, null);
    } else {
      final Object newResult;

      if (iResult instanceof Map) {
        ODocument doc = new ODocument();
        for (Map.Entry<?, ?> entry : ((Map<?, ?>) iResult).entrySet()) {
          String key = keyFromMapObject(entry.getKey());
          doc.field(key, entry.getValue());
        }
        newResult = Collections.singleton(doc).iterator();
      } else if (OMultiValue.isMultiValue(iResult) && (OMultiValue.getSize(iResult) > 0 && !(
          (OMultiValue.getFirstValue(iResult) instanceof OIdentifiable) || ((OMultiValue
              .getFirstValue(iResult) instanceof OResult))))) {
        newResult = Collections.singleton(new ODocument().field("value", iResult)).iterator();
      } else if (iResult instanceof OIdentifiable) {
        // CONVERT SINGLE VALUE IN A COLLECTION
        newResult = Collections.singleton(iResult).iterator();
      } else if (iResult instanceof Iterable<?>) {
        newResult = ((Iterable<OIdentifiable>) iResult).iterator();
      } else if (OMultiValue.isMultiValue(iResult)) {
        newResult = OMultiValue.getMultiValueIterator(iResult);
      } else {
        newResult = Collections.singleton(new ODocument().field("value", iResult)).iterator();
      }

      if (newResult == null) {
        send(OHttpUtils.STATUS_OK_NOCONTENT_CODE, "", OHttpUtils.CONTENT_TEXT_PLAIN, null, null);
      } else {
        writeRecords(newResult, null, iFormat, iAccept, iAdditionalProperties, mode);
      }
    }
  }

  public void writeRecords(final Object iRecords) throws IOException {
    writeRecords(iRecords, null, null, null, null);
  }

  public void writeRecords(final Object iRecords, final String iFetchPlan) throws IOException {
    writeRecords(iRecords, iFetchPlan, null, null, null);
  }

  public void writeRecords(final Object iRecords, final String iFetchPlan, String iFormat, final String accept) throws IOException {
    writeRecords(iRecords, iFetchPlan, iFormat, accept, null);
  }

  public void writeRecords(final Object iRecords, final String iFetchPlan, String iFormat, final String accept,
      final Map<String, Object> iAdditionalProperties) throws IOException {
    writeRecords(iRecords, iFetchPlan, iFormat, accept, iAdditionalProperties, null);
  }

  public void writeRecords(final Object iRecords, final String iFetchPlan, String iFormat, final String accept,
      final Map<String, Object> iAdditionalProperties, final String mode) throws IOException {
    if (iRecords == null) {
      send(OHttpUtils.STATUS_OK_NOCONTENT_CODE, "", OHttpUtils.CONTENT_TEXT_PLAIN, null, null);
      return;
    }
    final int size = OMultiValue.getSize(iRecords);
    final Iterator<Object> it = OMultiValue.getMultiValueIterator(iRecords);

    if (accept != null && accept.contains("text/csv")) {
      sendStream(OHttpUtils.STATUS_OK_CODE, OHttpUtils.STATUS_OK_DESCRIPTION, OHttpUtils.CONTENT_CSV, "data.csv",
          new OCallable<Void, OChunkedResponse>() {

            @Override
            public Void call(final OChunkedResponse iArgument) {
              final LinkedHashSet<String> colNames = new LinkedHashSet<String>();
              final List<OElement> records = new ArrayList<OElement>();

              // BROWSE ALL THE RECORD TO HAVE THE COMPLETE COLUMN
              // NAMES LIST
              while (it.hasNext()) {
                final Object r = it.next();

                if (r instanceof OResult) {

                  OResult result = (OResult) r;
                  records.add(result.toElement());

                  for (String fieldName : result.getPropertyNames()) {
                    colNames.add(fieldName);
                  }

                } else if (r != null && r instanceof OIdentifiable) {
                  final ORecord rec = ((OIdentifiable) r).getRecord();
                  if (rec != null) {
                    if (rec instanceof ODocument) {
                      final ODocument doc = (ODocument) rec;
                      records.add(doc);

                      for (String fieldName : doc.fieldNames()) {
                        colNames.add(fieldName);
                      }
                    }
                  }
                }
              }

              final List<String> orderedColumns = new ArrayList<String>(colNames);

              try {
                // WRITE THE HEADER
                for (int col = 0; col < orderedColumns.size(); ++col) {
                  if (col > 0)
                    iArgument.write(',');

                  iArgument.write(orderedColumns.get(col).getBytes());
                }
                iArgument.write(OHttpUtils.EOL);

                // WRITE EACH RECORD
                for (OElement doc : records) {
                  for (int col = 0; col < orderedColumns.size(); ++col) {
                    if (col > 0) {
                      iArgument.write(',');
                    }

                    Object value = doc.getProperty(orderedColumns.get(col));
                    if (value != null) {
                      if (!(value instanceof Number))
                        value = "\"" + value + "\"";

                      iArgument.write(value.toString().getBytes());
                    }
                  }
                  iArgument.write(OHttpUtils.EOL);
                }

                iArgument.flush();

              } catch (IOException e) {
                OLogManager.instance().error(this, "HTTP response: error on writing records", e);
              }

              return null;
            }
          });
    } else {
      if (iFormat == null)
        iFormat = JSON_FORMAT;
      else
        iFormat = JSON_FORMAT + "," + iFormat;

      final String sendFormat = iFormat;
      if (streaming) {
        sendStream(OHttpUtils.STATUS_OK_CODE, OHttpUtils.STATUS_OK_DESCRIPTION, OHttpUtils.CONTENT_JSON, null, iArgument -> {
          try {
            OutputStreamWriter writer = new OutputStreamWriter(iArgument);
            writeRecordsOnStream(iFetchPlan, sendFormat, iAdditionalProperties, it, writer);
            writer.flush();
          } catch (IOException e) {
            OLogManager.instance().error(this, "Error during writing of records to the HTTP response", e);
          }
          return null;
        });
      } else {
        final StringWriter buffer = new StringWriter();
        writeRecordsOnStream(iFetchPlan, iFormat, iAdditionalProperties, it, buffer);
        send(OHttpUtils.STATUS_OK_CODE, OHttpUtils.STATUS_OK_DESCRIPTION, OHttpUtils.CONTENT_JSON, buffer.toString(), null);
      }
    }
  }

  private void writeRecordsOnStream(String iFetchPlan, String iFormat, Map<String, Object> iAdditionalProperties,
      Iterator<Object> it, Writer buffer) throws IOException {
    final OJSONWriter json = new OJSONWriter(buffer, iFormat);
    json.beginObject();

    final String format = iFetchPlan != null ? iFormat + ",fetchPlan:" + iFetchPlan : iFormat;

    // WRITE RECORDS
    json.beginCollection(-1, true, "result");
    formatMultiValue(it, buffer, format);
    json.endCollection(-1, true);

    if (iAdditionalProperties != null) {
      for (Map.Entry<String, Object> entry : iAdditionalProperties.entrySet()) {

        final Object v = entry.getValue();
        if (OMultiValue.isMultiValue(v)) {
          json.beginCollection(-1, true, entry.getKey());
          formatMultiValue(OMultiValue.getMultiValueIterator(v), buffer, format);
          json.endCollection(-1, true);
        } else
          json.writeAttribute(entry.getKey(), v);

        if (Thread.currentThread().isInterrupted())
          break;

      }
    }

    json.endObject();
  }

  private void checkConnection() throws IOException {
    final Socket socket;
    if (connection.getProtocol() == null || connection.getProtocol().getChannel() == null)
      socket = null;
    else
      socket = connection.getProtocol().getChannel().socket;
    if (socket == null || socket.isClosed() || socket.isInputShutdown()) {
      OLogManager.instance().debug(this, "[OHttpResponse] found and removed pending closed channel %d (%s)", connection, socket);
      throw new IOException("Connection is closed");
    }
  }

  public void formatMultiValue(final Iterator<?> iIterator, final Writer buffer, final String format) throws IOException {
    if (iIterator != null) {
      int counter = 0;
      String objectJson;

      while (iIterator.hasNext()) {
        final Object entry = iIterator.next();
        if (entry != null) {
          if (counter++ > 0) {
            buffer.append(", ");
          }

          if (entry instanceof OResult) {
            objectJson = ((OResult) entry).toJSON();
            buffer.append(objectJson);
          } else if (entry instanceof OIdentifiable) {
            ORecord rec = ((OIdentifiable) entry).getRecord();
            if (rec != null) {
              try {
                objectJson = rec.toJSON(format);

                buffer.append(objectJson);
              } catch (Exception e) {
                OLogManager.instance().error(this, "Error transforming record " + rec.getIdentity() + " to JSON", e);
              }
            }
          } else if (OMultiValue.isMultiValue(entry)) {
            buffer.append("[");
            formatMultiValue(OMultiValue.getMultiValueIterator(entry), buffer, format);
            buffer.append("]");
          } else {
            buffer.append(OJSONWriter.writeValue(entry, format));
          }
        }
        checkConnection();
      }
    }
  }

  public void writeRecord(final ORecord iRecord) throws IOException {
    writeRecord(iRecord, null, null);
  }

  public void writeRecord(final ORecord iRecord, final String iFetchPlan, String iFormat) throws IOException {
    if (iFormat == null) {
      iFormat = JSON_FORMAT;
    }

    final String format = iFetchPlan != null ? iFormat + ",fetchPlan:" + iFetchPlan : iFormat;
    if (iRecord != null) {
      send(OHttpUtils.STATUS_OK_CODE, OHttpUtils.STATUS_OK_DESCRIPTION, OHttpUtils.CONTENT_JSON, iRecord.toJSON(format),
          OHttpUtils.HEADER_ETAG + iRecord.getVersion());
    }
  }

  public void sendStream(final int iCode, final String iReason, final String iContentType, InputStream iContent, long iSize)
      throws IOException {
    sendStream(iCode, iReason, iContentType, iContent, iSize, null, null);
  }

  public void sendStream(final int iCode, final String iReason, final String iContentType, InputStream iContent, long iSize,
      final String iFileName) throws IOException {
    sendStream(iCode, iReason, iContentType, iContent, iSize, iFileName, null);
  }

  public void sendStream(final int iCode, final String iReason, final String iContentType, InputStream iContent, long iSize,
      final String iFileName, Map<String, String> additionalHeaders) throws IOException {
    writeStatus(iCode, iReason);
    writeHeaders(iContentType);
    writeLine("Content-Transfer-Encoding: binary");

    if (iFileName != null) {
      writeLine("Content-Disposition: attachment; filename=\"" + iFileName + "\"");
    }

    if (additionalHeaders != null) {
      for (Map.Entry<String, String> entry : additionalHeaders.entrySet()) {
        writeLine(String.format("%s: %s", entry.getKey(), entry.getValue()));
      }
    }
    if (iSize < 0) {
      // SIZE UNKNOWN: USE A MEMORY BUFFER
      final ByteArrayOutputStream o = new ByteArrayOutputStream();
      if (iContent != null) {
        int b;
        while ((b = iContent.read()) > -1) {
          o.write(b);
        }
      }

      byte[] content = o.toByteArray();

      iContent = new ByteArrayInputStream(content);
      iSize = content.length;
    }

    writeLine(OHttpUtils.HEADER_CONTENT_LENGTH + (iSize));
    writeLine(null);

    if (iContent != null) {
      int b;
      while ((b = iContent.read()) > -1) {
        out.write(b);
      }
    }

    flush();
  }

  public void sendStream(final int iCode, final String iReason, final String iContentType, final String iFileName,
      final OCallable<Void, OChunkedResponse> iWriter) throws IOException {
    writeStatus(iCode, iReason);
    writeHeaders(iContentType);
    writeLine("Content-Transfer-Encoding: binary");
    writeLine("Transfer-Encoding: chunked");

    if (iFileName != null) {
      writeLine("Content-Disposition: attachment; filename=\"" + iFileName + "\"");
    }

    writeLine(null);

    final OChunkedResponse chunkedOutput = new OChunkedResponse(this);
    iWriter.call(chunkedOutput);
    chunkedOutput.close();

    flush();
  }

  // Compress content string
  public byte[] compress(String jsonStr) {
    if (jsonStr == null || jsonStr.length() == 0) {
      return null;
    }
    GZIPOutputStream gout = null;
    ByteArrayOutputStream baos = null;
    try {
      byte[] incoming = jsonStr.getBytes("UTF-8");
      baos = new ByteArrayOutputStream();
      gout = new GZIPOutputStream(baos, 16384); // 16KB
      gout.write(incoming);
      gout.finish();
      return baos.toByteArray();
    } catch (Exception ex) {
      OLogManager.instance().error(this, "Error on compressing HTTP response", ex);
    } finally {
      try {
        if (gout != null) {
          gout.close();
        }
        if (baos != null) {
          baos.close();
        }
      } catch (Exception ex) {
      }
    }
    return null;
  }

  /**
   * Stores additional headers to send
   */
  public void setHeader(final String iHeader) {
    headers = iHeader;
  }

  public OutputStream getOutputStream() {
    return out;
  }

  public void flush() throws IOException {
    out.flush();
    if (!keepAlive) {
      out.close();
    }
  }

  public String getContentType() {
    return contentType;
  }

  public void setContentType(String contentType) {
    this.contentType = contentType;
  }

  public String getContentEncoding() {
    return contentEncoding;
  }

  public void setContentEncoding(String contentEncoding) {
    this.contentEncoding = contentEncoding;
  }

  public void setStaticEncoding(String contentEncoding) {
    this.staticEncoding = contentEncoding;
  }

  public void setSessionId(String sessionId) {
    this.sessionId = sessionId;
  }

  public String getContent() {
    return content;
  }

  public void setContent(String content) {
    this.content = content;
  }

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }

  public void setJsonErrorResponse(boolean jsonErrorResponse) {
    this.jsonErrorResponse = jsonErrorResponse;
  }

  private String keyFromMapObject(Object key) {
    if (key instanceof String) {
      return (String) key;
    }
    return "" + key;
  }

  public void setStreaming(boolean streaming) {
    this.streaming = streaming;
  }

}


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

import java.util.List;

import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

@XmlRootElement(name = "orient-server")
public class OServerConfiguration {
  public static final String                        FILE_NAME            = "server-config.xml";
  // private static final String HEADER = "OrientDB Server configuration";
  public static final OServerStorageConfiguration[] EMPTY_CONFIG_ARRAY   = new OServerStorageConfiguration[0];
  @XmlTransient
  public String                                     location;

  @XmlElementWrapper
  @XmlElementRef(type = OServerHandlerConfiguration.class)
  public List<OServerHandlerConfiguration>          handlers;

  @XmlElementWrapper
  @XmlElementRef(type = OServerHookConfiguration.class)
  public List<OServerHookConfiguration>             hooks;

  @XmlElementRef(type = OServerNetworkConfiguration.class)
  public OServerNetworkConfiguration                network;

  @XmlElementWrapper
  @XmlElementRef(type = OServerStorageConfiguration.class)
  public OServerStorageConfiguration[]              storages;

  @XmlElementWrapper(required = false)
  @XmlElementRef(type = OServerUserConfiguration.class)
  public OServerUserConfiguration[]                 users;

  @XmlElementRef(type = OServerSecurityConfiguration.class)
  public OServerSecurityConfiguration               security;

  @XmlElementWrapper
  @XmlElementRef(type = OServerEntryConfiguration.class)
  public OServerEntryConfiguration[]                properties;

  public boolean                                    isAfterFirstTime;

  public static final String                        DEFAULT_CONFIG_FILE  = "config/orientdb-server-config.xml";

  public static final String                        PROPERTY_CONFIG_FILE = "orientdb.config.file";

  public static final String                        DEFAULT_ROOT_USER    = "root";
  public static final String                        GUEST_USER           = "guest";
  public static final String                        GUEST_PASS           = "guest";

  /**
   * Empty constructor for JAXB
   */
  public OServerConfiguration() {
  }

  public OServerConfiguration(OServerConfigurationLoaderXml iFactory) {
    location = FILE_NAME;
    network = new OServerNetworkConfiguration(iFactory);
    storages = EMPTY_CONFIG_ARRAY;
    security = new OServerSecurityConfiguration(iFactory);
  }

  public String getStoragePath(String iURL) {
    if (storages != null)
      for (OServerStorageConfiguration stg : storages)
        if (stg.name.equals(iURL))
          return stg.path;

    return null;
  }

  /**
   * Returns the property value configured, if any.
   *
   * @param iName
   *          Property name to find
   */
  public String getProperty(final String iName) {
    return getProperty(iName, null);
  }

  /**
   * Returns the property value configured, if any.
   *
   * @param iName
   *          Property name to find
   * @param iDefaultValue
   *          Default value returned if not found
   */
  public String getProperty(final String iName, final String iDefaultValue) {
    if (properties == null)
      return null;

    for (OServerEntryConfiguration p : properties) {
      if (p.name.equals(iName))
        return p.value;
    }

    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.object.db;

import com.orientechnologies.common.collection.OMultiValue;
import com.orientechnologies.common.concur.ONeedRetryException;
import com.orientechnologies.common.concur.lock.OLockException;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.util.OCommonConst;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.command.script.OCommandScriptException;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.conflict.ORecordConflictStrategy;
import com.orientechnologies.orient.core.db.*;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.db.object.ODatabaseObject;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.db.record.ORecordElement;
import com.orientechnologies.orient.core.db.record.ORecordOperation;
import com.orientechnologies.orient.core.dictionary.ODictionary;
import com.orientechnologies.orient.core.entity.OEntityManager;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.exception.OSerializationException;
import com.orientechnologies.orient.core.exception.OTransactionException;
import com.orientechnologies.orient.core.hook.ORecordHook;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.metadata.OMetadataInternal;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.metadata.security.*;
import com.orientechnologies.orient.core.query.OQuery;
import com.orientechnologies.orient.core.record.OElement;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordInternal;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.record.OSerializationThreadLocal;
import com.orientechnologies.orient.core.sql.OCommandSQLParsingException;
import com.orientechnologies.orient.core.sql.executor.OResult;
import com.orientechnologies.orient.core.sql.executor.OResultSet;
import com.orientechnologies.orient.core.storage.ORecordCallback;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.tx.OTransaction;
import com.orientechnologies.orient.core.tx.OTransactionNoTx;
import com.orientechnologies.orient.object.dictionary.ODictionaryWrapper;
import com.orientechnologies.orient.object.enhancement.OObjectEntityEnhancer;
import com.orientechnologies.orient.object.enhancement.OObjectEntitySerializer;
import com.orientechnologies.orient.object.enhancement.OObjectMethodFilter;
import com.orientechnologies.orient.object.enhancement.OObjectProxyMethodHandler;
import com.orientechnologies.orient.object.entity.OObjectEntityClassHandler;
import com.orientechnologies.orient.object.iterator.OObjectIteratorClass;
import com.orientechnologies.orient.object.iterator.OObjectIteratorCluster;
import com.orientechnologies.orient.object.metadata.OMetadataObject;
import com.orientechnologies.orient.object.serialization.OObjectSerializerHelper;
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyObject;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * Object Database instance. It's a wrapper to the class ODatabaseDocumentTx that handles conversion between ODocument instances and
 * POJOs using javassist APIs.
 *
 * @author Luca Molino
 * @see ODatabaseDocumentTx
 */
@SuppressWarnings("unchecked")
public class OObjectDatabaseTx extends ODatabaseWrapperAbstract<ODatabaseDocumentInternal, Object> implements ODatabaseObject {

  public static final String              TYPE = "object";
  protected           ODictionary<Object> dictionary;
  protected           OEntityManager      entityManager;
  protected           boolean             saveOnlyDirty;
  protected           boolean             lazyLoading;
  protected           boolean             automaticSchemaGeneration;
  protected           OMetadataObject     metadata;

  public OObjectDatabaseTx(final String iURL) {
    super(new ODatabaseDocumentTx(iURL));
    underlying.setDatabaseOwner(this);
    init();
  }

  /**
   * Constructor to wrap an existing database connect for object connections
   *
   * @param iDatabase an open database connection
   */
  public OObjectDatabaseTx(ODatabaseDocumentInternal iDatabase) {
    super(iDatabase);
    underlying.setDatabaseOwner(this);
    init();
  }

  public <T> T newInstance(final Class<T> iType) {
    return (T) newInstance(iType.getSimpleName(), null, OCommonConst.EMPTY_OBJECT_ARRAY);
  }

  public <T> T newInstance(final Class<T> iType, Object... iArgs) {
    return (T) newInstance(iType.getSimpleName(), null, iArgs);
  }

  public <RET> RET newInstance(String iClassName) {
    return (RET) newInstance(iClassName, null, OCommonConst.EMPTY_OBJECT_ARRAY);
  }

  @Override
  public <THISDB extends ODatabase> THISDB open(String iUserName, String iUserPassword) {
    super.open(iUserName, iUserPassword);
    saveOnlyDirty = getConfiguration().getValueAsBoolean(OGlobalConfiguration.OBJECT_SAVE_ONLY_DIRTY);

    entityManager.registerEntityClass(OUser.class);
    entityManager.registerEntityClass(ORole.class);
    metadata = new OMetadataObject((OMetadataInternal) underlying.getMetadata());
    this.registerFieldMappingStrategy();
    return (THISDB) this;
  }

  @Override
  public <THISDB extends ODatabase> THISDB open(OToken iToken) {
    super.open(iToken);
    entityManager.registerEntityClass(OUser.class);
    entityManager.registerEntityClass(ORole.class);
    metadata = new OMetadataObject((OMetadataInternal) underlying.getMetadata());
    this.registerFieldMappingStrategy();
    return (THISDB) this;
  }

  public OSecurityUser getUser() {
    return underlying.getUser();
  }

  public void setUser(OSecurityUser user) {
    underlying.setUser(user);
  }

  @Override
  public OMetadataObject getMetadata() {
    checkOpenness();
    if (metadata == null)
      metadata = new OMetadataObject((OMetadataInternal) underlying.getMetadata());
    return metadata;
  }

  public void setInternal(final ATTRIBUTES attribute, final Object iValue) {
    underlying.setInternal(attribute, iValue);
  }

  @Override
  public Iterable<ODatabaseListener> getListeners() {
    return underlying.getListeners();
  }

  public <DBTYPE extends ODatabase<?>> DBTYPE registerHook(final ORecordHook iHookImpl) {
    underlying.registerHook(iHookImpl);
    return (DBTYPE) this;
  }

  public <DBTYPE extends ODatabase<?>> DBTYPE registerHook(final ORecordHook iHookImpl, ORecordHook.HOOK_POSITION iPosition) {
    underlying.registerHook(iHookImpl, iPosition);
    return (DBTYPE) this;
  }

  public ORecordHook.RESULT callbackHooks(final ORecordHook.TYPE iType, final OIdentifiable iObject) {
    return underlying.callbackHooks(iType, iObject);
  }

  public Map<ORecordHook, ORecordHook.HOOK_POSITION> getHooks() {
    return underlying.getHooks();
  }

  public <DBTYPE extends ODatabase<?>> DBTYPE unregisterHook(final ORecordHook iHookImpl) {
    underlying.unregisterHook(iHookImpl);
    return (DBTYPE) this;
  }

  /**
   * Create a new POJO by its class name. Assure to have called the registerEntityClasses() declaring the packages that are part of
   * entity classes.
   *
   * @see OEntityManager#registerEntityClasses(String)
   */
  public <RET extends Object> RET newInstance(final String iClassName, final Object iEnclosingClass, Object... iArgs) {
    underlying.checkIfActive();

    checkSecurity(ORule.ResourceGeneric.CLASS, ORole.PERMISSION_CREATE, iClassName);

    try {
      Class<?> entityClass = entityManager.getEntityClass(iClassName);
      if (entityClass != null) {
        RET enhanced = (RET) OObjectEntityEnhancer.getInstance()
            .getProxiedInstance(entityManager.getEntityClass(iClassName), iEnclosingClass, underlying.newInstance(iClassName), null,
                iArgs);
        return (RET) enhanced;
      } else {
        throw new OSerializationException("Type " + iClassName
            + " cannot be serialized because is not part of registered entities. To fix this error register this class");
      }
    } catch (Exception e) {
      final String message = "Error on creating object of class " + iClassName;
      OLogManager.instance().error(this, message, e);

      throw OException.wrapException(new ODatabaseException(message), e);
    }
  }

  /**
   * Create a new POJO by its class name. Assure to have called the registerEntityClasses() declaring the packages that are part of
   * entity classes.
   *
   * @see OEntityManager#registerEntityClasses(String)
   */
  public <RET extends Object> RET newInstance(final String iClassName, final Object iEnclosingClass, ODocument iDocument,
      Object... iArgs) {
    checkSecurity(ORule.ResourceGeneric.CLASS, ORole.PERMISSION_CREATE, iClassName);

    try {
      Class<?> entityClass = entityManager.getEntityClass(iClassName);
      if (entityClass != null) {
        RET enhanced = (RET) OObjectEntityEnhancer.getInstance()
            .getProxiedInstance(entityManager.getEntityClass(iClassName), iEnclosingClass, iDocument, null, iArgs);
        return (RET) enhanced;
      } else {
        throw new OSerializationException("Type " + iClassName
            + " cannot be serialized because is not part of registered entities. To fix this error register this class");
      }
    } catch (Exception e) {
      final String message = "Error on creating object of class " + iClassName;
      OLogManager.instance().error(this, message, e);

      throw OException.wrapException(new ODatabaseException(message), e);
    }
  }

  public <RET> OObjectIteratorClass<RET> browseClass(final Class<RET> iClusterClass) {
    return browseClass(iClusterClass, true);
  }

  public <RET> OObjectIteratorClass<RET> browseClass(final Class<RET> iClusterClass, final boolean iPolymorphic) {
    if (iClusterClass == null)
      return null;

    return browseClass(iClusterClass.getSimpleName(), iPolymorphic);
  }

  public <RET> OObjectIteratorClass<RET> browseClass(final String iClassName) {
    return browseClass(iClassName, true);
  }

  public <RET> OObjectIteratorClass<RET> browseClass(final String iClassName, final boolean iPolymorphic) {
    checkOpenness();
    checkSecurity(ORule.ResourceGeneric.CLASS, ORole.PERMISSION_READ, iClassName);

    return new OObjectIteratorClass<RET>(this, getUnderlying(), iClassName, iPolymorphic);
  }

  public <RET> OObjectIteratorCluster<RET> browseCluster(final String iClusterName) {
    checkOpenness();
    checkSecurity(ORule.ResourceGeneric.CLUSTER, ORole.PERMISSION_READ, iClusterName);

    return (OObjectIteratorCluster<RET>) new OObjectIteratorCluster<Object>(this, getUnderlying(),
        getClusterIdByName(iClusterName));
  }

  public <RET> RET load(final Object iPojo) {
    return (RET) load(iPojo, null);
  }

  public <RET> RET reload(final Object iPojo) {
    return (RET) reload(iPojo, null, true);
  }

  public <RET> RET reload(final Object iPojo, final boolean iIgnoreCache) {
    return (RET) reload(iPojo, null, iIgnoreCache);
  }

  public <RET> RET reload(Object iPojo, final String iFetchPlan, final boolean iIgnoreCache) {
    return reload(iPojo, iFetchPlan, iIgnoreCache, true);
  }

  @Override
  public <RET> RET reload(Object iObject, String iFetchPlan, boolean iIgnoreCache, boolean force) {
    checkOpenness();
    if (iObject == null)
      return null;

    // GET THE ASSOCIATED DOCUMENT
    final ODocument record = getRecordByUserObject(iObject, true);
    underlying.reload(record, iFetchPlan, iIgnoreCache, force);

    iObject = stream2pojo(record, iObject, iFetchPlan, true);
    return (RET) iObject;

  }

  public <RET> RET load(final Object iPojo, final String iFetchPlan) {
    return (RET) load(iPojo, iFetchPlan, false);
  }

  public void attach(final Object iPojo) {
    OObjectEntitySerializer.attach(iPojo, this);
  }

  public <RET> RET attachAndSave(final Object iPojo) {
    attach(iPojo);
    return (RET) save(iPojo);
  }

  /**
   * Method that detaches all fields contained in the document to the given object. It returns by default a proxied instance.
   *
   * @param iPojo :- the object to detach
   *
   * @return the detached object
   */
  public <RET> RET detach(final Object iPojo) {
    return (RET) OObjectEntitySerializer.detach(iPojo, this);
  }

  /**
   * Method that detaches all fields contained in the document to the given object.
   *
   * @param <RET>
   * @param iPojo                    :- the object to detach
   * @param returnNonProxiedInstance :- defines if the return object will be a proxied instance or not. If set to TRUE and the
   *                                 object does not contains @Id and @Version fields it could procude data replication
   *
   * @return the object serialized or with detached data
   */
  public <RET> RET detach(final Object iPojo, boolean returnNonProxiedInstance) {
    return (RET) OObjectEntitySerializer.detach(iPojo, this, returnNonProxiedInstance);
  }

  /**
   * Method that detaches all fields contained in the document to the given object and recursively all object tree. This may throw a
   * {@link StackOverflowError} with big objects tree. To avoid it set the stack size with -Xss java option
   *
   * @param <RET>
   * @param iPojo                    :- the object to detach
   * @param returnNonProxiedInstance :- defines if the return object will be a proxied instance or not. If set to TRUE and the
   *                                 object does not contains @Id and @Version fields it could procude data replication
   *
   * @return the object serialized or with detached data
   */
  public <RET> RET detachAll(final Object iPojo, boolean returnNonProxiedInstance) {
    return detachAll(iPojo, returnNonProxiedInstance, new HashMap<Object, Object>(), new HashMap<Object, Object>());
  }

  @Override
  public <RET> RET load(Object iPojo, String iFetchPlan, boolean iIgnoreCache) {
    checkOpenness();
    if (iPojo == null)
      return null;

    // GET THE ASSOCIATED DOCUMENT
    ODocument record = getRecordByUserObject(iPojo, true);
    try {
      record.setInternalStatus(ORecordElement.STATUS.UNMARSHALLING);

      record = underlying.load(record, iFetchPlan, iIgnoreCache);

      return (RET) stream2pojo(record, iPojo, iFetchPlan);
    } finally {
      record.setInternalStatus(ORecordElement.STATUS.LOADED);
    }
  }

  @Override
  public <RET> RET lock(ORID recordId) throws OLockException {
    checkOpenness();
    if (recordId == null)
      return null;

    // GET THE ASSOCIATED DOCUMENT
    final ODocument record = (ODocument) underlying.lock(recordId);
    if (record == null)
      return null;

    return (RET) OObjectEntityEnhancer.getInstance().getProxiedInstance(record.getClassName(), entityManager, record, null);

  }

  @Override
  public <RET> RET lock(ORID recordId, long timeout, TimeUnit timeoutUnit) throws OLockException {
    checkOpenness();
    if (recordId == null)
      return null;

    // GET THE ASSOCIATED DOCUMENT
    final ODocument record = (ODocument) underlying.lock(recordId, timeout, timeoutUnit);
    if (record == null)
      return null;

    return (RET) OObjectEntityEnhancer.getInstance().getProxiedInstance(record.getClassName(), entityManager, record, null);
  }

  @Override
  public void unlock(ORID recordId) throws OLockException {
    checkOpenness();
    underlying.unlock(recordId);
  }

  public <RET> RET load(final ORID recordId) {
    return (RET) load(recordId, null);
  }

  public <RET> RET load(final ORID iRecordId, final String iFetchPlan) {
    return (RET) load(iRecordId, iFetchPlan, false);
  }

  @Override
  public <RET> RET load(ORID iRecordId, String iFetchPlan, boolean iIgnoreCache) {
    checkOpenness();
    if (iRecordId == null)
      return null;

    // GET THE ASSOCIATED DOCUMENT
    final ODocument record = (ODocument) underlying.load(iRecordId, iFetchPlan, iIgnoreCache);
    if (record == null)
      return null;

    return (RET) OObjectEntityEnhancer.getInstance().getProxiedInstance(record.getClassName(), entityManager, record, null);
  }

  /**
   * Saves an object to the databasein synchronous mode . First checks if the object is new or not. In case it's new a new ODocument
   * is created and bound to the object, otherwise the ODocument is retrieved and updated. The object is introspected using the Java
   * Reflection to extract the field values.
 If a multi value (array, collection or map of objects) is passed, then each single
   * object is stored separately.
   */
  public <RET> RET save(final Object iContent) {
    return (RET) save(iContent, (String) null, OPERATION_MODE.SYNCHRONOUS, false, null, null);
  }

  /**
   * Saves an object to the database specifying the mode. First checks if the object is new or not. In case it's new a new ODocument
   * is created and bound to the object, otherwise the ODocument is retrieved and updated. The object is introspected using the Java
   * Reflection to extract the field values.
 If a multi value (array, collection or map of objects) is passed, then each single
   * object is stored separately.
   */
  public <RET> RET save(final Object iContent, OPERATION_MODE iMode, boolean iForceCreate,
      final ORecordCallback<? extends Number> iRecordCreatedCallback, ORecordCallback<Integer> iRecordUpdatedCallback) {
    return (RET) save(iContent, null, iMode, false, iRecordCreatedCallback, iRecordUpdatedCallback);
  }

  /**
   * Saves an object in synchronous mode to the database forcing a record cluster where to store it. First checks if the object is
   * new or not. In case it's new a new ODocument is created and bound to the object, otherwise the ODocument is retrieved and
   * updated. The object is introspected using the Java Reflection to extract the field values.
 If a multi value (array,
   * collection or map of objects) is passed, then each single object is stored separately.
   *

   * Before to use the specified cluster a check is made to know if is allowed and figures in the configured and the record is valid
   * following the constraints declared in the schema.
   *
   * @see ODocument#validate()
   */
  public <RET> RET save(final Object iPojo, final String iClusterName) {
    return (RET) save(iPojo, iClusterName, OPERATION_MODE.SYNCHRONOUS, false, null, null);
  }

  /**
   * Saves an object to the database forcing a record cluster where to store it. First checks if the object is new or not. In case
   * it's new a new ODocument is created and bound to the object, otherwise the ODocument is retrieved and updated. The object is
   * introspected using the Java Reflection to extract the field values.
 If a multi value (array, collection or map of objects)
   * is passed, then each single object is stored separately.
   *

   * Before to use the specified cluster a check is made to know if is allowed and figures in the configured and the record is valid
   * following the constraints declared in the schema.
   *
   * @see ODocument#validate()
   */
  public <RET> RET save(final Object iPojo, final String iClusterName, OPERATION_MODE iMode, boolean iForceCreate,
      final ORecordCallback<? extends Number> iRecordCreatedCallback, ORecordCallback<Integer> iRecordUpdatedCallback) {
    checkOpenness();
    if (iPojo == null)
      return (RET) iPojo;
    else if (OMultiValue.isMultiValue(iPojo)) {
      // MULTI VALUE OBJECT: STORE SINGLE POJOS
      for (Object pojo : OMultiValue.getMultiValueIterable(iPojo)) {
        save(pojo, iClusterName);
      }
      return (RET) iPojo;
    } else {
      OSerializationThreadLocal.INSTANCE.get().clear();

      // GET THE ASSOCIATED DOCUMENT
      final Object proxiedObject = OObjectEntitySerializer.serializeObject(iPojo, this);
      final ODocument record = getRecordByUserObject(proxiedObject, true);
      try {
        record.setInternalStatus(ORecordElement.STATUS.MARSHALLING);

        if (!saveOnlyDirty || record.isDirty()) {
          // REGISTER BEFORE TO SERIALIZE TO AVOID PROBLEMS WITH CIRCULAR DEPENDENCY
          // registerUserObject(iPojo, record);
          deleteOrphans((((OObjectProxyMethodHandler) ((ProxyObject) proxiedObject).getHandler())));

          ODocument savedRecord = underlying
              .save(record, iClusterName, iMode, iForceCreate, iRecordCreatedCallback, iRecordUpdatedCallback);

          ((OObjectProxyMethodHandler) ((ProxyObject) proxiedObject).getHandler()).setDoc(savedRecord);
          ((OObjectProxyMethodHandler) ((ProxyObject) proxiedObject).getHandler()).updateLoadedFieldMap(proxiedObject, false);
          // RE-REGISTER FOR NEW RECORDS SINCE THE ID HAS CHANGED
          registerUserObject(proxiedObject, record);
        }
      } finally {
        record.setInternalStatus(ORecordElement.STATUS.LOADED);
      }
      return (RET) proxiedObject;
    }
  }

  public ODatabaseObject delete(final Object iPojo) {
    checkOpenness();

    if (iPojo == null)
      return this;

    ODocument record = getRecordByUserObject(iPojo, false);
    if (record == null) {
      final ORecordId rid = OObjectSerializerHelper.getObjectID(this, iPojo);
      if (rid == null)
        throw new OObjectNotDetachedException("Cannot retrieve the object's ID for '" + iPojo + "' because has not been detached");

      record = (ODocument) underlying.load(rid);
    }
    deleteCascade(record);

    underlying.delete(record);

    if (getTransaction() instanceof OTransactionNoTx)
      unregisterPojo(iPojo, record);

    return this;
  }

  @Override
  public ODatabaseObject delete(final ORID iRID) {
    checkOpenness();

    if (iRID == null)
      return this;

    final ORecord record = iRID.getRecord();
    if (record instanceof ODocument) {
      Object iPojo = getUserObjectByRecord(record, null);

      deleteCascade((ODocument) record);

      underlying.delete(record);

      if (getTransaction() instanceof OTransactionNoTx)
        unregisterPojo(iPojo, (ODocument) record);

    }
    return this;
  }

  @Override
  public ODatabaseObject delete(final ORID iRID, final int iVersion) {
    deleteRecord(iRID, iVersion, false);
    return this;
  }

  public ODatabaseObject delete(final ORecord iRecord) {
    underlying.delete(iRecord);
    return this;
  }

  public long countClass(final String iClassName) {
    checkOpenness();
    return underlying.countClass(iClassName);
  }

  public long countClass(final String iClassName, final boolean iPolymorphic) {
    checkOpenness();
    return underlying.countClass(iClassName, iPolymorphic);
  }

  public long countClass(final Class<?> iClass) {
    checkOpenness();
    return underlying.countClass(iClass.getSimpleName());
  }

  public ODictionary<Object> getDictionary() {
    checkOpenness();
    if (dictionary == null)
      dictionary = new ODictionaryWrapper(this, underlying.getDictionary().getIndex());

    return dictionary;
  }

  public OTransaction getTransaction() {
    return underlying.getTransaction();
  }

  public OObjectDatabaseTx begin() {
    underlying.begin();
    return this;
  }

  public OObjectDatabaseTx begin(final OTransaction.TXTYPE iType) {
    underlying.begin(iType);
    return this;
  }

  public OObjectDatabaseTx begin(final OTransaction iTx) {
    underlying.begin(iTx);
    return this;
  }

  @Override
  public OObjectDatabaseTx commit() {
    // BY PASS DOCUMENT DB
    return commit(false);
  }

  @Override
  public OObjectDatabaseTx commit(boolean force) throws OTransactionException {
    underlying.commit(force);

    if (getTransaction().isActive())
      return this;

    if (getTransaction().getRecordOperations() != null) {
      // UPDATE ID & VERSION FOR ALL THE RECORDS
      Object pojo = null;
      for (ORecordOperation entry : getTransaction().getRecordOperations()) {
        switch (entry.type) {
        case ORecordOperation.CREATED:
        case ORecordOperation.UPDATED:
          break;

        case ORecordOperation.DELETED:
          final ORecord rec = entry.getRecord();
          if (rec instanceof ODocument)
            unregisterPojo(pojo, (ODocument) rec);
          break;
        }
      }
    }

    return this;
  }

  @Override
  public OObjectDatabaseTx rollback() {
    return rollback(false);
  }

  @Override
  public OObjectDatabaseTx rollback(boolean force) throws OTransactionException {
    // BY PASS DOCUMENT DB
    underlying.rollback(force);

    if (!underlying.getTransaction().isActive()) {
      // COPY ALL TX ENTRIES
      final List<ORecordOperation> newEntries;
      if (getTransaction().getRecordOperations() != null) {
        newEntries = new ArrayList<ORecordOperation>();
        for (ORecordOperation entry : getTransaction().getRecordOperations())
          if (entry.type == ORecordOperation.CREATED)
            newEntries.add(entry);
      } else
        newEntries = null;
    }

    return this;
  }

  public OEntityManager getEntityManager() {
    return entityManager;
  }

  @Override
  public ODatabaseDocumentInternal getUnderlying() {
    return underlying;
  }

  /**
   * Returns the version number of the object. Version starts from 0 assigned on creation.
   *
   * @param iPojo User object
   */
  public int getVersion(final Object iPojo) {
    checkOpenness();
    final ODocument record = getRecordByUserObject(iPojo, false);
    if (record != null)
      return record.getVersion();

    return OObjectSerializerHelper.getObjectVersion(iPojo);
  }

  /**
   * Returns the object unique identity.
   *
   * @param iPojo User object
   */
  public ORID getIdentity(final Object iPojo) {
    checkOpenness();
    if (iPojo instanceof OIdentifiable)
      return ((OIdentifiable) iPojo).getIdentity();
    final ODocument record = getRecordByUserObject(iPojo, false);
    if (record != null)
      return record.getIdentity();
    return OObjectSerializerHelper.getObjectID(this, iPojo);
  }

  public boolean isSaveOnlyDirty() {
    return saveOnlyDirty;
  }

  public void setSaveOnlyDirty(boolean saveOnlyDirty) {
    this.saveOnlyDirty = saveOnlyDirty;
  }

  public boolean isAutomaticSchemaGeneration() {
    return automaticSchemaGeneration;
  }

  public void setAutomaticSchemaGeneration(boolean automaticSchemaGeneration) {
    this.automaticSchemaGeneration = automaticSchemaGeneration;
  }

  public Object newInstance() {
    checkOpenness();
    return new ODocument();
  }

  public <DBTYPE extends ODatabase> DBTYPE checkSecurity(ORule.ResourceGeneric resourceGeneric, String resourceSpecific,
      final byte iOperation) {
    return (DBTYPE) underlying.checkSecurity(resourceGeneric, resourceSpecific, iOperation);
  }

  public <DBTYPE extends ODatabase> DBTYPE checkSecurity(final ORule.ResourceGeneric iResource, final int iOperation,
      Object iResourceSpecific) {
    return (DBTYPE) underlying.checkSecurity(iResource, iOperation, iResourceSpecific);
  }

  public <DBTYPE extends ODatabase> DBTYPE checkSecurity(final ORule.ResourceGeneric iResource, final int iOperation,
      Object... iResourcesSpecific) {
    return (DBTYPE) underlying.checkSecurity(iResource, iOperation, iResourcesSpecific);
  }

  @Override
  public ODocument pojo2Stream(final Object iPojo, final ODocument iRecord) {
    if (iPojo instanceof ProxyObject) {
      return ((OObjectProxyMethodHandler) ((ProxyObject) iPojo).getHandler()).getDoc();
    }
    return OObjectSerializerHelper
        .toStream(iPojo, iRecord, getEntityManager(), getMetadata().getSchema().getClass(iPojo.getClass().getSimpleName()), this,
            this, saveOnlyDirty);
  }

  @Override
  public Object stream2pojo(ODocument iRecord, final Object iPojo, final String iFetchPlan) {
    return stream2pojo(iRecord, iPojo, iFetchPlan, false);
  }

  public Object stream2pojo(ODocument iRecord, final Object iPojo, final String iFetchPlan, boolean iReload) {
    if (iRecord.getInternalStatus() == ORecordElement.STATUS.NOT_LOADED)
      iRecord = (ODocument) iRecord.load();
    if (iReload) {
      if (iPojo != null) {
        if (iPojo instanceof Proxy) {
          ((OObjectProxyMethodHandler) ((ProxyObject) iPojo).getHandler()).setDoc(iRecord);
          ((OObjectProxyMethodHandler) ((ProxyObject) iPojo).getHandler()).updateLoadedFieldMap(iPojo, iReload);
          return iPojo;
        } else
          return OObjectEntityEnhancer.getInstance().getProxiedInstance(iPojo.getClass(), iRecord);
      } else
        return OObjectEntityEnhancer.getInstance().getProxiedInstance(iRecord.getClassName(), entityManager, iRecord, null);
    } else if (!(iPojo instanceof Proxy))
      return OObjectEntityEnhancer.getInstance().getProxiedInstance(iPojo.getClass(), iRecord);
    else
      return iPojo;
  }

  public boolean isLazyLoading() {
    return lazyLoading;
  }

  public void setLazyLoading(final boolean lazyLoading) {
    this.lazyLoading = lazyLoading;
  }

  public String getType() {
    return TYPE;
  }

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

  @Override
  public OObjectDatabaseTx setConflictStrategy(final ORecordConflictStrategy iResolver) {
    underlying.setConflictStrategy(iResolver);
    return this;
  }

  @Override
  public OObjectDatabaseTx setConflictStrategy(final String iStrategyName) {
    getStorage().setConflictStrategy(Orient.instance().getRecordConflictStrategy().getStrategy(iStrategyName));
    return this;
  }

  @Override
  public ODocument getRecordByUserObject(final Object iPojo, final boolean iCreateIfNotAvailable) {
    if (iPojo instanceof Proxy)
      return OObjectEntitySerializer.getDocument((Proxy) iPojo);
    return OObjectEntitySerializer.getDocument((Proxy) OObjectEntitySerializer.serializeObject(iPojo, this));
  }

  public Object getUserObjectByRecord(final OIdentifiable iRecord, final String iFetchPlan) {
    return getUserObjectByRecord(iRecord, iFetchPlan, true);
  }

  public Object getUserObjectByRecord(final OIdentifiable iRecord, final String iFetchPlan, final boolean iCreate) {
    final ODocument document = iRecord.getRecord();

    return OObjectEntityEnhancer.getInstance().getProxiedInstance(document.getClassName(), getEntityManager(), document, null);
  }

  @Override
  public void registerUserObject(final Object iObject, final ORecord iRecord) {
  }

  public void registerUserObjectAfterLinkSave(ORecord iRecord) {
  }

  public void unregisterPojo(final Object iObject, final ODocument iRecord) {
  }

  public boolean existsUserObjectByRID(ORID iRID) {
    return false;
  }

  public boolean isManaged(final Object iEntity) {
    return false;
  }

  public void registerClassMethodFilter(Class<?> iClass, OObjectMethodFilter iMethodFilter) {
    OObjectEntityEnhancer.getInstance().registerClassMethodFilter(iClass, iMethodFilter);
  }

  public void deregisterClassMethodFilter(final Class<?> iClass) {
    OObjectEntityEnhancer.getInstance().deregisterClassMethodFilter(iClass);
  }

  @Override
  public String incrementalBackup(String path) {
    return underlying.incrementalBackup(path);
  }

  @Override
  public void resetInitialization() {
    underlying.resetInitialization();
  }

  protected <RET> RET detachAll(final Object iPojo, boolean returnNonProxiedInstance, Map<Object, Object> alreadyDetached,
      Map<Object, Object> lazyObjects) {
    return (RET) OObjectEntitySerializer.detachAll(iPojo, this, returnNonProxiedInstance, alreadyDetached, lazyObjects);
  }

  protected void deleteCascade(final ODocument record) {
    if (record == null)
      return;
    List<String> toDeleteCascade = OObjectEntitySerializer.getCascadeDeleteFields(record.getClassName());
    if (toDeleteCascade != null) {
      for (String field : toDeleteCascade) {
        Object toDelete = record.field(field);
        if (toDelete instanceof OIdentifiable) {
          if (toDelete != null)
            delete(((OIdentifiable) toDelete).getIdentity());
        } else if (toDelete instanceof Collection) {
          for (OIdentifiable cascadeRecord : ((Collection<OIdentifiable>) toDelete)) {
            if (cascadeRecord != null)
              delete(((OIdentifiable) cascadeRecord).getIdentity());
          }
        } else if (toDelete instanceof Map) {
          for (OIdentifiable cascadeRecord : ((Map<Object, OIdentifiable>) toDelete).values()) {
            if (cascadeRecord != null)
              delete(((OIdentifiable) cascadeRecord).getIdentity());
          }
        }
      }
    }
  }

  protected void init() {
    entityManager = OEntityManager.getEntityManagerByDatabaseURL(getURL());
    entityManager.setClassHandler(OObjectEntityClassHandler.getInstance(getURL()));

    lazyLoading = true;
    if (!isClosed() && entityManager.getEntityClass(OUser.class.getSimpleName()) == null) {
      entityManager.registerEntityClass(OUser.class);
      entityManager.registerEntityClass(ORole.class);
    }
  }

  protected void deleteOrphans(final OObjectProxyMethodHandler handler) {
    for (ORID orphan : handler.getOrphans()) {
      final ODocument doc = orphan.getRecord();
      deleteCascade(doc);
      if (doc != null)
        underlying.delete(doc);
    }
    handler.getOrphans().clear();
  }

  private boolean deleteRecord(ORID iRID, final int iVersion, boolean prohibitTombstones) {
    checkOpenness();

    if (iRID == null)
      return true;

    ODocument record = iRID.getRecord();
    if (record != null) {
      Object iPojo = getUserObjectByRecord(record, null);

      deleteCascade(record);

      if (prohibitTombstones)
        underlying.cleanOutRecord(iRID, iVersion);
      else
        underlying.delete(iRID, iVersion);

      if (getTransaction() instanceof OTransactionNoTx)
        unregisterPojo(iPojo, record);

    }
    return false;
  }

  @Override
  public int addBlobCluster(String iClusterName, Object... iParameters) {
    return getUnderlying().addBlobCluster(iClusterName, iParameters);
  }

  @Override
  public Set<Integer> getBlobClusterIds() {
    return getUnderlying().getBlobClusterIds();
  }

  @Override
  public OSharedContext getSharedContext() {
    return underlying.getSharedContext();
  }

  /**
   * Register the static document binary mapping mode in the database context (only if it's not already set)
   */
  private void registerFieldMappingStrategy() {
    if (!this.getConfiguration().getContextKeys().contains(OGlobalConfiguration.DOCUMENT_BINARY_MAPPING.getKey())) {
      this.getConfiguration()
          .setValue(OGlobalConfiguration.DOCUMENT_BINARY_MAPPING, OGlobalConfiguration.DOCUMENT_BINARY_MAPPING.getValueAsInteger());
    }
  }

  /**
   * Returns a wrapped OCommandRequest instance to catch the result-set by converting it before to return to the user application.
   */
  public <RET extends OCommandRequest> RET command(final OCommandRequest iCommand) {
    return (RET) new OCommandSQLPojoWrapper(this, underlying.command(iCommand));
  }

  @Override
  public <RET extends List<?>> RET query(OQuery<?> iCommand, Object... iArgs) {
    checkOpenness();

    convertParameters(iArgs);

    final List<ODocument> result = underlying.query(iCommand, iArgs);

    if (result == null)
      return null;

    final List<Object> resultPojo = new ArrayList<Object>();
    Object obj;
    for (OIdentifiable doc : result) {
      if (doc instanceof ODocument) {
        // GET THE ASSOCIATED DOCUMENT
        if (((ODocument) doc).getClassName() == null)
          obj = doc;
        else
          obj = getUserObjectByRecord(((ODocument) doc), iCommand.getFetchPlan(), true);

        resultPojo.add(obj);
      } else {
        resultPojo.add(doc);
      }

    }

    return (RET) resultPojo;
  }

  /**
   * Converts an array of parameters: if a POJO is used, then replace it with its record id.
   *
   * @param iArgs Array of parameters as Object
   *
   * @see #convertParameter(Object)
   */
  protected void convertParameters(final Object... iArgs) {
    if (iArgs == null)
      return;

    // FILTER PARAMETERS
    for (int i = 0; i < iArgs.length; ++i)
      iArgs[i] = convertParameter(iArgs[i]);
  }

  /**
   * Sets as dirty a POJO. This is useful when you change the object and need to tell to the engine to treat as dirty.
   *
   * @param iPojo User object
   */
  public void setDirty(final Object iPojo) {
    if (iPojo == null)
      return;

    final ODocument record = getRecordByUserObject(iPojo, false);
    if (record == null)
      throw new OObjectNotManagedException("The object " + iPojo + " is not managed by current database");

    record.setDirty();
  }

  /**
   * Sets as not dirty a POJO. This is useful when you change some other object and need to tell to the engine to treat this one as
   * not dirty.
   *
   * @param iPojo User object
   */
  public void unsetDirty(final Object iPojo) {
    if (iPojo == null)
      return;

    final ODocument record = getRecordByUserObject(iPojo, false);
    if (record == null)
      return;

    ORecordInternal.unsetDirty(record);
  }

  /**
   * Convert a parameter: if a POJO is used, then replace it with its record id.
   *
   * @param iParameter Parameter to convert, if applicable
   *
   * @see #convertParameters(Object...)
   */
  protected Object convertParameter(final Object iParameter) {
    if (iParameter != null)
      // FILTER PARAMETERS
      if (iParameter instanceof Map<?, ?>) {
        Map<String, Object> map = (Map<String, Object>) iParameter;

        for (Map.Entry<String, Object> e : map.entrySet()) {
          map.put(e.getKey(), convertParameter(e.getValue()));
        }

        return map;
      } else if (iParameter instanceof Collection<?>) {
        List<Object> result = new ArrayList<Object>();
        for (Object object : (Collection<Object>) iParameter) {
          result.add(convertParameter(object));
        }
        return result;
      } else if (iParameter.getClass().isEnum()) {
        return ((Enum<?>) iParameter).name();
      } else if (!OType.isSimpleType(iParameter)) {
        final ORID rid = getIdentity(iParameter);
        if (rid != null && rid.isValid())
          // REPLACE OBJECT INSTANCE WITH ITS RECORD ID
          return rid;
      }

    return iParameter;
  }

  @Deprecated
  public boolean isMVCC() {
    return underlying.isMVCC();
  }

  @Deprecated
  public <DBTYPE extends ODatabase<?>> DBTYPE setMVCC(final boolean iMvcc) {
    underlying.setMVCC(iMvcc);
    return (DBTYPE) this;
  }

  /**
   * Returns true if current configuration retains objects, otherwise false
   *
   * @see #setRetainObjects(boolean)
   */
  @Deprecated
  public boolean isRetainObjects() {
    return false;
  }

  /**
   * Specifies if retain handled objects in memory or not. Setting it to false can improve performance on large inserts. Default is
   * enabled.
   *
   * @param iValue True to enable, false to disable it.
   *
   * @see #isRetainObjects()
   */
  @Deprecated
  public OObjectDatabaseTx setRetainObjects(final boolean iValue) {
    return this;
  }

  @Override
  public OLiveQueryMonitor live(String query, OLiveQueryResultListener listener, Object... args) {
    return underlying.live(query, listener, args);
  }

  @Override
  public OLiveQueryMonitor live(String query, OLiveQueryResultListener listener, Map<String, ?> args) {
    return underlying.live(query, listener, args);
  }

  @Override
  public OResultSet query(String query, Object... args) throws OCommandSQLParsingException, OCommandExecutionException {
    return underlying.query(query, args);
  }

  @Override
  public OResultSet query(String query, Map args) throws OCommandSQLParsingException, OCommandExecutionException {
    return underlying.query(query, args);
  }

  @Override
  public OResultSet command(String query, Object... args) throws OCommandSQLParsingException, OCommandExecutionException {
    return underlying.command(query, args);
  }

  public <RET extends List<?>> RET objectQuery(String iCommand, Object... iArgs) {
    checkOpenness();

    convertParameters(iArgs);

    final OResultSet result = underlying.query(iCommand, iArgs);

    if (result == null)
      return null;

    try {

      final List<Object> resultPojo = new ArrayList<Object>();
      Object obj;
      while (result.hasNext()) {
        OResult doc = result.next();
        if (doc.isElement()) {
          // GET THE ASSOCIATED DOCUMENT
          OElement elem = doc.getElement().get();
          if (elem.getSchemaType().isPresent())
            obj = getUserObjectByRecord(elem, null, true);
          else
            obj = elem;

          resultPojo.add(obj);
        } else {
          resultPojo.add(doc);
        }

      }

      return (RET) resultPojo;
    } finally {
      result.close();
    }
  }

  public <RET extends List<?>> RET objectQuery(String iCommand, Map<String, Object> iArgs) {
    checkOpenness();

    convertParameters(iArgs);

    final OResultSet result = underlying.query(iCommand, iArgs);

    if (result == null)
      return null;

    try {

      final List<Object> resultPojo = new ArrayList<Object>();
      Object obj;
      while (result.hasNext()) {
        OResult doc = result.next();
        if (doc.isElement()) {
          // GET THE ASSOCIATED DOCUMENT
          OElement elem = doc.getElement().get();
          if (elem.getSchemaType().isPresent())
            obj = getUserObjectByRecord(elem, null, true);
          else
            obj = elem;

          resultPojo.add(obj);
        } else {
          resultPojo.add(doc);
        }

      }

      return (RET) resultPojo;
    } finally {
      result.close();
    }
  }

  public <RET extends List<?>> RET objectCommand(String iCommand, Object... iArgs) {
    checkOpenness();

    convertParameters(iArgs);

    final OResultSet result = underlying.command(iCommand, iArgs);

    if (result == null)
      return null;

    try {

      final List<Object> resultPojo = new ArrayList<Object>();
      Object obj;
      while (result.hasNext()) {
        OResult doc = result.next();
        if (doc.isElement()) {
          // GET THE ASSOCIATED DOCUMENT
          OElement elem = doc.getElement().get();
          if (elem.getSchemaType().isPresent())
            obj = getUserObjectByRecord(elem, null, true);
          else
            obj = elem;

          resultPojo.add(obj);
        } else {
          resultPojo.add(doc);
        }

      }

      return (RET) resultPojo;
    } finally {
      result.close();
    }
  }

  public <RET extends List<?>> RET objectCommand(String iCommand, Map<String, Object> iArgs) {
    checkOpenness();

    convertParameters(iArgs);

    final OResultSet result = underlying.command(iCommand, iArgs);

    if (result == null)
      return null;

    try {

      final List<Object> resultPojo = new ArrayList<Object>();
      Object obj;
      while (result.hasNext()) {
        OResult doc = result.next();
        if (doc.isElement()) {
          // GET THE ASSOCIATED DOCUMENT
          OElement elem = doc.getElement().get();
          if (elem.getSchemaType().isPresent())
            obj = getUserObjectByRecord(elem, null, true);
          else
            obj = elem;

          resultPojo.add(obj);
        } else {
          resultPojo.add(doc);
        }

      }

      return (RET) resultPojo;
    } finally {
      result.close();
    }
  }

  @Override
  public OResultSet command(String query, Map args) throws OCommandSQLParsingException, OCommandExecutionException {
    return underlying.command(query, args);
  }

  @Override
  public OResultSet execute(String language, String script, Object... args)
      throws OCommandExecutionException, OCommandScriptException {
    return underlying.execute(language, script, args);
  }

  @Override
  public OResultSet execute(String language, String script, Map<String, ?> args)
      throws OCommandExecutionException, OCommandScriptException {
    return underlying.execute(language, script, args);
  }

  @Override
  public <T> T executeWithRetry(int nRetries, Function<ODatabaseSession, T> function)
      throws IllegalStateException, IllegalArgumentException, ONeedRetryException, UnsupportedOperationException {
    return underlying.executeWithRetry(nRetries, function);
  }
}


/*
 * Copyright 2012 Geomatys.
 *
 * 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.
 */
package com.orientechnologies.orient.core.index;

import com.orientechnologies.common.util.OCollections;
import com.orientechnologies.orient.core.exception.OConfigurationException;
import com.orientechnologies.orient.core.index.engine.OBaseIndexEngine;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.storage.OStorage;
import com.orientechnologies.orient.core.storage.index.hashindex.local.OHashIndexFactory;

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static com.orientechnologies.common.util.OClassLoaderHelper.lookupProviderWithOrientClassLoader;

/**
 * Utility class to create indexes. New OIndexFactory can be registered
 *

 *

 * In order to be detected, factories must implement the {@link OIndexFactory} interface.
 *

 *

 *

 * In addition to implementing this interface datasources should have a services file:

 * META-INF/services/com.orientechnologies.orient.core.index.OIndexFactory
 *

 *

 *

 * The file should contain a single line which gives the full name of the implementing class.
 *

 *

 *

 * Example:

 * org.mycompany.index.MyIndexFactory
 *

 *
 * @author Johann Sorel (Geomatys)
 */
public final class OIndexes {

  private static       Set<OIndexFactory> FACTORIES         = null;
  private static final Set<OIndexFactory> DYNAMIC_FACTORIES = Collections.synchronizedSet(new HashSet<>());
  private static       ClassLoader        orientClassLoader = OIndexes.class.getClassLoader();

  private OIndexes() {
  }

  /**
   * Cache a set of all factories. we do not use the service loader directly since it is not concurrent.
   *
   * @return Set<OIndexFactory>
   */
  private static synchronized Set<OIndexFactory> getFactories() {
    if (FACTORIES == null) {

      final Iterator<OIndexFactory> ite = lookupProviderWithOrientClassLoader(OIndexFactory.class, orientClassLoader);

      final Set<OIndexFactory> factories = new HashSet<>();
      while (ite.hasNext()) {
        factories.add(ite.next());
      }
      factories.addAll(DYNAMIC_FACTORIES);
      FACTORIES = Collections.unmodifiableSet(factories);
    }
    return FACTORIES;
  }

  /**
   * @return Iterator of all index factories
   */
  public static Iterator<OIndexFactory> getAllFactories() {
    return getFactories().iterator();
  }

  /**
   * Iterates on all factories and append all index types.
   *
   * @return Set of all index types.
   */
  private static Set<String> getIndexTypes() {
    final Set<String> types = new HashSet<>();
    final Iterator<OIndexFactory> ite = getAllFactories();
    while (ite.hasNext()) {
      types.addAll(ite.next().getTypes());
    }
    return types;
  }

  /**
   * Iterates on all factories and append all index engines.
   *
   * @return Set of all index engines.
   */
  public static Set<String> getIndexEngines() {
    final Set<String> engines = new HashSet<>();
    final Iterator<OIndexFactory> ite = getAllFactories();
    while (ite.hasNext()) {
      engines.addAll(ite.next().getAlgorithms());
    }
    return engines;
  }

  public static OIndexFactory getFactory(String indexType, String algorithm) {
    if (algorithm == null) {
      algorithm = chooseDefaultIndexAlgorithm(indexType);
    }

    algorithm = algorithm.toUpperCase(Locale.ENGLISH);
    final Iterator<OIndexFactory> ite = getAllFactories();

    while (ite.hasNext()) {
      final OIndexFactory factory = ite.next();
      if (factory.getTypes().contains(indexType) && factory.getAlgorithms().contains(algorithm)) {
        return factory;
      }
    }

    throw new OIndexException("Index with type " + indexType + " and algorithm " + algorithm + " does not exist.");
  }

  /**
   * @param storage   TODO
   * @param indexType index type
   *
   * @return OIndexInternal
   *
   * @throws OConfigurationException if index creation failed
   * @throws OIndexException         if index type does not exist
   */
  public static OIndexInternal<?> createIndex(OStorage storage, String name, String indexType, String algorithm,
      String valueContainerAlgorithm, ODocument metadata, int version) throws OConfigurationException, OIndexException {
    if (indexType.equalsIgnoreCase(OClass.INDEX_TYPE.UNIQUE_HASH_INDEX.name()) || indexType
        .equalsIgnoreCase(OClass.INDEX_TYPE.NOTUNIQUE_HASH_INDEX.name()) || indexType
        .equalsIgnoreCase(OClass.INDEX_TYPE.DICTIONARY_HASH_INDEX.name())) {
      if (!algorithm.equalsIgnoreCase("autosharding")) {
        algorithm = OHashIndexFactory.HASH_INDEX_ALGORITHM;
      }
    }

    return findFactoryByAlgorithmAndType(algorithm, indexType)
        .createIndex(name, storage, indexType, algorithm, valueContainerAlgorithm, metadata, version);

  }

  private static OIndexFactory findFactoryByAlgorithmAndType(String algorithm, String indexType) {

    for (OIndexFactory factory : getFactories()) {
      if (indexType == null || indexType.isEmpty() || (factory.getTypes().contains(indexType)) && factory.getAlgorithms()
          .contains(algorithm)) {
        return factory;
      }
    }
    throw new OIndexException(
        "Index type " + indexType + " with engine " + algorithm + " is not supported. Types are " + OCollections
            .toString(getIndexTypes()));
  }

  public static OBaseIndexEngine createIndexEngine(final String name, final String algorithm, final String type,
      final Boolean durableInNonTxMode, final OStorage storage, final int version, int apiVersion, boolean multivalue,
      final Map<String, String> indexProperties, ODocument metadata) {

    final OIndexFactory factory = findFactoryByAlgorithmAndType(algorithm, type);

    return factory
        .createIndexEngine(algorithm, name, durableInNonTxMode, storage, version, apiVersion, multivalue, indexProperties);
  }

  public static String chooseDefaultIndexAlgorithm(String type) {
    String algorithm = null;

    if (OClass.INDEX_TYPE.DICTIONARY.name().equalsIgnoreCase(type) || OClass.INDEX_TYPE.FULLTEXT.name().equalsIgnoreCase(type)
        || OClass.INDEX_TYPE.NOTUNIQUE.name().equalsIgnoreCase(type) || OClass.INDEX_TYPE.UNIQUE.name().equalsIgnoreCase(type)) {
      algorithm = ODefaultIndexFactory.CELL_BTREE_ALGORITHM;
    } else if (OClass.INDEX_TYPE.DICTIONARY_HASH_INDEX.name().equalsIgnoreCase(type) || OClass.INDEX_TYPE.FULLTEXT_HASH_INDEX.name()
        .equalsIgnoreCase(type) || OClass.INDEX_TYPE.NOTUNIQUE_HASH_INDEX.name().equalsIgnoreCase(type)
        || OClass.INDEX_TYPE.UNIQUE_HASH_INDEX.name().equalsIgnoreCase(type)) {
      algorithm = OHashIndexFactory.HASH_INDEX_ALGORITHM;
    }
    return algorithm;
  }

  /**
   * Scans for factory plug-ins on the application class path. This method is needed because the application class path can
   * theoretically change, or additional plug-ins may become available. Rather than re-scanning the classpath on every invocation of
   * the API, the class path is scanned automatically only on the first invocation. Clients can call this method to prompt a
   * re-scan. Thus this method need only be invoked by sophisticated applications which dynamically make new plug-ins available at
   * runtime.
   */
  private static synchronized void scanForPlugins() {
    // clear cache, will cause a rescan on next getFactories call
    FACTORIES = null;
  }

  /**
   * Register at runtime custom factories
   */
  public static void registerFactory(OIndexFactory factory) {
    DYNAMIC_FACTORIES.add(factory);
    scanForPlugins();
  }

  /**
   * Unregister custom factories
   */
  public static void unregisterFactory(OIndexFactory factory) {
    DYNAMIC_FACTORIES.remove(factory);
    scanForPlugins();
  }

}