/*******************************************************************************
* Copyright (c) 2015 IBM Corp.
*
* 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.ibm.ws.lars.upload.cli;
import java.io.BufferedReader;
import java.io.Console;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.ibm.ws.lars.upload.cli.ClientException.HelpDisplay;
import com.ibm.ws.massive.esa.MassiveEsa;
import com.ibm.ws.repository.common.enums.State;
import com.ibm.ws.repository.connections.RepositoryConnection;
import com.ibm.ws.repository.connections.RestRepositoryConnection;
import com.ibm.ws.repository.exceptions.RepositoryBackendException;
import com.ibm.ws.repository.exceptions.RepositoryBackendRequestFailureException;
import com.ibm.ws.repository.exceptions.RepositoryBadDataException;
import com.ibm.ws.repository.exceptions.RepositoryException;
import com.ibm.ws.repository.exceptions.RepositoryResourceDeletionException;
import com.ibm.ws.repository.resources.EsaResource;
import com.ibm.ws.repository.resources.RepositoryResource;
import com.ibm.ws.repository.resources.writeable.RepositoryResourceWritable;
import com.ibm.ws.repository.strategies.writeable.AddThenDeleteStrategy;
public class Main {
static final String CONNECTION_PROBLEM = "There was a problem connecting to the repository: ";
static final String MISSING_URL = "The repository url must be provided, either as an argument or in a configuration file.";
static final String INVALID_URL = "The supplied url is not valid: ";
static final String NO_IDS_FOR_DELETE = "No asset IDs were supplied.";
static final String ASSET_NOT_FOUND = "Asset not found in repository.";
static final String SERVER_ERROR = "The repository server returned an error.";
static final String NO_FILES = "No files to upload. The files to upload must be provided as arguments.";
private static Pattern versionPattern = Pattern.compile("productVersion=\"?([0-9\\.+]+)");
private Map<Option, String> options;
private Action action;
private final InputStream input;
private final PrintStream output;
/** Filter that only accepts .esa files */
private static final FileFilter ESA_FILTER = new FileFilter() {
@Override
public boolean accept(File file) {
if (file == null)
return false;
String name = file.getName();
return !file.isDirectory() && name != null && name.endsWith(".esa");
}
};
/**
* All logic here should be delegated to run, to allow for easier testing
*/
public static void main(String[] args) {
Main main = new Main(System.in, System.out);
try {
main.run(args);
} catch (ClientException e) {
System.exit(e.getReturnCode());
}
System.exit(0);
}
public Main(InputStream in, PrintStream out) {
this.input = in;
this.output = out;
}
/**
* Effectively a delegate of main, to allow for testing.
*
* @param args
* @throws ClientException
*/
public void run(String[] args) throws ClientException {
try {
List<String> remainingArgs = readActionAndOptions(args);
switch (action) {
case UPLOAD:
doUpload(remainingArgs);
break;
case DELETE:
doDelete(remainingArgs);
break;
case FIND:
doFind(remainingArgs);
break;
case FIND_AND_DELETE:
// doDelete has conditional code checking for the FIND_AND_DELETE action
doDelete(remainingArgs);
break;
case LISTALL:
doListAll(remainingArgs);
break;
case HELP:
showHelp(remainingArgs);
break;
default:
showHelp(remainingArgs);
break;
}
} catch (ClientException e) {
if (e.getHelpDisplay() == HelpDisplay.SHOW_HELP) {
new Help(output).printMessageAndGlobalUsage(e.getMessage());
} else {
output.println(e.getMessage());
}
throw e;
}
}
/**
* Returns the name which was used to invoke this utility.
* <p>
* This is found by reading the INVOKED environment variable which should be set by any script
* which launches this utility.
*
* @return the name by which this utility was invoked, or null if the jar was invoked directly
*/
public static String getInvokedName() {
return System.getenv("INVOKED");
}
/**
* Reads the action and options from the command line, storing them in <code>action</code> and
* <code>options</code>.
* <p>
* Reports errors and exits the program if there is a problem parsing the arguments.
* <p>
* The argument '--' is interpreted as an indication that all following arguments should not
* parsed as options.
*
* @param args the array of args from main
* @return the list of arguments which were not parsed as options
*/
private List<String> readActionAndOptions(String[] args) throws ClientException {
List<String> nonOptionArgs = new ArrayList<String>();
if (args.length == 0) {
throw new ClientException("No options were given", 1, HelpDisplay.SHOW_HELP);
}
String actionString = args[0];
// If we're not invoked from a script, the action should start with "--"
if (getInvokedName() == null) {
if (!actionString.startsWith("--")) {
throw new ClientException(actionString + " is not a valid action", 1, HelpDisplay.SHOW_HELP);
}
actionString = actionString.substring(2);
}
Action action = Action.getByArgument(actionString);
if (action == null) {
throw new ClientException(actionString + " is not a valid action", 1, HelpDisplay.SHOW_HELP);
}
this.action = action;
this.options = new HashMap<Option, String>();
boolean keepProcessingOptions = true;
for (int i = 1; i < args.length; i++) {
String arg = args[i];
if (keepProcessingOptions && arg.equals("--")) {
keepProcessingOptions = false;
} else if (keepProcessingOptions && arg.startsWith("--")) {
String[] argParts = arg.substring(2).split("=", 2);
if (argParts.length == 0) {
throw new ClientException(arg + " is not a valid option", 1, HelpDisplay.SHOW_HELP);
}
String optionName = argParts[0];
String value = argParts.length == 2 ? argParts[1] : null;
Option option = Option.getByArgument(optionName);
if (option == null) {
throw new ClientException(arg + " is not a valid option", 1, HelpDisplay.SHOW_HELP);
}
options.put(option, value);
} else {
nonOptionArgs.add(arg);
}
}
return nonOptionArgs;
}
/**
* Prints a help message to the output stream, either a global message (if remainingArgs is
* empty) or a help message for a particular command (if specified in remainingArgs).
*/
private void showHelp(List<String> remainingArgs) {
Help help = new Help(output);
if (remainingArgs.size() == 0 ||
remainingArgs.size() > 1) {
help.printGlobalUsage();
} else {
Action action = Action.getByArgument(remainingArgs.get(0));
if (action != null) {
switch (action) {
case HELP:
case UPLOAD:
case DELETE:
case LISTALL:
case FIND:
case FIND_AND_DELETE:
help.printCommandUsage(action.getUsage(), action.getHelpDetail());
help.printGlobalOptions();
break;
default:
help.printGlobalUsage();
break;
}
} else {
help.printGlobalUsage();
}
}
}
/**
* Uploads a list of ESAs.
* <p>
* This method will print error messages and exit the program if there is an error.
*
* @param remainingArgs a list of file paths to ESAs which should be uploaded.
*/
private void doUpload(List<String> remainingArgs) throws ClientException {
RepositoryConnection repoConnection = createRepoConnection();
List<File> files = new ArrayList<File>();
for (String arg : remainingArgs) {
File argFile = new File(arg);
// If we encounter a directory then add its contents
if (argFile.isDirectory()) {
File[] directoryContents = argFile.listFiles(ESA_FILTER);
if (directoryContents != null) {
files.addAll(Arrays.asList(directoryContents));
}
} else {
files.add(argFile);
}
}
if (files.isEmpty()) {
throw new ClientException(NO_FILES, 1, HelpDisplay.SHOW_HELP);
}
// Specifying the wrong file is a likely mistake, check them all before we upload anything
for (File file : files) {
if (!file.canRead()) {
throw new ClientException("File " + file.toString() + " can't be read", 1, HelpDisplay.NO_HELP);
}
if (file.isDirectory()) {
// We don't expect we'd ever hit this case but, if we do (due to a logic error eg in ESA_FILTER)
// then bomb out
throw new ClientException("File " + file.toString() + " is a directory", 1, HelpDisplay.NO_HELP);
}
}
MassiveEsa uploader;
try {
uploader = new MassiveEsa(repoConnection);
} catch (RepositoryException ex) {
throw new ClientException("An error occurred while connecting to the repository: " + ex.getMessage(), 1, HelpDisplay.NO_HELP, ex);
}
int size = files.size();
for (int i = 0; i < size; i++) {
File file = files.get(i);
try {
output.print((i + 1) + " of " + size + ": Uploading " + file.toString() + " ... ");
List<RepositoryResource> deletedResources = new ArrayList<>();
AddThenDeleteStrategy uploadStrategy = new AddThenDeleteStrategy(State.PUBLISHED, State.PUBLISHED, true, null, deletedResources);
uploader.addEsasToMassive(Collections.singleton(file), uploadStrategy);
// Did this upload operation cause us to delete one or more existing assets?
if (deletedResources.size() > 1) {
// This is an unusual case: we replaced more than one existing (duplicate) assets
output.println("done, replacing multiple duplicate assets:");
for (RepositoryResource deletedResource : deletedResources) {
output.println(resourceToString(deletedResource));
}
} else if (deletedResources.size() == 1) {
// More common case: we replaced one asset. Effectively, we are updating that asset.
output.println("done, replacing existing asset " + resourceToString(deletedResources.get(0)));
} else {
// Most common case... we didn't replace anything and just
// uploaded this new asset
output.println("done");
}
} catch (RepositoryException ex) {
if (!file.getPath().endsWith(".esa")) {
throw new ClientException("\nAn error occurred while uploading " + file.toString() + ": "
+ "file does not appear to be an esa file.", 1, HelpDisplay.NO_HELP, ex);
} else
throw new ClientException("\nAn error occurred while uploading " + file.toString() + ": " + ex.getMessage(), 1, HelpDisplay.NO_HELP, ex);
}
}
}
private void doListAll(List<String> params) throws ClientException {
RepositoryConnection repoConnection = createRepoConnection();
Collection<? extends RepositoryResource> assets = null;
try {
assets = repoConnection.getAllResources();
} catch (RepositoryBackendException e) {
throw new ClientException("An error was recieved from the repository: " + e.getMessage(), 1, HelpDisplay.NO_HELP, e);
}
output.println("Listing all assets in the repository:");
printAssets(assets);
}
/**
* @param assets
*/
private void printAssets(Collection<? extends RepositoryResource> assets) {
if (assets.size() == 0) {
output.println("No assets found in repository");
return;
}
printTabbed("Asset ID", "Asset Type", "Liberty Version", "Asset Name");
for (RepositoryResource resource : assets) {
String type = resource.getType().getValue();
if (type.startsWith("com.ibm.websphere")) {
type = type.substring(18);
}
String name = resource.getName();
String shortName = null;
if (resource instanceof EsaResource) {
shortName = ((EsaResource) resource).getShortName();
}
if (shortName != null) {
name = name + " (" + shortName + ")";
}
String appliesTo = "";
if (resource instanceof EsaResource) {
EsaResource esa = (EsaResource) resource;
String fullAppliesTo = esa.getAppliesTo();
if (fullAppliesTo != null) {
Matcher versionMatcher = versionPattern.matcher(fullAppliesTo);
if (versionMatcher.find()) {
appliesTo = versionMatcher.group(1);
}
}
}
printTabbed(resource.getId(), type, appliesTo, name);
}
}
void printTabbed(String id, String type, String appliesTo, String name) {
output.format("%-30.30s | %-15.15s | %-15.15s | %s%n", id, type, appliesTo, name);
}
private List<String> doFind(List<String> remainingArgs) throws ClientException {
RepositoryConnection repoConnection = createRepoConnection();
Collection<? extends RepositoryResource> assets = null;
try {
if (remainingArgs.size() > 0) {
String searchString = remainingArgs.get(0);
assets = repoConnection.findResources(searchString, null, null, null);
} else {
assets = repoConnection.getAllResources();
}
} catch (RepositoryBackendException e) {
throw new ClientException("An error was recieved from the repository: " + e.getMessage(), 1, HelpDisplay.NO_HELP, e);
}
if (options.containsKey(Option.NAME)) {
String name = options.get(Option.NAME);
Iterator<? extends RepositoryResource> i = assets.iterator();
while (i.hasNext()) {
String assetName = i.next().getName();
if (assetName == null || !!!assetName.contains(name)) {
i.remove();
}
}
}
if (action == Action.FIND_AND_DELETE) {
List<String> assetIds = new ArrayList<String>(assets.size());
for (RepositoryResource resource : assets) {
assetIds.add(resource.getId());
}
return assetIds;
}
printAssets(assets);
return null;
}
private void doDelete(List<String> remainingArgs) throws ClientException {
RepositoryConnection repoConnection = createRepoConnection();
if (remainingArgs.size() == 0 && !!!options.containsKey(Option.FIND_DELETE)) {
throw new ClientException(NO_IDS_FOR_DELETE, 1, HelpDisplay.SHOW_HELP);
}
if (action == Action.FIND_AND_DELETE) {
remainingArgs = doFind(remainingArgs);
}
BufferedReader inputReader = new BufferedReader(new InputStreamReader(input));
for (String id : remainingArgs) {
RepositoryResourceWritable toDelete = null;
try {
toDelete = (RepositoryResourceWritable) repoConnection.getResource(id);
} catch (RepositoryBadDataException e) {
// This shouldn't happen unless there is client lib bug
throw new ClientException("Asset " + id + " not deleted. " + e.getMessage(), 1, HelpDisplay.NO_HELP, e);
} catch (RepositoryBackendRequestFailureException e) {
// Server said no
RepositoryBackendRequestFailureException requestFailed = e;
int response = requestFailed.getResponseCode();
if (response == 404) {
// Not found, go on to the next one
output.println("Asset " + id + " not deleted. " + ASSET_NOT_FOUND);
continue;
}
// Anything else should be a server error
throw new ClientException("Asset " + id + " not deleted. " + SERVER_ERROR + e.getMessage(), 1, HelpDisplay.NO_HELP, e);
} catch (RepositoryBackendException e) {
// Anything else is probably some kind of connection problem, so ditch out
throw new ClientException("Asset " + id + " not deleted. " + CONNECTION_PROBLEM + e.getMessage(), 1, HelpDisplay.NO_HELP, e);
}
if ((action == Action.FIND_AND_DELETE) && !!!options.containsKey(Option.NO_PROMPTS)) {
output.println("Delete asset " + toDelete.getId() + " " + toDelete.getName() + " (y/N)?");
try {
if (!!!("y".equalsIgnoreCase(inputReader.readLine()))) {
continue;
}
} catch (IOException e) {
throw new ClientException(e.getMessage(), 1, HelpDisplay.NO_HELP, e);
}
}
try {
toDelete.delete();
} catch (RepositoryResourceDeletionException | RepositoryBackendException e) {
// This can be an IO issue, or a request fail. Request fail is either a server
// problem, or a non-existent asset. We've just checked that the asset exists, so
// non-existent asset seems unlikely, most likely a server problem. As the source exception
// isn't public, we can't check. Either way, this is probably terminal
String message = "Asset " + id + " not deleted. There was a problem with the repository";
Throwable cause = e.getCause();
if (cause != null) {
message += cause.getMessage();
} else {
message += e.getMessage();
}
throw new ClientException(message, 1, HelpDisplay.NO_HELP, e);
}
output.println("Deleted asset " + id);
}
}
/**
* Creates a RepositoryConnection object from the options on the command line
*
* @return
*/
private RepositoryConnection createRepoConnection() throws ClientException {
String urlString = null;
String username = null;
String password = null;
boolean promptForPassword = false;
if (options.containsKey(Option.CONFIG_FILE)) {
File configFile = new File(options.get(Option.CONFIG_FILE));
try (InputStream in = new FileInputStream(configFile)) {
Properties props = new Properties();
props.load(in);
urlString = props.getProperty(Option.URL.getArgument());
username = props.getProperty(Option.USERNAME.getArgument());
password = props.getProperty(Option.PASSWORD.getArgument());
if (password.trim().length() == 0) {
password = null;
promptForPassword = true;
}
} catch (IOException e) {
output.println("Error reading config file: " + e.getMessage());
}
}
if (options.containsKey(Option.URL)) {
urlString = options.get(Option.URL);
}
if (options.containsKey(Option.USERNAME)) {
username = options.get(Option.USERNAME);
}
if (options.containsKey(Option.PASSWORD)) {
password = options.get(Option.PASSWORD);
if (password == null) {
promptForPassword = true;
} else {
promptForPassword = false;
}
}
if (username == null && (password != null || promptForPassword)) {
throw new ClientException("A username must be provided if a password is provided or will be prompted for", 1, HelpDisplay.SHOW_HELP);
}
if (promptForPassword) {
char[] passwordChars = getConsole().readPassword("Password:");
if (passwordChars == null) {
throw new ClientException("No password provided", 1, HelpDisplay.NO_HELP);
}
password = new String(passwordChars);
}
if (urlString == null) {
throw new ClientException(MISSING_URL, 1, HelpDisplay.SHOW_HELP);
}
// Check that the URL is at least valid, otherwise it doesn't seem to get
// checked until a request is made to the repository. Checking it here allows
// a better/more helpful error message
try {
new URL(urlString);
} catch (MalformedURLException e) {
throw new ClientException(INVALID_URL + urlString, 1, HelpDisplay.NO_HELP, e);
}
RestRepositoryConnection connection = new RestRepositoryConnection(username, password, "0", urlString);
return connection;
}
/**
* Get the console, or throw a suitable exception if it cannot be opened.
*
* @throws ClientException if the console cannot be opened
*/
private Console getConsole() throws ClientException {
Console console = System.console();
if (console == null) {
throw new ClientException("Failed to open an interactive prompt", 1, HelpDisplay.NO_HELP);
}
return console;
}
/**
* Converts this resource into a string that can be displayed to the user and which should be
* useful when presented in the context of "this resource replaced that one when uploaded."
*/
private String resourceToString(RepositoryResource resource) {
// At present, we only support features. This code will need to be
// revisited in order to upload other types of resources
if (!(resource instanceof EsaResource)) {
throw new IllegalArgumentException("This method only supports resources of type ESAResource. Type was: " + resource.getClass().getName() + " was supplied.");
}
EsaResource esaResource = (EsaResource) resource;
return String.format("%s type=%s, appliesTo=%s, version=%s, provideFeature=%s",
esaResource.getName(),
esaResource.getType(),
esaResource.getAppliesTo(),
esaResource.getVersion(),
esaResource.getProvideFeature());
}
}