co.cask.cdap.gateway.tools.ClientToolBase.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.gateway.tools.ClientToolBase.java

Source

/*
 * Copyright  2014 Cask Data, Inc.
 *
 * 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 co.cask.cdap.gateway.tools;

import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.utils.UsageException;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.log4j.Level;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Base class for CDAP Tools.
 */
public abstract class ClientToolBase {

    /**
     * for debugging. should only be set to true in unit tests.
     * when true, program will print the stack trace after the usage.
     */
    public static boolean debug = false;

    static {
        // this turns off all logging but we don't need that for a cmdline tool
        org.apache.log4j.Logger.getRootLogger().setLevel(Level.OFF);
    }

    private String toolName;
    protected static final Gson GSON = new Gson();

    private static final String HOST_OPTION = "host";
    private static final String PORT_OPTION = "port";
    private static final String HELP_OPTION = "help";
    private static final String VERBOSE_OPTION = "verbose";
    private static final String API_KEY_OPTION = "apikey";
    private static final String TOKEN_FILE_OPTION = "token-file";
    private static final String TOKEN_OPTION = "token";

    protected Options options;

    protected boolean help = false;
    protected boolean verbose = false;
    protected boolean forceNoSSL = false;
    protected String apikey = null; // the api key for authentication
    protected String tokenFile = null; // the file containing the access token only
    protected String accessToken = null; // the access token for secure connections
    protected String hostname = null;
    protected int port = -1;

    /**
     * A base class for implementing CDAP tools.
     *
     * @param toolName The name of the tool.
     */
    public ClientToolBase(String toolName) {
        this.toolName = toolName;
    }

    /**
     * Returns the name of the tool.
     *
     * @return tool name
     */
    public String getToolName() {
        return this.toolName;
    }

    /**
     * Forces no SSL.
     *
     * @return Returns this instance.
     */
    public ClientToolBase disallowSSL() {
        this.forceNoSSL = true;
        return this;
    }

    /**
     * Adds all basic options for a tool as well as additional options as specified by the addOptions method.
     */
    private void buildOptions() {
        options = new Options();
        options.addOption(null, HOST_OPTION, true, "To specify the CDAP host");
        options.addOption(null, PORT_OPTION, true,
                "To specify the port to use. The default value is --port " + Constants.Gateway.DEFAULT_PORT);
        options.addOption(null, HELP_OPTION, false, "To print this message");
        options.addOption(null, VERBOSE_OPTION, false, "Prints verbose output");
        options.addOption(null, API_KEY_OPTION, true, "To specify an API key for authentication");
        options.addOption(null, TOKEN_OPTION, true, "To specify the access token for secure CDAP");
        options.addOption(null, TOKEN_FILE_OPTION, true, "To specify a path to the access token for secure CDAP");
        addOptions(options);
    }

    /**
     * Parses the basic arguments which are common to all tools.
     *
     * @param line The instance of CommandLine which has already parsed the arguments.
     */
    private void parseBasicArgs(CommandLine line) {
        if (line.hasOption(HELP_OPTION)) {
            printUsage(false);
            help = true;
            return;
        }
        verbose = line.hasOption(VERBOSE_OPTION);
        hostname = line.getOptionValue(HOST_OPTION, null);
        port = line.hasOption(PORT_OPTION) ? parseNumericArg(line, PORT_OPTION).intValue() : -1;
        apikey = line.hasOption(API_KEY_OPTION) ? line.getOptionValue(API_KEY_OPTION) : null;
        accessToken = line.hasOption(TOKEN_OPTION) ? line.getOptionValue(TOKEN_OPTION).replaceAll("(\r|\n)", "")
                : null;
        tokenFile = line.getOptionValue(TOKEN_FILE_OPTION, null);
        // read the access token if one is not provided but the access token file is provided.
        if (accessToken == null && tokenFile != null) {
            accessToken = readTokenFile();
        }
    }

    /**
     * Specifies any additional options that could appear in the tool.
     * Should be overridden by child classes to add new optional arguments.
     *
     * @param options The Options object to add to.
     */
    protected void addOptions(Options options) {
    }

    /**
     * Parses args based on the options that were added when buildOptions was called.
     *
     * @param args The array of arguments to parse.
     * @return Returns true if the parsing succeeded and the help option was not specified.
     */
    protected boolean parseArguments(String[] args) {
        // parse generic args first
        CommandLineParser parser = new BasicParser();
        // Check all the options of the command line
        try {
            CommandLine line = parser.parse(options, args);
            parseBasicArgs(line);
            if (help) {
                return false;
            }
            return parseAdditionalArguments(line);
        } catch (ParseException e) {
            printUsage(true);
        } catch (IndexOutOfBoundsException e) {
            printUsage(true);
        }
        return true;
    }

    /**
     * Used to validate all additional arguments added by child class. Should return
     * the error message that should be displayed along with the usage if there are
     * invalid arguments.
     *
     * @return The error message to display along with the usage. Null if the usage should not be printed.
     */
    protected String validateArguments() {
        return null;
    }

    /**
     * Should be implemented by child classes to run the main logic. In order to make it testable,
     * instead of exiting in case of error it returns null, whereas in case of
     * success it returns the retrieved value as shown on the console.
     *
     * @param config The configuration of the gateway
     * @return null in case of error. A String representing the retrieved value
     * in case of success
     */
    protected abstract String execute(CConfiguration config);

    /**
     * Should be overridden by child classes to parse additional arguments if needed. This will often
     * go hand in hand with overriding addOptions. For positional arguments, first parse all optional
     * arguments and then check the remaining arguments in the line object using getArgs.
     *
     * @param line The CommandLine object which contains all arguments that have been passed in.
     * @return A boolean indicating whether or not the parsing was successful/valid or not.
     */
    protected boolean parseAdditionalArguments(CommandLine line) {
        return true;
    }

    /**
     * The main execute method for all tools. Parses the arguments, checks that they are valid,
     * and then calls the child class's execute method. In order to make it testable,
     * instead of exiting in case of error it returns null, whereas in case of
     * success it returns the retrieved value as shown on the console.
     *
     * @param args The array of arguments to parse.
     * @param config The configuration of the gateway
     * @return null in case of error. A String representing the retrieved value
     * in case of success
     */
    public String execute(String[] args, CConfiguration config) {
        try {
            buildOptions();
            boolean parseResult = parseArguments(args);
            if (help) {
                return "";
            }
            if (!parseResult) {
                printUsage(true);
                return null;
            }

            String usageMessage = validateArguments();
            if (usageMessage != null) {
                usage(usageMessage);
            }
            return execute(config);
        } catch (UsageException e) {
            if (debug) { // this is mainly for debugging the unit test
                System.err.println("Exception for arguments: " + Arrays.toString(args) + ". Exception: " + e);
                e.printStackTrace(System.err);
            }
        }
        return null;
    }

    /**
     * Should be overridden if any information should be printed before the
     * available command line options like positional arguments.
     *
     * @param error Used to print to different streams, System.out vs System.err
     */
    protected void printUsageTop(boolean error) {
        PrintStream out = error ? System.err : System.out;
        out.println(toolName + " Usage: ");
    }

    /**
     * Sends http requests with apikey and access token headers
     * and checks the status of the request afterwards.
     *
     * @param requestBase The request to send. This method adds the apikey and access token headers
     *                    if they are valid.
     * @param expectedCodes The list of expected status codes from the request. If set to null,
     *                      this method checks that the status code is OK.
     * @return The HttpResponse if the request was successfully sent and the request status code
     * is one of expectedCodes or OK if expectedCodes is null. Otherwise, returns null.
     */
    protected HttpResponse sendHttpRequest(HttpClient client, HttpRequestBase requestBase,
            List<Integer> expectedCodes) {
        if (apikey != null) {
            requestBase.setHeader(Constants.Gateway.API_KEY, apikey);
        }
        if (accessToken != null) {
            requestBase.setHeader("Authorization", "Bearer " + accessToken);
        }
        try {
            HttpResponse response = client.execute(requestBase);
            // if expectedCodes is null, just check that we have OK status code
            if (expectedCodes == null) {
                if (!checkHttpStatus(response, HttpStatus.SC_OK)) {
                    return null;
                }
            } else {
                if (!checkHttpStatus(response, expectedCodes)) {
                    return null;
                }
            }
            return response;
        } catch (IOException e) {
            System.err.println("Error sending HTTP request: " + e.getMessage());
            return null;
        }
    }

    protected Long parseNumericArg(CommandLine line, String option) {
        if (line.hasOption(option)) {
            try {
                return Long.valueOf(line.getOptionValue(option));
            } catch (NumberFormatException e) {
                usage(option + " must have an integer argument");
            }
        }
        return null;
    }

    /**
     * Print an error message followed by the usage statement.
     *
     * @param errorMessage the error message
     */
    protected void usage(String errorMessage) {
        if (errorMessage != null) {
            System.err.println("Error: " + errorMessage);
        }
        printUsage(true);
    }

    /**
     * Prints the usage message to the PrintStream indicated by the error parameter
     *
     * @param error Indicates which stream to print to. If true, throws UsageException.
     */
    protected void printUsage(boolean error) {
        // print the positional args, if any, from child class
        printUsageTop(error);
        PrintWriter pw = error ? new PrintWriter(System.err) : new PrintWriter(System.out);
        pw.println("Options:\n");
        HelpFormatter formatter = new HelpFormatter();
        formatter.printOptions(pw, 100, options, 0, 10);
        pw.flush();
        pw.close();
        if (error) {
            throw new UsageException();
        }
    }

    /**
     * Reads the access token from the access token file. Returns null if the read fails
     * @return the access token from the access token file. Null if the read fails.
     */
    protected String readTokenFile() {
        if (tokenFile != null) {
            try {
                return Files.toString(new File(tokenFile), Charsets.UTF_8).replaceAll("(\r|\n)", "");
            } catch (FileNotFoundException e) {
                usage("Could not find access token file: " + tokenFile);
            } catch (IOException e) {
                usage("Could not read access token file: " + tokenFile);
            }
        }
        return null;
    }

    /**
     * Error Description from HTTPResponse
     */
    private class ErrorMessage {
        @SerializedName("error_description")
        private String errorDescription;

        public String getErrorDescription() {
            return errorDescription;
        }
    }

    /**
     * Prints the error response from the connection
     * @param errorStream the stream to read the response from
     */
    protected void readUnauthorizedError(InputStream errorStream) {
        PrintStream out = verbose ? System.out : System.err;
        out.println(HttpStatus.SC_UNAUTHORIZED + " Unauthorized");
        if (accessToken == null) {
            out.println("No access token provided");
            return;
        }
        try {
            Reader reader = new InputStreamReader(errorStream);
            String responseError = GSON.fromJson(reader, ErrorMessage.class).getErrorDescription();
            if (responseError != null && !responseError.isEmpty()) {
                out.println(responseError);
            }
        } catch (Exception e) {
            out.println("Unknown unauthorized error");
        }
    }

    /**
     * Check whether the Http return code is as expected. If not, print the error
     * message and return false. Otherwise, if verbose is on, print the response
     * status line.
     *
     * @param response the HTTP response
     * @param expected the expected HTTP status code
     * @return whether the response is as expected
     */
    protected boolean checkHttpStatus(HttpResponse response, int expected) {
        return checkHttpStatus(response, Collections.singletonList(expected));
    }

    /**
     * Check whether the Http return code is as expected. If not, print the
     * status message and return false. Otherwise, if verbose is on, print the
     * response status line.
     *
     * @param response the HTTP response
     * @param expected the list of expected HTTP status codes
     * @return whether the response is as expected
     */
    protected boolean checkHttpStatus(HttpResponse response, List<Integer> expected) {
        try {
            return checkHttpStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().toString(),
                    response.getEntity().getContent(), expected);
        } catch (Exception e) {
            // error stream cannot be received
            return checkHttpStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().toString(),
                    null, expected);
        }
    }

    /**
     * Checks that an http request was executed properly. Compares statusCode to the expected codes. If the status code
     * is not expected and is unauthorized, then an appropriate message is given.
     * @param statusCode the status code of the http request
     * @param statusLine the status line which is printed
     * @param errorStream the error stream for handling unauthorized requests
     * @param expected the expeced status codes
     * @return
     */
    protected boolean checkHttpStatus(int statusCode, String statusLine, InputStream errorStream,
            List<Integer> expected) {
        if (!expected.contains(statusCode)) {
            PrintStream out = verbose ? System.out : System.err;
            if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
                if (errorStream != null) {
                    readUnauthorizedError(errorStream);
                    return false;
                }
            }
            // other errors
            out.println(statusLine);
            return false;
        }
        if (verbose) {
            System.out.println(statusLine);
        }
        return true;
    }
}