Package co.cask.cdap.gateway.tools

Source Code of co.cask.cdap.gateway.tools.ClientToolBase$ErrorMessage

/*
* 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;
  }
}
TOP

Related Classes of co.cask.cdap.gateway.tools.ClientToolBase$ErrorMessage

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.