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

org.bidib.wizard.common.script.engine.ScriptEngine Maven / Gradle / Ivy

package org.bidib.wizard.common.script.engine;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;

import org.apache.commons.collections4.CollectionUtils;
import org.bidib.jbidibc.messages.utils.ThreadFactoryBuilder;
import org.bidib.wizard.api.context.ApplicationContext;
import org.bidib.wizard.api.script.ScriptCommand;
import org.bidib.wizard.api.script.ScriptEngineListener;
import org.bidib.wizard.api.script.ScriptOptionAwareCommand;
import org.bidib.wizard.api.script.ScriptStatus;
import org.bidib.wizard.api.script.Scripting;
import org.bidib.wizard.common.script.ScriptExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ScriptEngine {
    private static final Logger LOGGER = LoggerFactory.getLogger(ScriptEngine.class);

    private final Object scriptWorkerLock = new Object();

    // private Thread scriptWorker;
    private Future scriptWorker;

    private AtomicBoolean scriptRunning = new AtomicBoolean(false);

    private AtomicBoolean scriptRepeating = new AtomicBoolean(false);

    private List> scriptCommands;

    private T scripting;

    private final ApplicationContext context;

    private List> engineListeners = new LinkedList>();

    private final ScheduledExecutorService scriptEngineWorkers =
        Executors
            .newScheduledThreadPool(1,
                new ThreadFactoryBuilder().setNameFormat("scriptEngineWorkers-thread-%d").build());

    public ScriptEngine(T scripting, final ApplicationContext context) {
        this.scripting = scripting;
        this.context = context;
    }

    public void addScriptEngineListener(ScriptEngineListener listener) {
        engineListeners.add(listener);
    }

    public void removeScriptEngineListener(ScriptEngineListener listener) {
        engineListeners.remove(listener);
    }

    public void setScriptRepeating(boolean repeating) {
        this.scriptRepeating.set(repeating);
    }

    public void setScriptCommands(List> scriptCommands) {
        LOGGER.info("Set the new script commands.");
        synchronized (scriptWorkerLock) {
            if (scriptRunning.get()) {
                LOGGER
                    .warn(
                        "The script engine is currently processing commands. Stop the engine before setting new commands.");
                throw new IllegalStateException(
                    "The script engine is currently processing commands. Stop the engine before setting new commands.");
            }
            this.scriptCommands = scriptCommands;
        }
    }

    public boolean hasScriptCommands() {
        return CollectionUtils.isNotEmpty(this.scriptCommands);
    }

    public void startScript() {

        startStepScript(cmd -> Boolean.TRUE);
    }

    public void startStepScript(final Function, Boolean> beforeStepCallback) {

        synchronized (scriptWorkerLock) {
            if (scriptCommands != null && scriptWorker == null) {
                LOGGER.info("Create and start scriptWorker.");
                scriptRunning.set(true);

                signalScriptStatus(ScriptStatus.RUNNING);

                scriptWorker = scriptEngineWorkers.submit(() -> {
                    LOGGER.info("Start execution of script commands.");

                    try {

                        int currentExecution = 1;

                        do {

                            if (currentExecution > 1) {
                                scripting.echo("Repeating execution: " + currentExecution);
                            }

                            for (ScriptCommand command : scriptCommands) {

                                if (!scriptRunning.get() || Thread.currentThread().isInterrupted()) {
                                    LOGGER.info("Script execution is stopped.");
                                    break;
                                }

                                processCommand(command, beforeStepCallback);
                            }

                            currentExecution++;
                        }
                        while (scriptRepeating.get() && scriptRunning.get());

                        if (context.get(Scripting.KEY_SCRIPT_ERRORS) != null) {
                            LOGGER.warn("Script errors detected: {}", context.get(Scripting.KEY_SCRIPT_ERRORS));

                            signalScriptStatus(ScriptStatus.FINISHED_WITH_ERRORS);
                        }
                        else {
                            signalScriptStatus(ScriptStatus.FINISHED);
                        }
                    }
                    catch (ScriptExecutionException ex) {
                        LOGGER
                            .warn(
                                "Executing script command has failed. Set the scriptStatus to aborted. Failing script command: {}",
                                ex.getScriptCommand(), ex);

                        if (ex.getScriptCommand() != null) {
                            addError(context, ex.getMessage() + " - Failing command: " + ex.getScriptCommand());
                        }
                        else {
                            addError(context, ex.getMessage());
                        }

                        signalScriptStatus(ScriptStatus.ABORTED);
                    }
                    catch (Exception ex) {
                        LOGGER.warn("Executing script command has failed. Set the scriptStatus to aborted.", ex);

                        addError(context, ex.getMessage());

                        signalScriptStatus(ScriptStatus.ABORTED);
                    }
                    finally {
                        LOGGER.info("Script worker has finished.");

                        // check if an option is set
                        checkOptions(context, beforeStepCallback);

                        // the script has finished
                        scriptRunning.set(false);

                        LOGGER.info("Release the script worker.");
                        scriptWorker = null;
                    }
                });

                LOGGER.info("Start script worker has passed.");
            }
            else if (scriptWorker != null) {
                LOGGER.warn("Script worker is still running.");
            }
        }
    }

    protected void checkOptions(
        final ApplicationContext context, final Function, Boolean> beforeStepCallback) {

        final Map options = context.get(Scripting.KEY_OPTIONS, Map.class);
        if (!options.isEmpty()) {

            try {
                // check if we must disconnect
                Object optionDisconnectOnError = options.get(Scripting.OPTION_DISCONNECT_ON_ERROR);
                if (optionDisconnectOnError instanceof String) {
                    boolean disconnectOnError = Boolean.parseBoolean((String) optionDisconnectOnError);
                    if (disconnectOnError) {
                        LOGGER.info("Disconnect on error is enabled.");
                        if (ScriptStatus.ABORTED == scriptStatus) {

                            ScriptCommand disconnectCommand =
                                this.scriptCommands
                                    .stream().filter(cmd -> cmd instanceof ScriptOptionAwareCommand)
                                    .filter(cmd -> Scripting.OPTION_DISCONNECT_ON_ERROR
                                        .equals(((ScriptOptionAwareCommand) cmd).getOptionKey()))
                                    .findFirst().orElse(null);
                            if (disconnectCommand != null) {
                                LOGGER.info("Submit the disconnect command for execution: {}", disconnectCommand);
                                scriptEngineWorkers.submit(() -> processCommand(disconnectCommand, beforeStepCallback));
                            }
                            else {
                                LOGGER.warn("No disconnect command configured.");
                            }
                        }
                    }
                }
            }
            catch (Exception ex) {
                LOGGER.warn("Evaluate options failed.", ex);
            }
        }
    }

    private void processCommand(
        ScriptCommand command, final Function, Boolean> beforeStepCallback) {
        updateCurrentCommand(command);
        if (beforeStepCallback.apply(command)) {
            command.execute(scripting, context);
        }
        else {
            LOGGER.warn("Execution of steps was aborted by user in beforeStepCallback.");
            throw new ScriptExecutionException("Execution of steps was aborted by user.", command);
        }

    }

    protected void addError(final ApplicationContext context, String errorDescription) {

        List scriptErrors = (List) context.get(Scripting.KEY_SCRIPT_ERRORS);
        if (scriptErrors == null) {
            scriptErrors = new LinkedList<>();
            context.register(Scripting.KEY_SCRIPT_ERRORS, scriptErrors);
        }

        LOGGER.info("Add script error: {}", errorDescription);
        scriptErrors.add(errorDescription);
    }

    /**
     * Initiate stop the script worker but don't wait for termination.
     */
    public void stopScript() {
        stopScript(null);
    }

    /**
     * Initiate stop the script worker and wait at most waitForTermination for termination.
     * 
     * @param waitForTermination
     *            the time to wait in milliseconds
     */
    public void stopScript(Long waitForTermination) {
        scriptRunning.set(false);
        LOGGER.info("Stop the script.");
        synchronized (scriptWorkerLock) {
            if (scriptWorker != null) {
                LOGGER.info("Interrupt the script worker: {}", scriptWorker);
                // scriptWorker.interrupt();
                boolean successful = scriptWorker.cancel(true);
                LOGGER.info("Interrupt the script worker was successful: {}", successful);

                if (waitForTermination != null) {
                    long waitTime = waitForTermination.longValue();
                    LOGGER.info("Wait for termination of script worker for {}ms", waitTime);
                    try {
                        // scriptWorker.join(waitTime);
                        scriptWorker.get(waitTime, TimeUnit.MILLISECONDS);
                    }
                    catch (CancellationException ex) {
                        LOGGER.warn("Wait for termination of script worker has finished with CancellationException.");
                    }
                    catch (InterruptedException | ExecutionException | TimeoutException ex) {
                        LOGGER.warn("Wait for termination of script worker was interrupted.", ex);
                    }

                    // release the script worker instance
                    scriptWorker = null;
                }
                else {
                    LOGGER.info("Do not wait for termination of script worker.");
                }
            }
            else {
                LOGGER.info("Script worker is not available.");
            }
        }
    }

    private ScriptStatus scriptStatus = ScriptStatus.STOPPED;

    private void signalScriptStatus(ScriptStatus scriptStatus) {

        this.scriptStatus = scriptStatus;

        for (ScriptEngineListener listener : engineListeners) {
            listener.scriptStatusChanged(scriptStatus);
        }
    }

    private void updateCurrentCommand(final ScriptCommand command) {
        for (ScriptEngineListener listener : engineListeners) {
            listener.currentCommandChanged(command);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy