All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arcadedb.query.java.JavaQueryEngine Maven / Gradle / Ivy

There is a newer version: 24.11.1
Show newest version
/*
 * Copyright © 2021-present Arcade Data Ltd ([email protected])
 *
 * 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.arcadedb.query.java;

import com.arcadedb.ContextConfiguration;
import com.arcadedb.GlobalConfiguration;
import com.arcadedb.database.DatabaseInternal;
import com.arcadedb.database.Document;
import com.arcadedb.database.Identifiable;
import com.arcadedb.exception.CommandExecutionException;
import com.arcadedb.exception.CommandParsingException;
import com.arcadedb.query.QueryEngine;
import com.arcadedb.query.sql.executor.InternalResultSet;
import com.arcadedb.query.sql.executor.ResultInternal;
import com.arcadedb.query.sql.executor.ResultSet;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;

public class JavaQueryEngine implements QueryEngine {
  public static final String                       ENGINE_NAME       = "java";
  private final       long                         timeout;
  private final       ThreadPoolExecutor           userCodeExecutor;
  private final       ArrayBlockingQueue userCodeExecutorQueue;
  private final       Set                  registeredClasses = new HashSet<>();
  private final       Set                  registeredMethods = new HashSet<>();

  private static final AnalyzedQuery ANALYZED_QUERY = new AnalyzedQuery() {
    @Override
    public boolean isIdempotent() {
      return false;
    }

    @Override
    public boolean isDDL() {
      return false;
    }
  };

  /**
   * Registers a class to be used in a query via Java reflection.
   *
   * @param classFullName Full name of the class (with package). For inner classes, use $ as separator, example: `com.arcadedb.query.java.JavaFunctionsTest$Sum`.
   *
   * @return
   */
  @Override
  public JavaQueryEngine registerFunctions(final String classFullName) {
    if (classFullName.indexOf("::") > -1)
      registeredMethods.add(classFullName);
    else
      registeredClasses.add(classFullName);

    return this;
  }

  /**
   * Unregister a class to be not used by the Java Query engine.
   */
  @Override
  public QueryEngine unregisterFunctions() {
    registeredClasses.clear();
    registeredMethods.clear();
    return this;
  }

  public static class JavaQueryEngineFactory implements QueryEngineFactory {
    @Override
    public String getLanguage() {
      return ENGINE_NAME;
    }

    @Override
    public QueryEngine getInstance(final DatabaseInternal database) {
      return new JavaQueryEngine(database);
    }
  }

  protected JavaQueryEngine(final DatabaseInternal database) {
    this.userCodeExecutorQueue = new ArrayBlockingQueue<>(1_000);
    this.userCodeExecutor = new ThreadPoolExecutor(8, 8, 30, TimeUnit.SECONDS, userCodeExecutorQueue, new ThreadPoolExecutor.CallerRunsPolicy());
    this.timeout = database.getConfiguration().getValueAsLong(GlobalConfiguration.POLYGLOT_COMMAND_TIMEOUT);
  }

  @Override
  public void close() {
    userCodeExecutor.shutdown();
    userCodeExecutorQueue.clear();
  }

  @Override
  public ResultSet command(final String query, ContextConfiguration configuration, final Object... parameters) {
    try {
      return executeUserCode(() -> {

        final String[] parts = query.split("::");
        if (parts.length != 2)
          throw new CommandParsingException(
              "Java function name '" + query + "' must contain the full package of the class, :: and the method. Example: org.acme.Math::sum");

        if (!registeredMethods.contains(query)) {
          if (!registeredClasses.contains(parts[0]))
            throw new SecurityException("The Java class '" + parts[0] + "' or the method '" + query
                + "' was not registered to be used by the Java Query engine. Please register it before using.");
        }

        final Class impl = Class.forName(parts[0]);

        final Object[] parameterArray = new Object[parameters.length];

        // LOOK FOR THE RIGHT METHOD TO INVOKE
        final Method rightMethod = searchMethod(parts, impl, parameterArray, parameters);

        if (rightMethod == null)
          throw new NoSuchMethodException(
              "Java function '" + query + "' not found on classpath (class '" + parts[0] + "' method '" + parts[1] + "' with parameters " + Arrays.toString(
                  parameters) + ")");

        final Object instance = Modifier.isStatic(rightMethod.getModifiers()) ? null : impl.getConstructor().newInstance();

        final Object result = rightMethod.invoke(instance, parameterArray);

        final InternalResultSet resultSet;
        if (result instanceof ResultSet)
          resultSet = (InternalResultSet) result;
        else if (result instanceof Iterable) {
          resultSet = new InternalResultSet();
          for (final Object o : (Iterable) result)
            resultSet.add(extractResult(o));
        } else {
          resultSet = new InternalResultSet();
          resultSet.add(extractResult(result));
        }

        return resultSet;

      }, timeout);

    } catch (final CommandExecutionException e) {
      throw e;
    } catch (final ExecutionException e) {
      // USE THE UNDERLYING CAUSE BYPASSING THE NOT RELEVANT EXECUTION EXCEPTION
      throw new CommandExecutionException("Error on executing user code", e.getCause());
    } catch (final Exception e) {
      throw new CommandExecutionException("Error on executing user code", e);
    }

  }

  private Method searchMethod(final String[] parts, final Class impl, final Object[] parameterArray, final Object[] parameters) {
    Method rightMethod = null;
    for (final Method method : impl.getMethods()) {
      if (method.getName().equals(parts[1])) {
        if (method.getParameterCount() == parameters.length) {

          // RESET PARAMETER ARRAY
          Arrays.fill(parameterArray, null);

          boolean allParamsMatch = true;
          final Parameter[] methodParameters = method.getParameters();
          for (int i = 0; i < methodParameters.length; i++) {
            final Object parameterValue = parameters[i];
            if (parameterValue == null)
              continue;

            final Parameter methodParameter = methodParameters[i];

            parameterArray[i] = parameterValue;

            final Class methodParameterType = methodParameter.getType();
            final Class parameterValueClass = parameterValue.getClass();

            if (!parameterValueClass.isAssignableFrom(methodParameterType)) {
              if (methodParameterType.isPrimitive()) {
                // CHECK FOR AUTOBOXING
                if (methodParameterType.equals(Integer.TYPE)) {
                  if (!parameterValueClass.equals(Integer.class))
                    allParamsMatch = false;
                } else if (methodParameterType.equals(Long.TYPE)) {
                  if (!parameterValueClass.equals(Long.class))
                    allParamsMatch = false;
                } else if (methodParameterType.equals(Float.TYPE)) {
                  if (!parameterValueClass.equals(Float.class))
                    allParamsMatch = false;
                } else if (methodParameterType.equals(Double.TYPE)) {
                  if (!parameterValueClass.equals(Double.class))
                    allParamsMatch = false;
                } else if (methodParameterType.equals(Byte.TYPE)) {
                  if (!parameterValueClass.equals(Byte.class))
                    allParamsMatch = false;
                } else if (methodParameterType.equals(Character.TYPE)) {
                  if (!parameterValueClass.equals(Character.class))
                    allParamsMatch = false;
                } else if (methodParameterType.equals(Short.TYPE)) {
                  if (!parameterValueClass.equals(Short.class))
                    allParamsMatch = false;
                }
              }
            } else
              allParamsMatch = false;

            if (!allParamsMatch)
              break;
          }

          if (allParamsMatch)
            rightMethod = method;

          break;
        }
      }
    }

    return rightMethod;
  }

  @Override
  public ResultSet command(final String query, ContextConfiguration configuration, final Map parameters) {
    if (parameters == null || parameters.size() == 0)
      return command(query, null);
    throw new UnsupportedOperationException("Execution of a command with parameters referenced by name is not supported for Java engine");
  }

  @Override
  public String getLanguage() {
    return ENGINE_NAME;
  }

  @Override
  public AnalyzedQuery analyze(final String query) {
    try {
      executeUserCode(() -> {
        return null;
      }, timeout);
    } catch (final CommandExecutionException e) {
      throw e;
    } catch (final ExecutionException e) {
      // USE THE UNDERLYING CAUSE BYPASSING THE NOT RELEVANT EXECUTION EXCEPTION
      throw new CommandExecutionException("Error on executing user code", e.getCause());
    } catch (final Exception e) {
      throw new CommandExecutionException("Error on analyzing user code", e);
    }

    return ANALYZED_QUERY;
  }

  @Override
  public ResultSet query(final String query, ContextConfiguration configuration, final Map parameters) {
    throw new UnsupportedOperationException("Execution of a query (idempotent) is not supported for polyglot engine. Use command instead");
  }

  @Override
  public ResultSet query(final String query, ContextConfiguration configuration, final Object... parameters) {
    throw new UnsupportedOperationException("Execution of a query (idempotent) is not supported for polyglot engine. Use command instead");
  }

  private ResultSet executeUserCode(final Callable task, final long executionTimeoutMs) throws Exception {
    // IF NOT INITIALIZED, EXECUTE AS SOON AS THE SERVICE STARTS
    final Future future = userCodeExecutor.submit(task);

    try {
      final Object result = executionTimeoutMs > 0 ? future.get(executionTimeoutMs, TimeUnit.MILLISECONDS) : future.get();
      if (result instanceof Exception)
        throw (Exception) result;

      return (ResultSet) result;

    } catch (final TimeoutException e) {
      future.cancel(true); //this method will stop the running underlying task
      throw e;
    }
  }

  private ResultInternal extractResult(final Object o) {
    if (o instanceof Document)
      return new ResultInternal((Document) o);
    else if (o instanceof Identifiable)
      return new ResultInternal((Identifiable) o);
    else if (o instanceof Map)
      return new ResultInternal((Map) o);

    return new ResultInternal().setProperty("value", o);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy