com.spotify.styx.cli.Main.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.styx.cli.Main.java

Source

/*-
 * -\-\-
 * Spotify Styx CLI
 * --
 * Copyright (C) 2016 Spotify AB
 * --
 * 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.spotify.styx.cli;

import static com.spotify.styx.serialization.Json.OBJECT_MAPPER;
import static com.spotify.styx.serialization.Json.deserialize;
import static com.spotify.styx.serialization.Json.serialize;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableList;
import com.spotify.apollo.Client;
import com.spotify.apollo.Request;
import com.spotify.apollo.Response;
import com.spotify.apollo.core.Service;
import com.spotify.apollo.core.Services;
import com.spotify.apollo.environment.ApolloEnvironmentModule;
import com.spotify.apollo.http.client.HttpClientModule;
import com.spotify.styx.api.BackfillPayload;
import com.spotify.styx.api.BackfillsPayload;
import com.spotify.styx.api.ResourcesPayload;
import com.spotify.styx.api.RunStateDataPayload;
import com.spotify.styx.model.Backfill;
import com.spotify.styx.model.BackfillBuilder;
import com.spotify.styx.model.BackfillInput;
import com.spotify.styx.model.Event;
import com.spotify.styx.model.Resource;
import com.spotify.styx.model.Workflow;
import com.spotify.styx.model.WorkflowId;
import com.spotify.styx.model.WorkflowInstance;
import com.spotify.styx.model.WorkflowState;
import com.spotify.styx.util.EventUtil;
import com.spotify.styx.util.ParameterUtil;
import java.io.IOException;
import java.net.URLEncoder;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Argument;
import net.sourceforge.argparse4j.inf.ArgumentAction;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import net.sourceforge.argparse4j.inf.Subparsers;
import net.sourceforge.argparse4j.internal.HelpScreenException;
import okio.ByteString;

public final class Main {

    private static final String UTF_8 = "UTF-8";
    private static final String ENV_VAR_PREFIX = "STYX_CLI";
    private static final String STYX_API_ENDPOINT = "/api/v2";
    private static final int TTL_REQUEST = 90;

    private static final String COMMAND_DEST = "command";
    private static final String SUBCOMMAND_DEST = "subcommand";
    private static final String COMPONENT_DEST = "component";
    private static final String WORKFLOW_DEST = "workflow";
    private static final String PARAMETER_DEST = "parameter";

    private static final int EXIT_CODE_SUCCESS = 0;
    private static final int EXIT_CODE_UNKNOWN_ERROR = 1;
    private static final int EXIT_CODE_ARGUMENT_ERROR = 2;
    private static final int EXIT_CODE_API_ERROR = 3;
    private static final String STYX_CLI_VERSION = "Styx CLI " + Main.class.getPackage().getImplementationVersion();

    private final StyxCliParser parser;
    private final Namespace namespace;
    private final String apiHost;
    private final Service cliService;
    private final CliOutput cliOutput;
    private Client client;

    private Main(StyxCliParser parser, Namespace namespace, String apiHost, Service cliService,
            CliOutput cliOutput) {
        this.parser = Objects.requireNonNull(parser);
        this.namespace = Objects.requireNonNull(namespace);
        this.apiHost = Objects.requireNonNull(apiHost);
        this.cliService = Objects.requireNonNull(cliService);
        this.cliOutput = Objects.requireNonNull(cliOutput);
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        final StyxCliParser parser = new StyxCliParser();
        final Namespace namespace;
        final String apiHost;

        try {
            namespace = parser.parser.parseArgs(args);
            apiHost = namespace.getString(parser.host.getDest());
            if (apiHost == null) {
                throw new ArgumentParserException("Styx API host not set", parser.parser);
            }
        } catch (HelpScreenException e) {
            System.exit(EXIT_CODE_SUCCESS);
            return; // needed to convince compiler that flow is interrupted
        } catch (ArgumentParserException e) {
            parser.parser.handleError(e);
            System.exit(EXIT_CODE_ARGUMENT_ERROR);
            return; // needed to convince compiler that flow is interrupted
        }

        final Service cliService = Services.usingName("styx-cli").withEnvVarPrefix(ENV_VAR_PREFIX)
                .withModule(ApolloEnvironmentModule.create()).withModule(HttpClientModule.create()).build();

        final boolean plainOutput = namespace.getBoolean(parser.plain.getDest());
        final boolean jsonOutput = namespace.getBoolean(parser.json.getDest());
        final CliOutput cliOutput;
        if (jsonOutput) {
            cliOutput = new JsonCliOutput();
        } else if (plainOutput) {
            cliOutput = new PlainCliOutput();
        } else {
            cliOutput = new PrettyCliOutput();
        }

        new Main(parser, namespace, apiHost, cliService, cliOutput).run();
    }

    private void run() {
        final Command command = namespace.get(COMMAND_DEST);

        try (Service.Instance instance = cliService.start()) {
            client = ApolloEnvironmentModule.environment(instance).environment().client();

            switch (command) {
            case LIST:
                activeStates();
                break;

            case EVENTS:
                eventsForWorkflowInstance();
                break;

            case TRIGGER:
                triggerWorkflowInstance();
                break;

            case HALT:
                haltWorkflowInstance();
                break;

            case RETRY:
                retryWorkflowInstance();
                break;

            case BACKFILL:
                final BackfillCommand backfillCommand = namespace.get(SUBCOMMAND_DEST);
                switch (backfillCommand) {
                case CREATE:
                    backfillCreate();
                    break;
                case EDIT:
                    backfillEdit();
                    break;
                case HALT:
                    backfillHalt();
                    break;
                case SHOW:
                    backfillShow();
                    break;
                case LIST:
                    backfillList();
                    break;
                default:
                    // parsing unknown command will fail so this would only catch non-exhaustive switches
                    throw new ArgumentParserException(
                            String.format("Unrecognized command: %s %s", command, backfillCommand), parser.parser);
                }
                break;

            case RESOURCE:
                final ResourceCommand resourceCommand = namespace.get(SUBCOMMAND_DEST);
                switch (resourceCommand) {
                case CREATE:
                    resourceCreate();
                    break;
                case EDIT:
                    resourceEdit();
                    break;
                case SHOW:
                    resourceShow();
                    break;
                case LIST:
                    resourceList();
                    break;
                default:
                    // parsing unknown command will fail so this would only catch non-exhaustive switches
                    throw new ArgumentParserException(
                            String.format("Unrecognized command: %s %s", command, resourceCommand), parser.parser);
                }
                break;

            case WORKFLOW:
                final WorkflowCommand workflowCommand = namespace.get(SUBCOMMAND_DEST);
                switch (workflowCommand) {
                case SHOW:
                    workflowShow();
                    break;
                default:
                    // parsing unknown command will fail so this would only catch non-exhaustive switches
                    throw new ArgumentParserException(
                            String.format("Unrecognized command: %s %s", command, workflowCommand), parser.parser);
                }
                break;

            default:
                // parsing unknown command will fail so this would only catch non-exhaustive switches
                throw new ArgumentParserException("Unrecognized command: " + command, parser.parser);
            }
        } catch (ArgumentParserException e) {
            parser.parser.handleError(e);
            System.exit(EXIT_CODE_ARGUMENT_ERROR);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            System.exit(EXIT_CODE_UNKNOWN_ERROR);
        } catch (Exception e) {
            System.err.println(e.getClass().getSimpleName() + ": " + e.getMessage());
            System.exit(EXIT_CODE_API_ERROR);
        }
    }

    private void backfillCreate() throws ExecutionException, InterruptedException, IOException {
        final String component = namespace.getString(parser.backfillCreateComponent.getDest());
        final String workflow = namespace.getString(parser.backfillCreateWorkflow.getDest());
        final String start = namespace.getString(parser.backfillCreateStart.getDest());
        final String end = namespace.getString(parser.backfillCreateEnd.getDest());
        final int concurrency = namespace.getInt(parser.backfillCreateConcurrency.getDest());

        final BackfillInput backfillInput = BackfillInput.create(Instant.parse(start), Instant.parse(end),
                component, workflow, concurrency);

        final ByteString payload = serialize(backfillInput);
        final ByteString response = send(Request.forUri(apiUrl("backfills"), "POST").withPayload(payload));
        final Backfill backfill = deserialize(response, Backfill.class);
        cliOutput.printBackfill(backfill);
    }

    private void backfillEdit() throws ExecutionException, InterruptedException, IOException {
        final Integer concurrency = namespace.getInt(parser.backfillEditConcurrency.getDest());
        final String id = namespace.getString(parser.backfillEditId.getDest());

        final ByteString getResponse = send(Request.forUri(apiUrl("backfills", id)));
        final BackfillPayload backfillPayload = deserialize(getResponse, BackfillPayload.class);
        final BackfillBuilder editedBackfillBuilder = backfillPayload.backfill().builder();
        if (concurrency != null) {
            editedBackfillBuilder.concurrency(concurrency);
        }
        final ByteString putPayload = serialize(editedBackfillBuilder.build());
        final ByteString putResponse = send(Request.forUri(apiUrl("backfills", id), "PUT").withPayload(putPayload));

        final Backfill newBackfill = deserialize(putResponse, Backfill.class);
        cliOutput.printBackfill(newBackfill);
    }

    private void backfillHalt() throws ExecutionException, InterruptedException {
        final String id = namespace.getString(parser.backfillHaltId.getDest());
        send(Request.forUri(apiUrl("backfills", id), "DELETE"));
    }

    private void backfillShow() throws ExecutionException, InterruptedException, IOException {
        final String id = namespace.getString(parser.backfillShowId.getDest());
        final ByteString response = send(Request.forUri(apiUrl("backfills", id)));
        final BackfillPayload backfillStatus = deserialize(response, BackfillPayload.class);
        cliOutput.printBackfillPayload(backfillStatus);
    }

    private void backfillList() throws IOException, ExecutionException, InterruptedException {
        String uri = apiUrl("backfills");
        final List<String> queries = new ArrayList<>();
        if (namespace.getBoolean(parser.backfillListShowAll.getDest())) {
            queries.add("showAll=true");
        }
        final String component = namespace.getString(parser.backfillListComponent.getDest());
        if (component != null) {
            queries.add("component=" + URLEncoder.encode(component, UTF_8));
        }
        final String workflow = namespace.getString(parser.backfillListWorkflow.getDest());
        if (workflow != null) {
            queries.add("workflow=" + URLEncoder.encode(workflow, UTF_8));
        }
        if (!queries.isEmpty()) {
            uri += "?" + String.join("&", queries);
        }
        final ByteString response = send(Request.forUri(uri));
        final BackfillsPayload backfills = deserialize(response, BackfillsPayload.class);
        cliOutput.printBackfills(backfills.backfills());
    }

    private void resourceCreate() throws ExecutionException, InterruptedException, IOException {
        final String id = namespace.getString(parser.resourceCreateId.getDest());
        final int concurrency = namespace.getInt(parser.resourceCreateConcurrency.getDest());

        final ByteString payload = serialize(Resource.create(id, concurrency));
        final ByteString response = send(Request.forUri(apiUrl("resources"), "POST").withPayload(payload));
        final Resource resource = deserialize(response, Resource.class);
        cliOutput.printResources(Collections.singletonList(resource));
    }

    private void resourceEdit() throws ExecutionException, InterruptedException, IOException {
        final String id = namespace.getString(parser.resourceEditId.getDest());
        final Integer concurrency = namespace.getInt(parser.resourceEditConcurrency.getDest());

        final ByteString getResponse = send(Request.forUri(apiUrl("resources", id)));
        Resource resource = deserialize(getResponse, Resource.class);
        if (concurrency != null) {
            resource = Resource.create(resource.id(), concurrency);
        }
        final ByteString putPayload = serialize(resource);
        final ByteString putResponse = send(Request.forUri(apiUrl("resources", id), "PUT").withPayload(putPayload));

        final Resource newResource = deserialize(putResponse, Resource.class);
        cliOutput.printResources(Collections.singletonList(newResource));
    }

    private void resourceShow() throws ExecutionException, InterruptedException, IOException {
        final String id = namespace.getString(parser.resourceShowId.getDest());
        final ByteString response = send(Request.forUri(apiUrl("resources", id)));
        final Resource resource = deserialize(response, Resource.class);
        cliOutput.printResources(Collections.singletonList(resource));
    }

    private void resourceList() throws IOException, ExecutionException, InterruptedException {
        final ByteString response = send(Request.forUri(apiUrl("resources")));
        final ResourcesPayload resources = deserialize(response, ResourcesPayload.class);
        cliOutput.printResources(resources.resources());
    }

    private void workflowShow() throws ExecutionException, InterruptedException, IOException {
        final String cid = namespace.getString(parser.workflowShowComponentId.getDest());
        final String wid = namespace.getString(parser.workflowShowWorkflowId.getDest());

        // Fetch config and state in parallel
        final CompletableFuture<Response<ByteString>> workflowResponse = sendAsync(
                Request.forUri(apiUrl("workflows", cid, wid))).toCompletableFuture();
        final CompletableFuture<Response<ByteString>> stateResponse = sendAsync(
                Request.forUri(apiUrl("workflows", cid, wid, "state"))).toCompletableFuture();

        // Wait for both responses
        CompletableFuture.allOf(workflowResponse, stateResponse).get();

        final ByteString workflowPayload = requireSuccess(workflowResponse.get()).payload()
                .orElse(ByteString.EMPTY);
        final ByteString statePayload = requireSuccess(stateResponse.get()).payload().orElse(ByteString.EMPTY);

        final Workflow workflow = deserialize(workflowPayload, Workflow.class);
        final WorkflowState workflowState = deserialize(statePayload, WorkflowState.class);

        cliOutput.printWorkflow(workflow, workflowState);
    }

    private void activeStates() throws IOException, ExecutionException, InterruptedException {
        String uri = apiUrl("status", "activeStates");
        final String component = namespace.getString(parser.listComponent.getDest());
        if (component != null) {
            uri += "?component=" + URLEncoder.encode(component, UTF_8);
        }

        final ByteString response = send(Request.forUri(uri).withTtl(Duration.ofSeconds(TTL_REQUEST)));
        final RunStateDataPayload runStateDataPayload = deserialize(response, RunStateDataPayload.class);
        cliOutput.printStates(runStateDataPayload);
    }

    private void eventsForWorkflowInstance() throws ExecutionException, InterruptedException, IOException {
        final WorkflowInstance workflowInstance = getWorkflowInstance(namespace);
        final String component = workflowInstance.workflowId().componentId();
        final String workflow = workflowInstance.workflowId().id();
        final String parameter = workflowInstance.parameter();

        final ByteString response = send(Request.forUri(apiUrl("status", "events", component, workflow, parameter))
                .withTtl(Duration.ofSeconds(TTL_REQUEST)));

        final JsonNode jsonNode = OBJECT_MAPPER.readTree(response.toByteArray());

        if (!jsonNode.isObject()) {
            throw new RuntimeException("Invalid json returned from API");
        }

        final ObjectNode json = (ObjectNode) jsonNode;
        final ArrayNode events = json.withArray("events");
        final ImmutableList.Builder<CliOutput.EventInfo> eventInfos = ImmutableList.builder();
        for (JsonNode eventWithTimestamp : events) {
            final long ts = eventWithTimestamp.get("timestamp").asLong();
            final JsonNode event = eventWithTimestamp.get("event");

            String eventName;
            String eventInfo;
            try {
                Event typedEvent = OBJECT_MAPPER.convertValue(event, Event.class);
                eventName = EventUtil.name(typedEvent);
                eventInfo = CliUtil.info(typedEvent);
            } catch (IllegalArgumentException e) {
                // fall back to just inspecting the json
                eventName = event.get("@type").asText();
                eventInfo = "";
            }

            eventInfos.add(CliOutput.EventInfo.create(ts, eventName, eventInfo));
        }

        cliOutput.printEvents(eventInfos.build());
    }

    private void triggerWorkflowInstance()
            throws ExecutionException, InterruptedException, JsonProcessingException {
        final WorkflowInstance workflowInstance = getWorkflowInstance(namespace);
        final ByteString payload = serialize(workflowInstance);
        send(Request.forUri(apiUrl("scheduler", "trigger"), "POST").withPayload(payload));
        cliOutput.printMessage("Triggered! Use `styx ls -c " + workflowInstance.workflowId().componentId()
                + "` to check active workflow instances.");
    }

    private void haltWorkflowInstance() throws ExecutionException, InterruptedException, JsonProcessingException {
        final WorkflowInstance workflowInstance = getWorkflowInstance(namespace);
        final ByteString payload = serialize(Event.halt(workflowInstance));
        send(Request.forUri(apiUrl("scheduler", "events"), "POST").withPayload(payload));
    }

    private void retryWorkflowInstance() throws ExecutionException, InterruptedException, JsonProcessingException {
        final WorkflowInstance workflowInstance = getWorkflowInstance(namespace);
        final ByteString payload = serialize(Event.dequeue(workflowInstance));
        send(Request.forUri(apiUrl("scheduler", "events"), "POST").withPayload(payload));
    }

    private static WorkflowInstance getWorkflowInstance(Namespace namespace) {
        return WorkflowInstance.create(
                WorkflowId.create(namespace.getString(COMPONENT_DEST), namespace.getString(WORKFLOW_DEST)),
                namespace.getString(PARAMETER_DEST));
    }

    private String apiUrl(CharSequence... parts) {
        return "http://" + apiHost + STYX_API_ENDPOINT + "/" + String.join("/", parts);
    }

    private ByteString send(Request request) throws ExecutionException, InterruptedException {
        // TODO: Move requireSuccess use into callers of send()
        return requireSuccess(sendAsync(request).toCompletableFuture().get()).payload().orElse(ByteString.EMPTY);
    }

    private static <T> Response<T> requireSuccess(Response<T> response) {
        switch (response.status().family()) {
        case SUCCESSFUL:
            return response;
        default:
            throw new RuntimeException(
                    String.format("API error: %d %s", response.status().code(), response.status().reasonPhrase()));
        }
    }

    private CompletionStage<Response<ByteString>> sendAsync(Request request)
            throws ExecutionException, InterruptedException {
        return client.send(request.withHeader("User-Agent", STYX_CLI_VERSION));
    }

    private static class StyxCliParser {

        final ArgumentParser parser = ArgumentParsers.newArgumentParser("styx").description("Styx CLI")
                .version(STYX_CLI_VERSION);

        final PartitionAction partitionAction = new PartitionAction();

        final Subparsers subCommands = parser.addSubparsers().title("commands").metavar(" ");

        final Subparsers backfillParser = Command.BACKFILL.parser(subCommands).addSubparsers().title("commands")
                .metavar(" ");

        final Subparser backfillShow = BackfillCommand.SHOW.parser(backfillParser);
        final Argument backfillShowId = backfillShow.addArgument("backfill").help("Backfill ID");

        final Subparser backfillEdit = BackfillCommand.EDIT.parser(backfillParser);
        final Argument backfillEditId = backfillEdit.addArgument("backfill").help("Backfill ID");
        final Argument backfillEditConcurrency = backfillEdit.addArgument("--concurrency")
                .help("set the concurrency value for the backfill").type(Integer.class);

        final Subparser backfillHalt = BackfillCommand.HALT.parser(backfillParser);
        final Argument backfillHaltId = backfillHalt.addArgument("backfill").help("Backfill ID");

        final Subparser backfillList = BackfillCommand.LIST.parser(backfillParser);
        final Argument backfillListWorkflow = backfillList.addArgument("-w", "--workflow")
                .help("only show backfills for WORKFLOW");
        final Argument backfillListComponent = backfillList.addArgument("-c", "--component")
                .help("only show backfills for COMPONENT");
        final Argument backfillListShowAll = backfillList.addArgument("-a", "--show-all").setDefault(false)
                .action(Arguments.storeTrue()).help("show all backfills, even halted and all-triggered ones");

        final Subparser backfillCreate = BackfillCommand.CREATE.parser(backfillParser);
        final Argument backfillCreateComponent = backfillCreate.addArgument("component").help("Component ID");
        final Argument backfillCreateWorkflow = backfillCreate.addArgument("workflow").help("Workflow ID");
        final Argument backfillCreateStart = backfillCreate.addArgument("start")
                .help("Start date/datehour (inclusive)").action(partitionAction);
        final Argument backfillCreateEnd = backfillCreate.addArgument("end").help("End date/datehour (exclusive)")
                .action(partitionAction);
        final Argument backfillCreateConcurrency = backfillCreate.addArgument("concurrency")
                .help("The number of jobs to run in parallel").type(Integer.class);

        final Subparsers resourceParser = Command.RESOURCE.parser(subCommands).addSubparsers().title("commands")
                .metavar(" ");

        final Subparser resourceShow = ResourceCommand.SHOW.parser(resourceParser);
        final Argument resourceShowId = resourceShow.addArgument("id").help("Resource ID");

        final Subparser resourceEdit = ResourceCommand.EDIT.parser(resourceParser);
        final Argument resourceEditId = resourceEdit.addArgument("id").help("Resource ID");
        final Argument resourceEditConcurrency = resourceEdit.addArgument("--concurrency")
                .help("set the concurrency value for the resource").type(Integer.class);

        final Subparser resourceList = ResourceCommand.LIST.parser(resourceParser);

        final Subparser resourceCreate = ResourceCommand.CREATE.parser(resourceParser);
        final Argument resourceCreateId = resourceCreate.addArgument("id").help("Resource ID");
        final Argument resourceCreateConcurrency = resourceCreate.addArgument("concurrency")
                .help("The concurrency of this resource").type(Integer.class);

        final Subparsers workflowParser = Command.WORKFLOW.parser(subCommands).addSubparsers().title("commands")
                .metavar(" ");

        final Subparser workflowShow = WorkflowCommand.SHOW.parser(workflowParser);
        final Argument workflowShowComponentId = workflowShow.addArgument("component").help("Component ID");
        final Argument workflowShowWorkflowId = workflowShow.addArgument("workflow").help("Workflow ID");

        final Subparser list = Command.LIST.parser(subCommands);
        final Argument listComponent = list.addArgument("-c", "--component")
                .help("only show instances for COMPONENT");

        final Subparser events = addWorkflowInstanceArguments(Command.EVENTS.parser(subCommands));
        final Subparser trigger = addWorkflowInstanceArguments(Command.TRIGGER.parser(subCommands));
        final Subparser halt = addWorkflowInstanceArguments(Command.HALT.parser(subCommands));
        final Subparser retry = addWorkflowInstanceArguments(Command.RETRY.parser(subCommands));

        final Argument host = parser.addArgument("-H", "--host")
                .help("Styx API host (can also be set with environment variable " + ENV_VAR_PREFIX + "_HOST)")
                .setDefault(System.getenv(ENV_VAR_PREFIX + "_HOST")).action(Arguments.store());

        final Argument json = parser.addArgument("--json").help("json output").setDefault(false)
                .action(Arguments.storeTrue());

        final Argument plain = parser.addArgument("-p", "--plain").help("plain output").setDefault(false)
                .action(Arguments.storeTrue());

        final Argument version = parser.addArgument("--version").action(Arguments.version());

        private static Subparser addWorkflowInstanceArguments(Subparser subparser) {
            subparser.addArgument(COMPONENT_DEST).help("Component id");
            subparser.addArgument(WORKFLOW_DEST).help("Workflow id");
            subparser.addArgument(PARAMETER_DEST)
                    .help("Parameter identifying the workflow instance, e.g. '2016-09-14' or '2016-09-14T17'");
            return subparser;
        }
    }

    private enum Command {
        LIST("ls", "List active workflow instances"), EVENTS("e", "List events for a workflow instance"), HALT("h",
                "Halt a workflow instance"), TRIGGER("t", "Trigger a completed workflow instance"), RETRY("r",
                        "Retry a workflow instance that is in a waiting state"), RESOURCE(null,
                                "Commands related to resources"), BACKFILL(null,
                                        "Commands related to backfills"), WORKFLOW(null,
                                                "Commands related to workflows");

        private final String alias;
        private final String description;

        Command(String alias, String description) {
            this.alias = alias;
            this.description = description;
        }

        public Subparser parser(Subparsers subCommands) {
            Subparser subparser = subCommands.addParser(name().toLowerCase()).setDefault(COMMAND_DEST, this)
                    .description(description).help(description);

            if (alias != null && !alias.isEmpty()) {
                subparser.aliases(alias);
            }

            return subparser;
        }
    }

    private enum BackfillCommand {
        LIST("ls", "List active backfills. Use option -a (--show-all) to show all"), CREATE("",
                "Create a backfill"), EDIT("e", "Edit a backfill"), HALT("h",
                        "Halt a backfill"), SHOW("get", "Show info about a specific backfill");

        private final String alias;
        private final String description;

        BackfillCommand(String alias, String description) {
            this.alias = alias;
            this.description = description;
        }

        public Subparser parser(Subparsers subCommands) {
            final Subparser subparser = subCommands.addParser(name().toLowerCase())
                    .setDefault(SUBCOMMAND_DEST, this).description(description).help(description);

            if (alias != null && !alias.isEmpty()) {
                subparser.aliases(alias);
            }

            return subparser;
        }
    }

    private enum ResourceCommand {
        LIST("ls", "List resources"), CREATE("", "Create a resource"), EDIT("e", "Edit a resource"), SHOW("get",
                "Show info about a specific resource");

        private final String alias;
        private final String description;

        ResourceCommand(String alias, String description) {
            this.alias = alias;
            this.description = description;
        }

        public Subparser parser(Subparsers subCommands) {
            final Subparser subparser = subCommands.addParser(name().toLowerCase())
                    .setDefault(SUBCOMMAND_DEST, this).description(description).help(description);

            if (alias != null && !alias.isEmpty()) {
                subparser.aliases(alias);
            }

            return subparser;
        }
    }

    private enum WorkflowCommand {
        SHOW("get", "Show info about a specific workflow");

        private final String alias;
        private final String description;

        WorkflowCommand(String alias, String description) {
            this.alias = alias;
            this.description = description;
        }

        public Subparser parser(Subparsers subCommands) {
            final Subparser subparser = subCommands.addParser(name().toLowerCase())
                    .setDefault(SUBCOMMAND_DEST, this).description(description).help(description);

            if (alias != null && !alias.isEmpty()) {
                subparser.aliases(alias);
            }

            return subparser;
        }
    }

    private static class PartitionAction implements ArgumentAction {

        @Override
        public void run(ArgumentParser parser, Argument arg, Map<String, Object> attrs, String flag, Object value)
                throws ArgumentParserException {
            try {
                attrs.put(arg.getDest(), ParameterUtil.parseDateHour(value.toString()));
            } catch (DateTimeParseException dateHourException) {
                try {
                    attrs.put(arg.getDest(), ParameterUtil.parseDate(value.toString()));
                } catch (Exception dateException) {
                    throw new ArgumentParserException(String.format(
                            "could not parse date/datehour for parameter '%s'; if datehour: [%s], if date: [%s]",
                            arg.textualName(), dateHourException.getMessage(), dateException.getMessage()), parser);
                }
            }
        }

        @Override
        public void onAttach(Argument arg) {
        }

        @Override
        public boolean consumeArgument() {
            return true;
        }
    }
}