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