org.hawkular.agent.commandcli.CommandCli.java Source code

Java tutorial

Introduction

Here is the source code for org.hawkular.agent.commandcli.CommandCli.java

Source

/*
 * Copyright 2015-2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * 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.hawkular.agent.commandcli;

import java.io.BufferedInputStream;
import java.io.Console;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.LogManager;
import java.util.regex.Pattern;

import org.hawkular.bus.common.BasicMessage;
import org.hawkular.bus.common.BasicMessageWithExtraData;
import org.hawkular.bus.common.BinaryData;
import org.hawkular.cmdgw.api.ApiDeserializer;
import org.jboss.aesh.cl.CommandLine;
import org.jboss.aesh.cl.internal.OptionType;
import org.jboss.aesh.cl.internal.ProcessedCommand;
import org.jboss.aesh.cl.internal.ProcessedCommandBuilder;
import org.jboss.aesh.cl.internal.ProcessedOptionBuilder;
import org.jboss.aesh.cl.parser.CommandLineParser;
import org.jboss.aesh.cl.parser.CommandLineParserBuilder;
import org.jboss.aesh.cl.parser.CommandLineParserException;
import org.jboss.logging.Logger;

import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.ws.WebSocket;
import com.squareup.okhttp.ws.WebSocketCall;
import com.squareup.okhttp.ws.WebSocketListener;

import okio.Buffer;
import okio.BufferedSink;

/**
 * A simple CLI that lets you send JSON commands to a Hawkular Server.
 */
public class CommandCli {
    // read our logging config from our jar if the user didn't provide one
    static {
        if (System.getProperty("java.util.logging.config.file") == null) {
            String filename = Boolean.getBoolean("debug") ? "/logging-debug.properties" : "/logging.properties";
            final InputStream inputStream = CommandCli.class.getResourceAsStream(filename);
            try {
                LogManager.getLogManager().readConfiguration(inputStream);
            } catch (final IOException e) {
                java.util.logging.Logger.getAnonymousLogger().severe("Could not load " + filename);
                java.util.logging.Logger.getAnonymousLogger().severe(e.getMessage());
            }
        }
    }

    private static final Logger log = Logger.getLogger(CommandCli.class);

    private static final String COMMAND_NAME = "hawkular-command";

    private static final String OPT_OUTPUT_DIR = "output-dir"; // where to put the response output
    private static final String OPT_SERVER_WEB_SOCKET_URL = "server-url"; // where the hawkular server is
    private static final String OPT_USERNAME = "username";
    private static final String OPT_PASSWORD = "password";
    private static final String OPT_COMMAND = "command";
    private static final String OPT_EXPECTED_RESPONSE = "expected-response";
    private static final String OPT_REQUEST_FILE = "request-file";
    private static final String OPT_BINARY_DATA_FILE = "binary-data-file";
    private static final char OPT_PROPERTY = 'P';

    private static class Config {
        File outputDir;
        String serverUrl;
        String username;
        String password;
        String command;
        String expectedResponse;
        String jsonRequest;
        File binaryDataFile;

        public String toString() {
            StringBuilder str = new StringBuilder("CLI Configuration:\n");
            str.append("Server URL:        ").append(this.serverUrl).append("\n");
            str.append("Username:          ").append(this.username).append("\n");
            str.append("Password:          ").append("***").append("\n");
            str.append("Output Directory:  ").append(this.outputDir).append("\n");
            str.append("Command:           ").append(this.command).append("\n");
            str.append("Expected Response: ").append(this.expectedResponse).append("\n");
            str.append("Binary Data File:  ").append(this.binaryDataFile).append("\n");
            str.append("JSON Request:      ").append(this.jsonRequest).append("\n");
            return str.toString();
        }
    }

    private static class CliWebSocketListener implements WebSocketListener {
        private final Config config;
        private final OkHttpClient httpClient;
        private final CountDownLatch latch = new CountDownLatch(1);
        private WebSocket webSocket;

        public CliWebSocketListener(OkHttpClient httpClient, WebSocketCall wsc, Config config) {
            this.httpClient = httpClient;
            this.config = config;
            wsc.enqueue(this);
        }

        public void waitForResponse() throws InterruptedException {
            latch.await();
        }

        @Override
        public void onOpen(WebSocket ws, Response response) {
            this.webSocket = ws;
            log.debugf("Web socket opened to [%s]", config.serverUrl);
            try {
                sendCommandNow();
            } catch (Exception e) {
                log.errorf(e, "Failed to send command");
                shutdown(e);
            }
        }

        @Override
        public void onClose(int code, String reason) {
            log.debugf("Web socket closed. code=[%d], reason=[%s]", code, reason);
            shutdown(null);
        }

        @Override
        public void onFailure(IOException error, Response response) {
            log.errorf(error, "Command failed: %s", response);
            shutdown(error);
        }

        @Override
        public void onMessage(ResponseBody responseBody) throws IOException {

            boolean finished = true; // stop after this message unless we know we should expect more
            try {
                BasicMessageWithExtraData<? extends BasicMessage> msgWithData;

                if (responseBody.contentType().equals(WebSocket.TEXT)) {
                    String nameAndJsonStr = responseBody.string();
                    msgWithData = new ApiDeserializer().deserialize(nameAndJsonStr);
                } else if (responseBody.contentType().equals(WebSocket.BINARY)) {
                    InputStream input = responseBody.byteStream();
                    msgWithData = new ApiDeserializer().deserialize(input);
                } else {
                    throw new IllegalArgumentException(
                            "Unknown mediatype type, please report this bug: " + responseBody.contentType());
                }

                BasicMessage msg = msgWithData.getBasicMessage();
                BinaryData binary = msgWithData.getBinaryData();

                String messageName = msg.getClass().getSimpleName();
                log.debugf("JSON response: %s=%s", messageName, msg.toJSON());

                long now = System.currentTimeMillis();
                File jsonMessageFile = new File(config.outputDir, messageName + now + ".json");
                Files.write(jsonMessageFile.toPath(), msg.toJSON().getBytes(), StandardOpenOption.CREATE_NEW);
                log.infof("JSON response stored in file: %s", jsonMessageFile);
                if (binary != null) {
                    File binaryFile = new File(config.outputDir, messageName + now + ".binary");
                    Files.copy(binary, binaryFile.toPath());
                    log.infof("Binary data was in response and has been stored in file: %s", binaryFile);
                }

                // See if the response was the expected one. If so, we can stop.
                // If we weren't given an exepcted response, then we assume the response is the
                // same name of the command except with the word "Response" in the name as opposed to "Request".
                // Also, remember that JSON commands can be specified with just the simple class name of the
                // fully qualified class name. We should check for both combinations. So if we send
                // "EchoRequest" command, we can expect "EchoResponse" back. If we send
                // "org.abc.MyRequest" we can expect "org.abc.MyResponse"
                // NOTE: if we get the GenericErrorResponse, we immediately abort.
                if (msg.getClass().getName().contains("GenericErrorResponse")) {
                    finished = true;
                } else {
                    String expectedResponse = config.expectedResponse;
                    if (expectedResponse == null || expectedResponse.isEmpty()) {
                        expectedResponse = config.command.replace("Request", "Response");
                    }
                    finished = (Arrays.asList(msg.getClass().getSimpleName(), msg.getClass().getName())
                            .contains(expectedResponse));
                }
            } finally {
                responseBody.close();
                if (finished) {
                    shutdown(null);
                }
            }
        }

        @Override
        public void onPong(Buffer buf) {
        }

        private void sendCommandNow() throws Exception {
            if (config.binaryDataFile == null) {
                Buffer buffer = new Buffer();
                buffer.writeUtf8(config.jsonRequest);
                RequestBody requestBody = RequestBody.create(WebSocket.TEXT, buffer.readByteArray());
                webSocket.sendMessage(requestBody);
            } else {
                RequestBody requestBody = new RequestBody() {
                    @Override
                    public MediaType contentType() {
                        return WebSocket.BINARY;
                    }

                    @Override
                    public void writeTo(BufferedSink bufferedSink) throws IOException {
                        // write the JSON request itself
                        bufferedSink.write(config.jsonRequest.getBytes());

                        // now stream the binary data
                        int bufferSize = 32768;
                        try (InputStream input = new BufferedInputStream(new FileInputStream(config.binaryDataFile),
                                bufferSize)) {
                            byte[] buffer = new byte[bufferSize];
                            for (int bytesRead = input.read(buffer); bytesRead != -1; bytesRead = input
                                    .read(buffer)) {
                                bufferedSink.write(buffer, 0, bytesRead);
                                bufferedSink.flush();
                            }
                        } catch (IOException ioe) {
                            throw new RuntimeException("Failed to emit to sink", ioe);
                        }
                    }
                };

                webSocket.sendMessage(requestBody);
            }
        }

