/******************************************************************************
* Copyright (c) 2012-2015 VMware, Inc. 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 com.vmware.bdd.cli.rest;
import com.vmware.bdd.apitypes.Connect;
import com.vmware.bdd.apitypes.TaskRead;
import com.vmware.bdd.apitypes.TaskRead.Status;
import com.vmware.bdd.apitypes.TaskRead.Type;
import com.vmware.bdd.cli.auth.LoginClient;
import com.vmware.bdd.cli.auth.LoginResponse;
import com.vmware.bdd.cli.commands.CommandsUtils;
import com.vmware.bdd.cli.commands.Constants;
import com.vmware.bdd.cli.commands.CookieCache;
import com.vmware.bdd.exception.BddException;
import com.vmware.bdd.utils.CommonUtil;
import org.apache.commons.io.IOUtils;
import org.apache.http.*;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.log4j.Logger;
import org.fusesource.jansi.AnsiConsole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestTemplate;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* RestClient provides common rest apis required by resource operations.
*
*/
@Component
public class RestClient {
static final Logger logger = Logger.getLogger(RestClient.class);
private String hostUri;
@Autowired
private RestTemplate client;
@Autowired
private LoginClient loginClient;
@Autowired
private HttpClient httpClient;
private RestClient() {
}
public String getContentAsString(String path) {
String uri = hostUri + path;
logger.debug("getContentAsString @ " + uri);
HttpGet getRequest = new HttpGet(uri);
String cookieInfo = readCookieInfo();
getRequest.setHeader("Cookie", cookieInfo == null ? "" : cookieInfo);
try {
HttpResponse response = httpClient.execute(getRequest);
int responseCode = response.getStatusLine().getStatusCode();
InputStream contentStream = response.getEntity().getContent();
String contentAsString = IOUtils.toString(contentStream, "UTF-8");
logger.debug("content as string: " + contentAsString);
if(responseCode != org.apache.http.HttpStatus.SC_OK) {
HttpStatus status = HttpStatus.valueOf(responseCode);
RestErrorHandler.handleHttpErrCode(status, contentAsString);
}
return contentAsString;
} catch (IOException e) {
throw new CliRestException("HTTP Response Error: " + e.getMessage());
} finally {
getRequest.releaseConnection();
}
}
/**
* connect to a Serengeti server
*
* @param host
* host url with optional port
* @param username
* serengeti login user name
* @param password
* serengeti password
*/
public Connect.ConnectType connect(final String host, final String username,
final String password) {
String oldHostUri = hostUri;
hostUri =
Constants.HTTPS_CONNECTION_PREFIX + host
+ Constants.HTTPS_CONNECTION_LOGIN_SUFFIX;
Connect.ConnectType connectType = null;
try {
LoginResponse response = loginClient.login(hostUri, username, password);
//200
if (response.getResponseCode() == HttpStatus.OK.value()) {
if(CommonUtil.isBlank(response.getSessionId())) {
if (isConnected()) {
System.out.println(Constants.CONNECTION_ALREADY_ESTABLISHED);
connectType = Connect.ConnectType.SUCCESS;
} else {
System.out.println(Constants.CONNECT_FAILURE_NO_SESSION_ID);
connectType = Connect.ConnectType.ERROR;
}
} else {
//normal response
writeCookieInfo(response.getSessionId());
System.out.println(Constants.CONNECT_SUCCESS);
connectType = Connect.ConnectType.SUCCESS;
}
}
//401
else if(response.getResponseCode() == HttpStatus.UNAUTHORIZED.value()) {
System.out.println(Constants.CONNECT_UNAUTHORIZATION_CONNECT);
//recover old hostUri
hostUri = oldHostUri;
connectType = Connect.ConnectType.UNAUTHORIZATION;
}
//500
else if(response.getResponseCode() == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
System.out.println(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
connectType = Connect.ConnectType.ERROR;
} else {
//error
System.out.println(
String.format(
Constants.UNSUPPORTED_HTTP_RESPONSE_CODE, response.getResponseCode()));
//recover old hostUri
hostUri = oldHostUri;
connectType = Connect.ConnectType.ERROR;
}
} catch (Exception e) {
System.out.println(Constants.CONNECT_FAILURE + ": "
+ (CommandsUtils.getExceptionMessage(e)));
connectType = Connect.ConnectType.ERROR;
}
return connectType;
}
private boolean isConnected() {
return (hostUri != null) && (!CommandsUtils.isBlank(readCookieInfo()));
}
/**
* Disconnect the session
*/
public void disconnect() {
try {
checkConnection();
getContentAsString(Constants.REST_PATH_LOGOUT);
} catch (CliRestException cliRestException) {
if (cliRestException.getStatus() == HttpStatus.UNAUTHORIZED) {
writeCookieInfo("");
}
} catch (Exception e) {
System.out.println(Constants.DISCONNECT_FAILURE + ": "
+ CommandsUtils.getExceptionMessage(e));
}
}
private void writeCookieInfo(String cookie) {
CookieCache.put(CookieCache.COOKIE, cookie);
}
private String readCookieInfo() {
String cookieValue = "";
cookieValue = CookieCache.get(CookieCache.COOKIE);
return cookieValue;
}
private <T> ResponseEntity<T> restGetById(final String path,
final String id, final Class<T> respEntityType,
final boolean hasDetailQueryString) {
String targetUri =
hostUri + Constants.HTTPS_CONNECTION_API + path + "/" + id;
if (hasDetailQueryString) {
targetUri += Constants.QUERY_DETAIL;
}
return restGetByUri(targetUri, respEntityType);
}
private <T> ResponseEntity<T> restGet(final String path,
final Class<T> respEntityType, final boolean hasDetailQueryString) {
String targetUri = hostUri + Constants.HTTPS_CONNECTION_API + path;
if (hasDetailQueryString) {
targetUri += Constants.QUERY_DETAIL;
}
return restGetByUri(targetUri, respEntityType);
}
private <T> ResponseEntity<T> restGetByUri(String uri,
Class<T> respEntityType) {
HttpHeaders headers = buildHeaders();
HttpEntity<String> entity = new HttpEntity<String>(headers);
return client.exchange(uri, HttpMethod.GET, entity, respEntityType);
}
private HttpHeaders buildHeaders(boolean withCookie) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
List<MediaType> acceptedTypes = new ArrayList<MediaType>();
acceptedTypes.add(MediaType.APPLICATION_JSON);
acceptedTypes.add(MediaType.TEXT_HTML);
headers.setAccept(acceptedTypes);
if (withCookie) {
String cookieInfo = readCookieInfo();
headers.add("Cookie", cookieInfo == null ? "" : cookieInfo);
}
return headers;
}
private HttpHeaders buildHeaders() {
return buildHeaders(true);
}
/**
* Create an object through rest apis
*
* @param entity
* the creation content
* @param path
* the rest url
* @param verb
* the http method
* @param prettyOutput
* output callback
*/
public void createObject(Object entity, final String path,
final HttpMethod verb, PrettyOutput... prettyOutput) {
checkConnection();
try {
if (verb == HttpMethod.POST) {
ResponseEntity<String> response = restPost(path, entity);
if (!validateAuthorization(response)) {
return;
}
processResponse(response, HttpMethod.POST, prettyOutput);
} else {
throw new Exception(Constants.HTTP_VERB_ERROR);
}
} catch (Exception e) {
if(e instanceof CliRestException) {
throw (CliRestException)e;
}
if(e instanceof BddException) {
throw (BddException)e;
}
throw new CliRestException(CommandsUtils.getExceptionMessage(e));
}
}
private ResponseEntity<String> restPost(String path, Object entity) {
String targetUri = hostUri + Constants.HTTPS_CONNECTION_API + path;
HttpHeaders headers = buildHeaders();
HttpEntity<Object> postEntity = new HttpEntity<Object>(entity, headers);
return client.exchange(targetUri, HttpMethod.POST, postEntity,
String.class);
}
/*
* Will process normal response with/without a task location header
*/
private TaskRead processResponse(ResponseEntity<String> response,
HttpMethod verb, PrettyOutput... prettyOutput) throws Exception {
HttpStatus responseStatus = response.getStatusCode();
if (responseStatus == HttpStatus.ACCEPTED) {//Accepted with task in the location header
//get task uri from response to trace progress
HttpHeaders headers = response.getHeaders();
URI taskURI = headers.getLocation();
String[] taskURIs = taskURI.toString().split("/");
String taskId = taskURIs[taskURIs.length - 1];
TaskRead taskRead;
int oldProgress = 0;
Status oldTaskStatus = null;
Status taskStatus = null;
int progress = 0;
do {
ResponseEntity<TaskRead> taskResponse =
restGetById(Constants.REST_PATH_TASK, taskId, TaskRead.class,
false);
//task will not return exception as it has status
taskRead = taskResponse.getBody();
progress = (int) (taskRead.getProgress() * 100);
taskStatus = taskRead.getStatus();
//fix cluster deletion exception
Type taskType = taskRead.getType();
if ((taskType == Type.DELETE) && (taskStatus == TaskRead.Status.COMPLETED)) {
clearScreen();
System.out.println(taskStatus + " " + progress + "%\n");
break;
}
if (taskType == Type.SHRINK && !taskRead.getFailNodes().isEmpty()) {
throw new CliRestException(taskRead.getFailNodes().get(0).getErrorMessage());
}
if ((prettyOutput != null && prettyOutput.length > 0 && prettyOutput[0].isRefresh(true))
|| oldTaskStatus != taskStatus
|| oldProgress != progress) {
//clear screen and show progress every few seconds
clearScreen();
//output completed task summary first in the case there are several related tasks
if (prettyOutput != null && prettyOutput.length > 0
&& prettyOutput[0].getCompletedTaskSummary() != null) {
for (String summary : prettyOutput[0]
.getCompletedTaskSummary()) {
System.out.println(summary + "\n");
}
}
System.out.println(taskStatus + " " + progress + "%\n");
if (prettyOutput != null && prettyOutput.length > 0) {
// print call back customize the detailed output case by case
prettyOutput[0].prettyOutput();
}
if (oldTaskStatus != taskStatus || oldProgress != progress) {
oldTaskStatus = taskStatus;
oldProgress = progress;
if (taskRead.getProgressMessage() != null) {
System.out.println(taskRead.getProgressMessage());
}
}
}
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException ex) {
//ignore
}
} while (taskStatus != TaskRead.Status.COMPLETED
&& taskStatus != TaskRead.Status.FAILED
&& taskStatus != TaskRead.Status.ABANDONED
&& taskStatus != TaskRead.Status.STOPPED);
String errorMsg = taskRead.getErrorMessage();
if (!taskRead.getStatus().equals(TaskRead.Status.COMPLETED)) {
throw new CliRestException(errorMsg);
} else { //completed
if (taskRead.getType().equals(Type.VHM)) {
logger.info("task type is vhm");
Thread.sleep(5*1000);
if (prettyOutput != null && prettyOutput.length > 0
&& prettyOutput[0].isRefresh(true)) {
//clear screen and show progress every few seconds
clearScreen();
System.out.println(taskStatus + " " + progress + "%\n");
// print call back customize the detailed output case by case
if (prettyOutput != null && prettyOutput.length > 0) {
prettyOutput[0].prettyOutput();
}
}
} else {
return taskRead;
}
}
}
return null;
}
private void clearScreen() {
AnsiConsole.systemInstall();
String separator = "[";
char ESC = 27;
String clearScreen = "2J";
System.out.print(ESC + separator + clearScreen);
AnsiConsole.systemUninstall();
}
/**
* Generic method to get an object by id
*
* @param id
* @param entityType
* the object type
* @param path
* the rest url
* @param verb
* the http method
* @param detail
* flag to retrieve detailed information or not
* @return the object
*/
public <T> T getObject(final String id, Class<T> entityType,
final String path, final HttpMethod verb, final boolean detail) {
checkConnection();
try {
if (verb == HttpMethod.GET) {
ResponseEntity<T> response =
restGetById(path, id, entityType, detail);
if (!validateAuthorization(response)) {
return null;
}
T objectRead = response.getBody();
return objectRead;
} else {
throw new Exception(Constants.HTTP_VERB_ERROR);
}
} catch (Exception e) {
if(e instanceof CliRestException) {
throw (CliRestException)e;
}
if(e instanceof BddException) {
throw (BddException)e;
}
throw new CliRestException(CommandsUtils.getExceptionMessage(e));
}
}
public <T> T getObject(final String path, Class<T> entityType, final HttpMethod verb, Object body) {
checkConnection();
try {
if (verb == HttpMethod.POST) {
ResponseEntity<T> response = this.restQueryWithBody(path, entityType, body);
if (!validateAuthorization(response)) {
return null;
}
T objectRead = response.getBody();
return objectRead;
} else {
throw new Exception(Constants.HTTP_VERB_ERROR);
}
} catch (Exception e) {
if(e instanceof CliRestException) {
throw (CliRestException)e;
}
if(e instanceof BddException) {
throw (BddException)e;
}
throw new CliRestException(CommandsUtils.getExceptionMessage(e));
}
}
/**
* Method to get by path
*
* @param entityType
* @param path
* @param verb
* @param detail
* @return
*/
public <T> T getObjectByPath(Class<T> entityType, final String path,
final HttpMethod verb, final boolean detail) {
checkConnection();
try {
if (verb == HttpMethod.GET) {
ResponseEntity<T> response = restGet(path, entityType, detail);
T objectRead = response.getBody();
return objectRead;
} else {
throw new Exception(Constants.HTTP_VERB_ERROR);
}
} catch (Exception e) {
if(e instanceof CliRestException) {
throw (CliRestException)e;
}
if(e instanceof BddException) {
throw (BddException)e;
}
throw new CliRestException(CommandsUtils.getExceptionMessage(e));
}
}
/**
* Generic method to get all objects of a type
*
* @param entityType
* object type
* @param path
* the rest url
* @param verb
* the http method
* @param detail
* flag to retrieve detailed information or not
* @return the objects
*/
public <T> T getAllObjects(final Class<T> entityType, final String path,
final HttpMethod verb, final boolean detail) {
checkConnection();
try {
if (verb == HttpMethod.GET) {
ResponseEntity<T> response = restGet(path, entityType, detail);
if (!validateAuthorization(response)) {
return null;
}
T objectsRead = response.getBody();
return objectsRead;
} else {
throw new Exception(Constants.HTTP_VERB_ERROR);
}
} catch (Exception e) {
throw new CliRestException(CommandsUtils.getExceptionMessage(e));
}
}
/**
* Delete an object by id
*
* @param id
* @param path
* the rest url
* @param verb
* the http method
* @param prettyOutput
* utput callback
*/
public void deleteObject(final String id, final String path,
final HttpMethod verb, PrettyOutput... prettyOutput) {
checkConnection();
try {
if (verb == HttpMethod.DELETE) {
ResponseEntity<String> response = restDelete(path, id);
if (!validateAuthorization(response)) {
return;
}
processResponse(response, HttpMethod.DELETE, prettyOutput);
} else {
throw new Exception(Constants.HTTP_VERB_ERROR);
}
} catch (Exception e) {
if(e instanceof CliRestException) {
throw (CliRestException)e;
}
if(e instanceof BddException) {
throw (BddException)e;
}
throw new CliRestException(CommandsUtils.getExceptionMessage(e));
}
}
private ResponseEntity<String> restDelete(String path, String id) {
String targetUri =
hostUri + Constants.HTTPS_CONNECTION_API + path + "/" + id;
HttpHeaders headers = buildHeaders();
HttpEntity<String> entity = new HttpEntity<String>(headers);
return client
.exchange(targetUri, HttpMethod.DELETE, entity, String.class);
}
private void checkConnection() {
if (hostUri == null) {
throw new CliRestException(Constants.NEED_CONNECTION);
} else if (CommandsUtils.isBlank(readCookieInfo())) {
throw new CliRestException(Constants.CONNECT_CHECK_LOGIN);
}
}
/**
* process requests with query parameters
*
* @param id
* @param path
* the rest url
* @param verb
* the http method
* @param queryStrings
* required query strings
* @param prettyOutput
* output callback
*/
public void actionOps(final String id, final String path,
final HttpMethod verb, final Map<String, String> queryStrings,
PrettyOutput... prettyOutput) {
checkConnection();
try {
if (verb == HttpMethod.PUT) {
ResponseEntity<String> response =
restActionOps(path, id, queryStrings);
if (!validateAuthorization(response)) {
return;
}
processResponse(response, HttpMethod.PUT, prettyOutput);
} else {
throw new Exception(Constants.HTTP_VERB_ERROR);
}
} catch (Exception e) {
throw new CliRestException(CommandsUtils.getExceptionMessage(e));
}
}
private ResponseEntity<String> restActionOps(String path, String id,
Map<String, String> queryStrings) {
String targetUri =
hostUri + Constants.HTTPS_CONNECTION_API + path + "/" + id;
if (queryStrings != null) {
targetUri = targetUri + buildQueryStrings(queryStrings);
}
HttpHeaders headers = buildHeaders();
HttpEntity<String> entity = new HttpEntity<String>(headers);
return client.exchange(targetUri, HttpMethod.PUT, entity, String.class);
}
private String buildQueryStrings(Map<String, String> queryStrings) {
StringBuilder stringBuilder = new StringBuilder("?");
Set<Entry<String, String>> entryset = queryStrings.entrySet();
for (Entry<String, String> entry : entryset) {
stringBuilder.append(entry.getKey() + "=" + entry.getValue() + "&");
}
int length = stringBuilder.length();
if (stringBuilder.charAt(length - 1) == '&') {
return stringBuilder.substring(0, length - 1);
} else {
return stringBuilder.toString();
}
}
/**
* Update an object
*
* @param entity
* the updated content
* @param path
* the rest url
* @param verb
* the http method
* @param prettyOutput
* output callback
*/
public void update(Object entity, final String path, final HttpMethod verb,
PrettyOutput... prettyOutput) {
checkConnection();
try {
if (verb == HttpMethod.PUT) {
ResponseEntity<String> response = restUpdate(path, entity);
if (!validateAuthorization(response)) {
return;
}
processResponse(response, HttpMethod.PUT, prettyOutput);
} else {
throw new Exception(Constants.HTTP_VERB_ERROR);
}
} catch (Exception e) {
if(e instanceof CliRestException) {
throw (CliRestException)e;
}
if(e instanceof BddException) {
throw (BddException)e;
}
throw new CliRestException(CommandsUtils.getExceptionMessage(e));
}
}
public TaskRead updateWithReturn(Object entity, final String path, final HttpMethod verb, PrettyOutput... prettyOutput) {
checkConnection();
try {
if (verb == HttpMethod.PUT) {
ResponseEntity<String> response = restUpdate(path, entity);
if (!validateAuthorization(response)) {
return null;
}
return processResponse(response, HttpMethod.PUT, prettyOutput);
} else {
throw new Exception(Constants.HTTP_VERB_ERROR);
}
} catch (Exception e) {
throw new CliRestException(CommandsUtils.getExceptionMessage(e));
}
}
public void updateWithQueryStrings(Object entity, final String path, Map<String, String> queryStrings,
final HttpMethod verb, PrettyOutput... prettyOutput) {
String queryString = "";
if (queryStrings != null && !queryStrings.isEmpty()) {
queryString = buildQueryStrings(queryStrings);
}
update(entity, path + queryString, verb, prettyOutput);
}
private ResponseEntity<String> restUpdate(String path, Object entityName) {
String targetUri = hostUri + Constants.HTTPS_CONNECTION_API + path;
HttpHeaders headers = buildHeaders();
HttpEntity<Object> entity = new HttpEntity<Object>(entityName, headers);
return client.exchange(targetUri, HttpMethod.PUT, entity, String.class);
}
private <T> ResponseEntity<T> restQueryWithBody(String path, Class<T> entityType, Object body) {
String targetUri = hostUri + Constants.HTTPS_CONNECTION_API + path;
HttpHeaders headers = buildHeaders();
HttpEntity<Object> entity = new HttpEntity<Object>(body, headers);
return client.exchange(targetUri, HttpMethod.POST, entity, entityType);
}
@SuppressWarnings("rawtypes")
private boolean validateAuthorization(ResponseEntity response) {
if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
System.out.println(Constants.CONNECT_UNAUTHORIZATION_OPT);
return false;
}
return true;
}
}