/*******************************************************************************
* Copyright (c) 2011 GigaSpaces Technologies Ltd. All rights reserved
*
* 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.cloudifysource.shell;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.felix.service.command.CommandSession;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.SystemDefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.cloudifysource.dsl.internal.CloudifyConstants;
import org.cloudifysource.restclient.RestClient;
import org.cloudifysource.restclient.StringUtils;
import org.cloudifysource.restclient.exceptions.RestClientException;
import org.cloudifysource.shell.exceptions.CLIException;
import org.cloudifysource.shell.installer.CLIEventsDisplayer;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.Ansi.Color;
import org.fusesource.jansi.Ansi.Erase;
import com.j_spaces.kernel.Environment;
import com.j_spaces.kernel.PlatformVersion;
/**
* @author rafi, barakm
* @since 2.0.0
* <p/>
* This class includes different utilities used across the CLI.
*/
public final class ShellUtils {
private static final Logger logger = Logger.getLogger(ShellUtils.class.getName());
private static final char WIN_RETURN_CHAR = '\r';
private static final char LINUX_RETURN_CHAR = '\n';
private static final long TWO_WEEKS_IN_MILLIS = 86400000L * 14L;
private static final File VERSION_CHECK_FILE =
new File(System.getProperty("user.home") + "/.karaf/lastVersionCheckTimestamp");
private static final int VERSION_CHECK_READ_TIMEOUT = 5000;
private static final String BOLD_ANSI_CHAR_SEQUENCE = "\u001B[1m";
private static final char FIRST_ESC_CHAR = 27;
private static final char SECOND_ESC_CHAR = '[';
private static final char COMMAND_CHAR = 'm';
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
private static volatile ResourceBundle defaultMessageBundle;
private ShellUtils() {
}
/**
* returns the message as it appears in the default message bundle.
*
* @param msgName
* the message key as it is defined in the message bundle.
* @param arguments
* the message arguments
* @return the formatted message according to the message key.
*/
public static String getFormattedMessage(final String msgName, final Object... arguments) {
return getFormattedMessage(getMessageBundle(), msgName, arguments);
}
/**
* returns the message as it appears in the given message bundle.
*
* @param messageBundle
* the message bundle that holds the message.
* @param msgName
* the message key as it is defined in the message bundle.
* @param arguments
* the message arguments
* @return the formatted message according to the message key.
*/
public static String getFormattedMessage(final ResourceBundle messageBundle, final String msgName,
final Object... arguments) {
final String message = messageBundle.getString(msgName);
if (message == null) {
logger.warning("Missing resource in messages resource bundle: " + msgName);
return msgName;
}
try {
return MessageFormat.format(message, arguments);
} catch (final IllegalArgumentException e) {
logger.fine("Failed to format message: " + msgName + " with format: "
+ message + " and arguments: " + Arrays.toString(arguments));
return msgName;
}
}
/**
*
* @param session
* the command session.
* @param messageKey
* the message key.
* @return true if user hits 'y' OR 'yes' else returns false
* @throws IOException
* indicates a failure while accessing the session's stdout.
*/
public static boolean promptUser(final CommandSession session, final String messageKey)
throws IOException {
return promptUser(session, messageKey, EMPTY_OBJECT_ARRAY);
}
/**
* prompts the user with the given question.
*
* @param session
* the command session.
* @param messageKey
* the message key.
* @param messageArgs
* the message arguments.
* @return true if user hits 'y' OR 'yes' else returns false
* @throws IOException
* Indicates a failure while accessing the session's stdout.
*/
public static boolean promptUser(final CommandSession session, final String messageKey,
final Object... messageArgs)
throws IOException {
if ((Boolean) session.get(Constants.INTERACTIVE_MODE)) {
session.getConsole().print(Ansi.ansi().eraseLine(Erase.ALL));
final String confirmationQuestion = ShellUtils.getFormattedMessage(messageKey, messageArgs);
session.getConsole().print(confirmationQuestion + " ");
session.getConsole().flush();
char responseChar = '\0';
final StringBuilder responseBuffer = new StringBuilder();
while (true) {
responseChar = (char) session.getKeyboard().read();
if (responseChar == '\u007F') { // backspace
if (responseBuffer.length() > 0) {
responseBuffer.deleteCharAt(responseBuffer.length() - 1);
session.getConsole().print(Ansi.ansi().cursorLeft(1).eraseLine());
}
} else if (responseChar == WIN_RETURN_CHAR || responseChar == LINUX_RETURN_CHAR) {
session.getConsole().println();
break;
} else {
session.getConsole().print(responseChar);
responseBuffer.append(responseChar);
}
session.getConsole().flush();
}
final String responseStr = responseBuffer.toString().trim();
return "y".equalsIgnoreCase(responseStr) || "yes".equalsIgnoreCase(responseStr);
}
// Shell is running in nonInteractive mode. we skip the question.
return true;
}
/**
* Gets the given message formatted to be displayed in the specified color.
*
* @param message
* The text message
* @param color
* The color the message should be displayed in
* @return A formatted message text
*/
public static String getColorMessage(final String message, final Color color) {
final String formattedMessage = Ansi.ansi().fg(color).a(message).toString();
return formattedMessage + FIRST_ESC_CHAR + SECOND_ESC_CHAR + '0' + COMMAND_CHAR;
}
/**
* Gets the given message formatted to be displayed in bold characters.
*
* @param message
* The text message
* @return A formatted message text
*/
public static String getBoldMessage(final String message) {
return BOLD_ANSI_CHAR_SEQUENCE + message + FIRST_ESC_CHAR
+ SECOND_ESC_CHAR + '0' + COMMAND_CHAR;
}
/**
* Converts a comma-delimited string of instance IDs to a set of Integers.
*
* @param componentInstanceIDs
* a comma-delimited string of instance IDs
* @return instance IDs as a set of Integers
*/
public static Set<Integer> delimitedStringToSet(final String componentInstanceIDs) {
final String[] delimited = componentInstanceIDs.split(",");
final Set<Integer> intSet = new HashSet<Integer>();
for (final String str : delimited) {
intSet.add(Integer.valueOf(str));
}
return intSet;
}
/**
* Gets the recipes map from the session.
*
* @param session
* The command session to query
* @return The recipes map
*/
@SuppressWarnings("unchecked")
public static Map<String, File> getRecipes(final CommandSession session) {
return (Map<String, File>) session.get(Constants.RECIPES);
}
/**
* Gets the built-in messages bundle, with the default locale.
*
* @return The messages bundle
*/
public static ResourceBundle getMessageBundle() {
if (defaultMessageBundle == null) {
defaultMessageBundle = ResourceBundle.getBundle(
"MessagesBundle", Locale.getDefault());
}
return defaultMessageBundle;
}
/**
* Calculates how many milliseconds ahead is the specified target time. If it has passed already, throws a
* {@link TimeoutException} with the given error message.
*
* @param errorMessage
* The error message of the {@link TimeoutException}, if thrown
* @param end
* The target time, formatted in milliseconds
* @return The number of milliseconds ahead, before the target time is reached
* @throws TimeoutException
* Indicating the target time is in the past
*/
public static long millisUntil(final String errorMessage, final long end)
throws TimeoutException {
final long millisUntilEnd = end - System.currentTimeMillis();
if (millisUntilEnd < 0) {
throw new TimeoutException(errorMessage);
}
return millisUntilEnd;
}
/**
* Gets an "expected execution time" formatted message, with the current time in this format: HH:mm.
*
* @return a formatted "expected execution time" message
*/
public static String getExpectedExecutionTimeMessage() {
final String currentTime = new SimpleDateFormat("HH:mm").format(new Date());
return MessageFormat.format(
getMessageBundle().getString(
"expected_execution_time"), currentTime);
}
/**
* Gets the CLI directory.
*
* @return the CLI directory
*/
public static File getCliDirectory() {
return new File(Environment.getHomeDirectory(), "/tools/cli");
}
/**
* Verifies the given value is not null. If it is - throws an IllegalArgumentException with the message: <name>
* cannot be null.
*
* @param name
* The name to be used in the exception, if thrown
* @param value
* The value to verify
*/
public static void checkNotNull(final String name, final Object value) {
if (value == null) {
throw new IllegalArgumentException(name + " cannot be null");
}
}
/**
* Reads the properties from the specified file, and loads them into a {@link Properties} object.
*
* @param propertiesFile
* The file to read properties from
* @return A populated properties object
* @throws IOException
* Thrown if the specified file is not found or accessed appropriately
*/
public static Properties loadProperties(final File propertiesFile)
throws IOException {
final Properties properties = new Properties();
final FileInputStream fis = new FileInputStream(propertiesFile);
try {
properties.load(fis);
} finally {
fis.close();
}
return properties;
}
/**
* Checks if the operating system on this machine is Windows.
*
* @return True - if using Windows, False - otherwise.
*/
public static boolean isWindows() {
final String os = System.getProperty(
"os.name").toLowerCase();
return os.contains("win");
}
/**
* returns true if the last version check was done more than two weeks ago.
*
* @param session
* the command session.
* @return true if a version check is required else returns false.
*/
public static boolean shouldDoVersionCheck(final CommandSession session) {
final boolean interactive = (Boolean) session.get(Constants.INTERACTIVE_MODE);
if (!interactive) {
return false;
}
final long lastAskedTS = getLastTimeAskedAboutVersionCheck();
// check only if checked over a two weeks ago and user agrees
try {
if (lastAskedTS <= System.currentTimeMillis() - TWO_WEEKS_IN_MILLIS) {
final boolean userConfirms = ShellUtils.promptUser(session, "version_check_confirmation");
registerVersionCheck();
return userConfirms;
}
} catch (final IOException e) {
logger.log(Level.FINE, "Failed to prompt user", e);
}
return false;
}
/**
* Checks if the latest version is used.
*
* @param session
* the command session.
*/
public static void doVersionCheck(final CommandSession session) {
String currentBuildStr = PlatformVersion.getBuildNumber();
if (currentBuildStr.contains("-")) {
currentBuildStr = currentBuildStr.substring(0, currentBuildStr.indexOf("-"));
}
final int currentVersion = Integer.parseInt(currentBuildStr);
final int latestBuild = getLatestBuildNumber(currentVersion);
String message;
if (latestBuild == -1) {
message = ShellUtils.getFormattedMessage("could_not_get_version");
} else if (latestBuild > currentVersion) {
message = ShellUtils.getFormattedMessage("newer_version_exists");
} else {
message = ShellUtils.getFormattedMessage("version_up_to_date");
}
session.getConsole().println(message);
session.getConsole().println();
}
/**
* returns the latest cloudify version.
*
* @param currentVersion
* the current version.
* @return the latest cloudify version.
*/
public static int getLatestBuildNumber(final int currentVersion) {
final HttpClient client = new SystemDefaultHttpClient();
final HttpParams params = client.getParams();
HttpConnectionParams.setConnectionTimeout(params, VERSION_CHECK_READ_TIMEOUT);
HttpConnectionParams.setSoTimeout(params, VERSION_CHECK_READ_TIMEOUT);
final String url = "http://www.gigaspaces.com/downloadgen/latest-cloudify-version?build=" + currentVersion;
final HttpGet httpMethod = new HttpGet(url);
String responseBody = null;
try {
final HttpResponse response = client.execute(httpMethod);
final StatusLine statusLine = response.getStatusLine();
final int statusCode = statusLine.getStatusCode();
if (statusCode != CloudifyConstants.HTTP_STATUS_CODE_OK) {
logger.log(Level.FINE, "Could not get version from server - status code: " + statusCode);
return -1;
}
final HttpEntity entity = response.getEntity();
if (entity == null) {
logger.log(Level.FINE, "Missing response entity in version check");
return -1;
}
final InputStream instream = entity.getContent();
responseBody = StringUtils.getStringFromStream(instream);
logger.fine("Latest cloudify version is " + responseBody);
return Integer.parseInt(responseBody);
} catch (final NumberFormatException e) {
logger.fine("Get version response is not a number: " + responseBody);
return -1;
} catch (final IOException e) {
logger.fine("Failed to get latest version: " + e.getMessage());
return -1;
}
}
/**
* Returns the last time a version check was performed.
*
* @return the last time a version check was performed.
*/
public static long getLastTimeAskedAboutVersionCheck() {
long lastVersionCheckTS = 0;
if (VERSION_CHECK_FILE.exists()) {
DataInputStream dis = null;
try {
dis = new DataInputStream(new FileInputStream(VERSION_CHECK_FILE));
lastVersionCheckTS = dis.readLong();
} catch (final IOException e) {
logger.log(Level.FINE, "failed to read last checked version timestamp file", e);
} finally {
if (dis != null) {
try {
dis.close();
} catch (final IOException e) {
// ignore
}
}
}
}
return lastVersionCheckTS;
}
/**
* updates the file that contains the last version check time.
*/
public static void registerVersionCheck() {
DataOutputStream dos = null;
try {
dos = new DataOutputStream(new FileOutputStream(VERSION_CHECK_FILE));
dos.writeLong(System.currentTimeMillis());
} catch (final IOException e) {
logger.log(Level.FINE, "failed to write last checked version timestamp file", e);
} finally {
if (dos != null) {
try {
dos.close();
} catch (final IOException e) {
// ignore
}
}
}
}
/**
* Checks if the passed security profile uses a secure connection (SSL).
*
* @param springSecurityProfile
* The name of the security profile
* @return true - if the profile indicates SSL is used, false otherwise.
*/
public static boolean isSecureConnection(final String springSecurityProfile) {
final List<String> existingProfiles = Arrays.asList(springSecurityProfile.toLowerCase().split(","));
return existingProfiles.contains(CloudifyConstants.SPRING_PROFILE_SECURE.toLowerCase());
}
/**
* Returns the name of the protocol used for communication with the rest server. If the security is secure (SSL)
* returns "https", otherwise returns "http".
*
* @param springSecurityProfile
* The name of the security profile
* @return "https" if this is a secure connection, "http" otherwise.
*/
public static String getRestProtocol(final String springSecurityProfile) {
return getRestProtocol(isSecureConnection(springSecurityProfile));
}
/**
* Returns the name of the protocol used for communication with the rest server. If the security is secure (SSL)
* returns "https", otherwise returns "http".
*
* @param isSecureConnection
* Indicates whether SSL is used or not.
* @return "https" if this is a secure connection, "http" otherwise.
*/
public static String getRestProtocol(final boolean isSecureConnection) {
if (isSecureConnection) {
return "https";
} else {
return "http";
}
}
/**
* Returns the port used for communication with the rest server.
*
* @param springSecurityProfile
* The name of the security profile
* @return the correct port used by the rest service.
*/
public static int getRestPort(final String springSecurityProfile) {
return getDefaultRestPort(isSecureConnection(springSecurityProfile));
}
/**
* Returns the port used for communication with the rest server.
*
* @param isSecureConnection
* Indicates whether SSL is used or not.
* @return the correct port used by the rest service.
*/
public static int getDefaultRestPort(final boolean isSecureConnection) {
if (isSecureConnection) {
return CloudifyConstants.SECURE_REST_PORT;
} else {
return CloudifyConstants.DEFAULT_REST_PORT;
}
}
/**
* Returns the port used for communication with the rest server.
*
* @param isSecureConnection
* Indicates whether SSL is used or not.
* @return the correct port used by the rest service.
*/
public static String getDefaultRestPortAsString(final boolean isSecureConnection) {
return Integer.toString(getDefaultRestPort(isSecureConnection));
}
/********
* .
*
* @param url
* .
* @param isSecureConnection
* .
* @return .
* @throws MalformedURLException .
*/
public static String getFormattedRestUrl(final String url, final boolean isSecureConnection)
throws MalformedURLException {
String formattedURL = url;
if (!formattedURL.endsWith("/")) {
formattedURL = formattedURL + '/';
}
final String protocolPrefix = ShellUtils.getRestProtocol(isSecureConnection) + "://";
if (!formattedURL.startsWith("http://") && !formattedURL.startsWith("https://")) {
formattedURL = protocolPrefix + formattedURL;
}
URL urlObj;
urlObj = new URL(formattedURL);
if (urlObj.getPort() == -1) {
final StringBuilder urlSB = new StringBuilder(formattedURL);
final int portIndex = formattedURL.indexOf("/", protocolPrefix.length() + 1);
urlSB.insert(portIndex, ':' + ShellUtils.getDefaultRestPortAsString(isSecureConnection));
formattedURL = urlSB.toString();
urlObj = new URL(formattedURL);
}
return formattedURL;
}
/**
* uploads a file to repository using the pre-configured client.
*
* @param client
* .
* @param file
* .
* @param displayer
* .
* @return the returned upload key
* @throws RestClientException .
* @throws CLIException .
*/
public static String uploadToRepo(final RestClient client, final File file, final CLIEventsDisplayer displayer)
throws RestClientException, CLIException {
if (file != null) {
if (!file.isFile()) {
throw new CLIException(file.getAbsolutePath() + " is not a file or is missing");
}
displayer.printEvent("Uploading " + file.getAbsolutePath());
return client.upload(null, file).getUploadKey();
}
return null;
}
}