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

org.cp.elements.process.java.EmbeddedJavaProcessExecutor Maven / Gradle / Ivy

/*
 * Copyright 2011-Present Author or Authors.
 *
 * 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 org.cp.elements.process.java;

import static java.util.Arrays.asList;
import static org.cp.elements.lang.ClassUtils.getName;
import static org.cp.elements.lang.ClassUtils.getSimpleName;
import static org.cp.elements.lang.ClassUtils.isConstructorWithArrayParameter;
import static org.cp.elements.lang.ClassUtils.isDefaultConstructor;
import static org.cp.elements.lang.ElementsExceptionsFactory.newEmbeddedProcessExecutionException;
import static org.cp.elements.lang.ObjectUtils.isNullOrEqualTo;
import static org.cp.elements.util.ArrayUtils.indexOf;
import static org.cp.elements.util.ArrayUtils.nullSafeArray;
import static org.cp.elements.util.ArrayUtils.toStringArray;
import static org.cp.elements.util.stream.StreamUtils.stream;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.Callable;

import org.cp.elements.io.FileSystemUtils;
import org.cp.elements.lang.Assert;
import org.cp.elements.lang.ClassUtils;
import org.cp.elements.lang.Executable;
import org.cp.elements.lang.ObjectUtils;
import org.cp.elements.lang.StringUtils;
import org.cp.elements.lang.annotation.NullSafe;
import org.cp.elements.process.EmbeddedProcessExecutionException;
import org.cp.elements.process.ProcessExecutor;

/**
 * Launches an embedded Java process.
 *
 * @author John Blum
 * @see java.io.File
 * @see org.cp.elements.lang.Executable
 * @see org.cp.elements.process.ProcessExecutor
 * @since 1.0.0
 */
@SuppressWarnings({ "rawtypes", "unused" })
public class EmbeddedJavaProcessExecutor implements ProcessExecutor {

  protected static final Collection JAVA_CLASS_EXECUTORS = Collections.unmodifiableList(asList(
    new ExecutableExecutor<>(), new RunnableExecutor<>(), new CallableExecutor<>(), new MainMethodExecutor<>()));

  /**
   * Factory method used to construct an instance of the {@link EmbeddedJavaProcessExecutor}, which is used to execute
   * Java {@link Class Classes} embedded in the currently running Java program.
   *
   * @return a new instance of the {@link EmbeddedJavaProcessExecutor} class.
   * @see org.cp.elements.process.java.EmbeddedJavaProcessExecutor
   */
  public static EmbeddedJavaProcessExecutor newEmbeddedJavaProcessExecutor() {
    return new EmbeddedJavaProcessExecutor();
  }

  /**
   * Executes the given {@link String command-line} executable in the given {@link File directory}.
   *
   * @return {@link Void}.  The Java {@link Process} is run in embedded mode and therefore will not
   * have a forked {@link Process}.
   * @throws IllegalArgumentException if the {@link File directory} is the current working directory or {@literal null},
   * or the Java {@link Class} could not be resolved from the given command-line.
   * @see org.cp.elements.process.ProcessExecutor#execute(File, String...)
   * @see #execute(Class, String...)
   * @see java.io.File
   */
  @Override
  public Void execute(File directory, String... commandLine) {

    Assert.isTrue(isNullOrEqualTo(directory, FileSystemUtils.WORKING_DIRECTORY),
      "The Java class can only be ran in the same working directory [%1$s] as the containing process"
        + "; directory was [%2$s]", FileSystemUtils.WORKING_DIRECTORY, directory);

    Class type = resolveJavaClassFrom(commandLine);

    Assert.notNull(type, "The Java class to execute could not be resolved from the given command-line [%s]",
      Arrays.toString(commandLine));

    String[] args = resolveArgumentsFrom(type, commandLine);

    execute(type, args);

    return null;
  }