        private void shutdown(Exception e) {
            if (latch.getCount() == 0) {
                return;
            }

            try {
                if (webSocket != null) {
                    try {
                        if (e != null) {
                            webSocket.close(1011, e.getMessage());
                        } else {
                            webSocket.close(1000, CommandCli.class.getSimpleName() + " Done");
                        }
                    } catch (Exception closeException) {
                        // ignore this if it is just telling us the websocket is already closed
                        if (!(closeException instanceof IllegalStateException)
                                && !(closeException.getMessage().contains("Socket closed"))) {
                            throw closeException;
                        }
                    }
                }

                httpClient.getDispatcher().getExecutorService().shutdown();

            } catch (Exception e2) {
                log.errorf(e2, "Cannot fully close websocket");
            } finally {
                latch.countDown();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        ProcessedCommand<?> options = buildCommandLineOptions();

        try {
            Config config = parseCommandLine(options, args);
            CliWebSocketListener listener = sendCommand(config);
            listener.waitForResponse();
        } catch (CommandLineParserException pe) {
            log.errorf(pe, "Failed to parse command line.");
            printHelp(options);
        } catch (Exception ex) {
            log.errorf(ex, "Unexpected error");
        }
    }

    private static CliWebSocketListener sendCommand(Config config) throws Exception {

        OkHttpClient httpClient = new OkHttpClient();
        httpClient.setConnectTimeout(10, TimeUnit.SECONDS);
        httpClient.setReadTimeout(5, TimeUnit.MINUTES);

        Request request = new Request.Builder().url(config.serverUrl)
                .addHeader("Authorization", Credentials.basic(config.username, config.password))
                .addHeader("Accept", "application/json").build();

        WebSocketCall wsc = WebSocketCall.create(httpClient, request);
        CliWebSocketListener listener = new CliWebSocketListener(httpClient, wsc, config);
        return listener;
    }

    private static Config parseCommandLine(ProcessedCommand<?> options, String[] args) throws Exception {
        StringBuilder argLine = new StringBuilder(COMMAND_NAME);
        for (String str : args) {
            argLine.append(' ').append('\'').append(str).append('\'');
        }
        log.debugf("Command line: %s", argLine);

        CommandLineParser<?> parser = new CommandLineParserBuilder().processedCommand(options).create();

        CommandLine<?> commandLine = parser.parse(argLine.toString());
        if (commandLine.getParserException() != null) {
            throw commandLine.getParserException();
        }

        File outputDir = new File(commandLine.getOptionValue(OPT_OUTPUT_DIR, "."));
        String urlStr = commandLine.getOptionValue(OPT_SERVER_WEB_SOCKET_URL,
                "ws://127.0.0.1:8080/hawkular/command-gateway/ui/ws");
        String username = commandLine.getOptionValue(OPT_USERNAME);
        String password = commandLine.getOptionValue(OPT_PASSWORD);
        String command = commandLine.getOptionValue(OPT_COMMAND);
        String expectedResponse = commandLine.getOptionValue(OPT_EXPECTED_RESPONSE);
        String jsonRequestFileStr = commandLine.getOptionValue(OPT_REQUEST_FILE);
        String binaryDataFileStr = commandLine.getOptionValue(OPT_BINARY_DATA_FILE);
        Map<String, String> jsonProperties = commandLine.getOptionProperties("P");

        if (!Pattern.matches(".*[^/]/[^/].*", urlStr)) {
            log.debugf("URL [%s] did not specify a path - using '/hawkular/command-gateway/ui/ws'", urlStr);
            urlStr = urlStr + (urlStr.endsWith("/") ? "" : "/") + "hawkular/command-gateway/ui/ws";
        }

        if (password == null || password.isEmpty()) {
            password = readSecretFromStdin("Password:");
            if (password == null || password.isEmpty()) {
                throw new Exception("Password was not provided");
            }
        }

        if (!outputDir.isDirectory()) {
            outputDir.mkdirs();
            if (!outputDir.isDirectory()) {
                throw new Exception("Cannot create response directory: " + outputDir);
            }
        }

        if (binaryDataFileStr != null) {
            File file = new File(binaryDataFileStr);
            if (!file.isFile()) {
                throw new Exception(
                        "Binary data file does not exist or is not a regular file: " + binaryDataFileStr);
            }
            if (!(file.canRead())) {
                throw new Exception("Cannot read binary data file: " + binaryDataFileStr);
            }
        }

        StringBuilder jsonRequest = new StringBuilder().append(command).append("=");

        if (jsonRequestFileStr != null) {
            // the JSON request was given to us via file
            String requestString = new String(Files.readAllBytes(new File(jsonRequestFileStr).toPath()));

            // if the file had a newline or other characters padding its end, remove them
            int endOfJson = requestString.lastIndexOf('}');
            if (endOfJson == -1) {
                log.warnf("The request file [%s] does not appear to be valid JSON", jsonRequestFileStr);
                jsonRequest.append(requestString); // just send the whole thing and expect an error later
            } else {
                if (endOfJson == (requestString.length() - 1)) {
                    jsonRequest.append(requestString);
                } else {
                    int ignoring = requestString.length() - endOfJson - 1;
                    log.debugf("Ignoring [%d] non-JSON characters found at the end of the request file [%s]",
                            ignoring, jsonRequestFileStr);
                    jsonRequest.append(requestString.substring(0, endOfJson + 1));
                }
            }
        } else {
            // build the JSON using the properties passed on the command line
            jsonRequest.append("{");
            Iterator<Entry<String, String>> iter = jsonProperties.entrySet().iterator();
            while (iter.hasNext()) {
                Entry<String, String> jsonProperty = iter.next();
                jsonRequest.append('"').append(jsonProperty.getKey()).append('"');
                jsonRequest.append(':');
                jsonRequest.append('"').append(jsonProperty.getValue()).append('"');
                if (iter.hasNext()) {
                    jsonRequest.append(",");
                }
            }
            jsonRequest.append("}");
        }

        Config config = new Config();
        config.outputDir = outputDir;
        config.serverUrl = urlStr;
        config.username = username;
        config.password = password;
        config.command = command;
        config.expectedResponse = expectedResponse;
        config.jsonRequest = jsonRequest.toString();
        config.binaryDataFile = (binaryDataFileStr == null) ? null : new File(binaryDataFileStr);

        log.debug(config);

        return config;
    }

    private static void printHelp(ProcessedCommand<?> options) {
        if (options == null) {
            throw new RuntimeException("Cannot print help - options is null");
        }
        System.out.println(options.printHelp());
    }

    /**
     * Reads a secret from the console (stdin).
     *
     * @param message to present before reading
     * @return secret value or null if console is not available
     */
    private static String readSecretFromStdin(String message) {
        Console console = System.console();
        if (console == null) {
            return null;
        }
        console.writer().write(message);
        console.writer().flush();
        return String.valueOf(console.readPassword());
    }

    private static ProcessedCommand<?> buildCommandLineOptions() throws Exception {
        ProcessedCommandBuilder cmd = new ProcessedCommandBuilder();

        cmd.name(COMMAND_NAME);
        cmd.description("Sends commands to Hawkular Server");

        cmd.addOption(new ProcessedOptionBuilder().name(OPT_OUTPUT_DIR).optionType(OptionType.NORMAL)
                .type(String.class)
                .description("Directory to store the response. Default is the current directory.").create());
        cmd.addOption(new ProcessedOptionBuilder().name(OPT_SERVER_WEB_SOCKET_URL).optionType(OptionType.NORMAL)
                .type(String.class).description("The URL to connect to the Hawkular Server web socket. "
                        + "Default is ws://127.0.0.1:8080/hawkular/command-gateway/ui/ws")
                .create());
        cmd.addOption(new ProcessedOptionBuilder().name(OPT_USERNAME).optionType(OptionType.NORMAL)
                .type(String.class).description("User the CLI will use when connecting to Hawkular Server.")
                .required(true).create());
        cmd.addOption(new ProcessedOptionBuilder().name(OPT_PASSWORD).optionType(OptionType.NORMAL)
                .type(String.class).description("Credentials the CLI will use when connecting to Hawkular Server.")
                .required(false) // if not specified on command line, it will be requested on stdin later
                .create());
        cmd.addOption(
                new ProcessedOptionBuilder().name(OPT_COMMAND).optionType(OptionType.NORMAL).type(String.class)
                        .description("The name of the JSON command request to send.").required(true).create());
        cmd.addOption(new ProcessedOptionBuilder().name(OPT_EXPECTED_RESPONSE).optionType(OptionType.NORMAL)
                .type(String.class)
                .description("The expected name of the JSON response. "
                        + "If not specified, a guess will be made based on the name of the command request.")
                .create());
        cmd.addOption(new ProcessedOptionBuilder().name(OPT_BINARY_DATA_FILE).optionType(OptionType.NORMAL)
                .type(String.class)
                .description("File containing binary data to send along with the commmand (optional).").create());
        cmd.addOption(new ProcessedOptionBuilder().name(OPT_REQUEST_FILE).optionType(OptionType.NORMAL)
                .type(String.class).description("File containing the JSON request (optional).").create());
        cmd.addOption(new ProcessedOptionBuilder().name(String.valueOf(OPT_PROPERTY)).shortName(OPT_PROPERTY)
                .optionType(OptionType.GROUP).type(String.class).valueSeparator('=')
                .description("<name>=<value> properties that make up the JSON request. " + "Ignored if --"
                        + OPT_REQUEST_FILE + " is specified.")
                .create());

        return cmd.create();
    }

}