<commit_before>package io.bootique.command;

import com.google.inject.Provider;
import io.bootique.BootiqueException;
import io.bootique.cli.Cli;
import io.bootique.cli.CliFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

/**
 * A composite command made of the main command and auxiliary commands run before the main command or in parallel with it.
 *
 * @since 0.25
 */
class MultiCommand extends CommandWithMetadata {

    private final Command mainCommand;
    private final CommandDecorator extraCommands;
    private final Provider<CliFactory> cliFactoryProvider;
    private final Provider<CommandManager> commandManagerProvider;
    private final Provider<ExecutorService> executorProvider;

    public MultiCommand(
            Command mainCommand,
            CommandDecorator extraCommands,
            Provider<CliFactory> cliFactoryProvider,
            Provider<CommandManager> commandManagerProvider,
            Provider<ExecutorService> executorProvider) {

        super(mainCommand.getMetadata());
        this.mainCommand = mainCommand;
        this.cliFactoryProvider = cliFactoryProvider;
        this.commandManagerProvider = commandManagerProvider;
        this.executorProvider = executorProvider;
        this.extraCommands = extraCommands;
    }

    @Override
    public CommandOutcome run(Cli cli) {

        Collection<CommandOutcome> beforeFailures = runBlockingAndGetFailures(extraCommands.getBefore());

        if (beforeFailures.size() > 0) {
            // TODO: combine all results into a single message? or need a different type of CommandOutcome (e.g. MultiCommandOutcome)?
            return CommandOutcome.failed(1, "Some of the commands failed");
        }

        // not waiting for the outcome at least until we start supporting background non-terminating commands
        runNonBlocking(extraCommands.getParallel());

        return mainCommand.run(cli);
    }

    private Collection<CommandOutcome> runBlockingAndGetFailures(Collection<CommandRefWithArgs> cmdRefs) {

        switch (cmdRefs.size()) {
            case 0:
                return Collections.emptyList();
            case 1:
                return doRunBlockingAndGetFailures(cmdRefs.iterator().next());
            default:
                return doRunBlockingAndGetFailures(cmdRefs);
        }
    }

    private Collection<CommandOutcome> doRunBlockingAndGetFailures(CommandRefWithArgs cmdRef) {
        // a single command - we can bypass the thread pool...
        CommandOutcome outcome = run(cmdRef);
        return outcome.isSuccess() ? Collections.emptyList() : Collections.singletonList(outcome);
    }

    private Collection<CommandOutcome> doRunBlockingAndGetFailures(Collection<CommandRefWithArgs> cmdRefs) {

        Collection<CommandOutcome> failures = new ArrayList<>(3);

        runNonBlocking(cmdRefs).forEach(future -> {
            CommandOutcome outcome;

            try {
                outcome = future.get();
            } catch (InterruptedException e) {
                // when interrupted, throw error rather than return CommandOutcome#failed()
                // see comment in toOutcomeSupplier() method for details
                throw new BootiqueException(1, "Interrupted", e);
            } catch (ExecutionException e) {
                // we don't expect futures to ever throw errors
                throw new BootiqueException(1, "Unexpected error", e);
            }

            if (!outcome.isSuccess()) {
                failures.add(outcome);
            }
        });

        return failures;
    }

    private Collection<Future<CommandOutcome>> runNonBlocking(Collection<CommandRefWithArgs> cmdRefs) {

        // exit early, avoid pool creation if we don't need it
        if(cmdRefs.isEmpty()) {
            return Collections.emptyList();
        }

        ExecutorService executor = getExecutor();

        List<Future<CommandOutcome>> futureOutcomes = new ArrayList<>(cmdRefs.size());
        cmdRefs.forEach(ref -> futureOutcomes.add(executor.submit(() -> run(ref))));

        return futureOutcomes;
    }

    private CommandOutcome run(CommandRefWithArgs cmdRef) {

        CommandManager commandManager = getCommandManager();
        Cli cli = getCliFactory().createCli(cmdRef.getArgs());
        Command command = cmdRef.resolve(commandManager);

        CommandOutcome outcome;

        // TODO: we need to distinguish between interrupts and other errors and re-throw interrupts
        // (and require commands to re-throw InterruptedException instead of wrapping it in a CommandOutcome#failed()),
        // because otherwise an interrupt will not be guaranteed to terminate the chain of commands
        try {<commit_msg>Complex command invocation structures #131<commit_after>package io.bootique.command;

import com.google.inject.Provider;
import io.bootique.BootiqueException;
import io.bootique.cli.Cli;
import io.bootique.cli.CliFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

/**
 * A composite command made of the main command and auxiliary commands run before the main command or in parallel with it.
 *
 * @since 0.25
 */
class MultiCommand extends CommandWithMetadata {

    private final Command mainCommand;
    private final CommandDecorator extraCommands;
    private final Provider<CliFactory> cliFactoryProvider;
    private final Provider<CommandManager> commandManagerProvider;
    private final Provider<ExecutorService> executorProvider;

    public MultiCommand(
            Command mainCommand,
            CommandDecorator extraCommands,
            Provider<CliFactory> cliFactoryProvider,
            Provider<CommandManager> commandManagerProvider,
            Provider<ExecutorService> executorProvider) {

        super(mainCommand.getMetadata());
        this.mainCommand = mainCommand;
        this.cliFactoryProvider = cliFactoryProvider;
        this.commandManagerProvider = commandManagerProvider;
        this.executorProvider = executorProvider;
        this.extraCommands = extraCommands;
    }

    @Override
    public CommandOutcome run(Cli cli) {

        Collection<CommandOutcome> beforeResults = runBlocking(extraCommands.getBefore());
        for (CommandOutcome outcome : beforeResults) {
            if (!outcome.isSuccess()) {
                // TODO: combine all results into a single message? or need a different type of CommandOutcome (e.g. MultiCommandOutcome)?
                return CommandOutcome.failed(1, "Some of the commands failed");
            }
        }

        // not waiting for the outcome at least until we start supporting background non-terminating commands
        runNonBlocking(extraCommands.getParallel());

        return mainCommand.run(cli);
    }

    private Collection<CommandOutcome> runBlocking(Collection<CommandRefWithArgs> cmdRefs) {

        switch (cmdRefs.size()) {
            case 0:
                return Collections.emptyList();
            case 1:
                return doRunBlocking(cmdRefs.iterator().next());
            default:
                return doRunBlocking(cmdRefs);
        }
    }

    private Collection<CommandOutcome> doRunBlocking(CommandRefWithArgs cmdRef) {
        // a single command - we can bypass the thread pool...
        return Collections.singletonList(run(cmdRef));
    }

    private Collection<CommandOutcome> doRunBlocking(Collection<CommandRefWithArgs> cmdRefs) {

        Collection<CommandOutcome> outcomes = new ArrayList<>(3);

        runNonBlocking(cmdRefs).forEach(future -> {

            try {
                outcomes.add(future.get());
            } catch (InterruptedException e) {
                // when interrupted, throw error rather than return CommandOutcome#failed()
                // see comment in toOutcomeSupplier() method for details
                throw new BootiqueException(1, "Interrupted", e);
            } catch (ExecutionException e) {
                // we don't expect futures to ever throw errors
                throw new BootiqueException(1, "Unexpected error", e);
            }
        });

        return outcomes;
    }

    private Collection<Future<CommandOutcome>> runNonBlocking(Collection<CommandRefWithArgs> cmdRefs) {

        // exit early, avoid pool creation if we don't need it
        if (cmdRefs.isEmpty()) {
            return Collections.emptyList();
        }

        ExecutorService executor = getExecutor();

        List<Future<CommandOutcome>> futureOutcomes = new ArrayList<>(cmdRefs.size());
        cmdRefs.forEach(ref -> futureOutcomes.add(executor.submit(() -> run(ref))));

        return futureOutcomes;
    }

    private CommandOutcome run(CommandRefWithArgs cmdRef) {

        CommandManager commandManager = getCommandManager();
        Cli cli = getCliFactory().createCli(cmdRef.getArgs());
        Command command = cmdRef.resolve(commandManager);

        CommandOutcome outcome;

        // TODO: we need to distinguish between interrupts and other errors and re-throw interrupts
        // (and require commands to re-throw InterruptedException instead of wrapping it in a CommandOutcome#failed()),
        // because otherwise an interrupt will not be guaranteed to terminate the chain of commands
        try {
{'fraction_non_alphanumeric': 0.08132028321532694, 'fraction_numerical': 0.0022907122032486465, 'mean_word_length': 2.862082830719743, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 50, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1216155', 'n_tokens_mistral': 2431, 'n_tokens_neox': 2328, 'n_words': 651}