  /**
   * Resolves the arguments to the Java program identified by the given {@link Class} type
   * from the given array of {@link String arguments}.
   *
   * @param type Java {@link Class} containing the {@literal main} method of the Java program;
   * must not be {@literal null}.
   * @param args array of {@link String arguments} from which to extract the Java program arguments.
   * @return the array of {@link String arguments} to be passed to the Java program.
   */
  protected String[] resolveArgumentsFrom(Class type, String... args) {

    int index = indexOf(args, type.getName());
    int position = (index + 1);

    String[] arguments = StringUtils.EMPTY_STRING_ARRAY;

    if (index != -1 && position < args.length) {
      int length = (args.length - position);
      arguments = new String[length];
      System.arraycopy(args, position, arguments, 0, length);
    }

    return arguments;
  }

  /**
   * Resolves the Java {@link Class} containing the {@literal main} method to the Java program to execute
   * from the given array of {@link String arguments}.
   *
   * @param  {@link Class} type of the main Java program.
   * @param args array of {@link String arguments} from which to extract the main Java {@link Class}
   * of the Java program.
   * @return the Java {@link Class} containing the {@literal main} method to the Java program.
   * @see java.lang.Class
   */
  protected  Class resolveJavaClassFrom(String... args) {
    return Arrays.stream(nullSafeArray(args, String.class)).filter(ObjectUtils::isPresent).findFirst()
      .>map(ObjectUtils::loadClass).orElse(null);
  }

  /**
   * Executes the given Java {@link Class} passing the given array of {@link String arguments}.
   *
   * This execute method employs a {@link JavaClassExecutor} strategy to execute the given Java {@link Class}.
   * The Java {@link Class} is expected to either implement {@link Runnable}, {@link Callable}, {@link Executable}
   * or implement a {@literal main} {@link Method}.
   *
   * @param  {@link Class} type of the Java program return value.
   * @param type Java {@link Class} to execute in embedded mode.
   * @param args array of {@link String arguments} to pass to the Java {@link Class}.
   * @return an {@link Optional} return value from the execution of the Java {@link Class}.
   * @throws IllegalArgumentException if the Java {@link Class} is {@literal null}.
   * @throws EmbeddedProcessExecutionException if the Java {@link Class} is not executable.
   * @see org.cp.elements.process.java.EmbeddedJavaProcessExecutor.JavaClassExecutor
   * @see java.lang.Class
   * @see java.util.Optional
   */
  @SuppressWarnings("unchecked")
  public  Optional execute(Class type, String... args) {

    Assert.notNull(type, "Class type must not be null");

    return JAVA_CLASS_EXECUTORS.stream().filter(javaClassExecutor -> javaClassExecutor.isExecutable(type))
      .findFirst().map(javaClassExecutor -> javaClassExecutor.execute(type, (Object[]) args)).orElseThrow(() ->
        new EmbeddedProcessExecutionException(String.format("Unable to execute Java class [%s];"
          + " Please verify that your class either implements Runnable, Callable, Executable or has a main method",
            getName(type))));
  }

  /**
   * Strategy interface for executing Java {@link Class Classes}.
   *
   * @param  {@link Class} type of the executions return value (result).
   */
  interface JavaClassExecutor {

    /**
     * Determines whether the given Java {@link Class} is executable by this executor.
     *
     * @param type Java {@link Class} to evaluate.
     * @return a boolean value indicating whether this executor can execute the given Java {@link Class}.
     * @see java.lang.Class
     */
    boolean isExecutable(Class type);

    /**
     * Executes the given Java {@link Class} passing the array of {@link String arguments} used during execution.
     *
     * @param type Java {@link Class} to execute.
     * @param args array of {@link String arguments} passed during execution.
     * @return the {@link Optional} return value (result) from the execution.
     * @see java.lang.Class
     * @see java.util.Optional
     */
    Optional execute(Class type, Object... args);

    @NullSafe
    default boolean isTargetConstructor(Constructor constructor) {

      return Optional.ofNullable(constructor).map(localConstructor ->
        isDefaultConstructor(constructor) || isConstructorWithArrayParameter(constructor))
          .orElse(false);
    }

    @SuppressWarnings("unchecked")
    default  Constructor findConstructor(Class type) {

      return (Constructor) stream(nullSafeArray(type.getDeclaredConstructors(), Constructor.class))
        .filter(this::isTargetConstructor)
        .min((constructorOne, constructorTwo) ->
          constructorTwo.getParameterCount() - constructorOne.getParameterCount())
        .orElseThrow(() -> new EmbeddedProcessExecutionException(String.format(
          "No default constructor or constructor with arguments (%1$s(:Object[]) for type [%2$s] was found",
            getSimpleName(type), getName(type))));
    }

    default  T constructInstance(Class type, Object[] args) {

      try {
        Constructor constructor = findConstructor(type);

        constructor.setAccessible(true);

        return (isConstructorWithArrayParameter(constructor) ? constructor.newInstance((Object) args)
          : constructor.newInstance());
      }
      catch (IllegalAccessException | InstantiationException | InvocationTargetException cause) {
        throw newEmbeddedProcessExecutionException(cause, "Failed to construct an instance of Java class [%s]",
          getName(type));
      }
    }
  }

  /**
   * Execution strategy for executing {@link Callable} Java {@link Class Classes}.
   *
   * @param  {@link Class} type of the executions return value (result).
   * @see java.util.concurrent.Callable
   */
  static class CallableExecutor implements JavaClassExecutor {

    @Override
    public boolean isExecutable(Class type) {
      return Callable.class.isAssignableFrom(type);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Optional execute(Class type, Object... args) {

      try {
        Callable callable = this.>constructInstance(type, args);
        return Optional.ofNullable(callable.call());
      }
      catch (Exception cause) {
        throw newEmbeddedProcessExecutionException(cause, "Failed to call Java class [%s]", getName(type));
      }
    }
  }

  /**
   * Execution strategy for executing {@link Executable} Java {@link Class Classes}.
   *
   * @param  {@link Class} type of the executions return value (result).
   * @see org.cp.elements.lang.Executable
   */
  static class ExecutableExecutor implements JavaClassExecutor {

    @Override
    public boolean isExecutable(Class type) {
      return Executable.class.isAssignableFrom(type);
    }

    @SuppressWarnings("unchecked")
    public Optional execute(Class type, Object... args) {
      Executable executable = this.>constructInstance(type, args);
      return Optional.ofNullable(executable.execute(args));
    }
  }

  /**
   * Execution strategy for executing Java {@link Class Classes} having a {@literal main} {@link Method}.
   *
   * @param  {@link Class} type of the executions return value (result).
   */
  static class MainMethodExecutor implements JavaClassExecutor {

    @Override
    public boolean isExecutable(Class type) {
      return Arrays.stream(type.getDeclaredMethods()).anyMatch(ClassUtils::isMainMethod);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Optional execute(Class type, Object... args) {

      try {
        Method mainMethod = type.getDeclaredMethod(ClassUtils.MAIN_METHOD_NAME, String[].class);

        mainMethod.invoke(null, (Object) toStringArray(args));

        return Optional.empty();
      }
      catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException cause) {
        throw newEmbeddedProcessExecutionException(cause, "Failed to execute Java class [%s] using main method",
          getName(type));
      }
    }
  }

  /**
   * Execution strategy for executing {@link Runnable} Java {@link Class Classes}.
   *
   * @param  {@link Class} type of the executions return value (result).
   * @see java.lang.Runnable
   */
  static class RunnableExecutor implements JavaClassExecutor {

    @Override
    public boolean isExecutable(Class type) {
      return Runnable.class.isAssignableFrom(type);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Optional execute(Class type, Object... args) {

      Runnable runnable = this.constructInstance(type, args);

      runnable.run();

      return Optional.empty();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy