/*******************************************************************************
* Copyright (c) 2013 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.restclient;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.SystemDefaultHttpClient;
import org.cloudifysource.dsl.internal.CloudifyConstants;
import org.cloudifysource.dsl.internal.CloudifyErrorMessages;
import org.cloudifysource.dsl.rest.response.Response;
import org.cloudifysource.restclient.exceptions.RestClientException;
import org.cloudifysource.restclient.exceptions.RestClientHttpException;
import org.cloudifysource.restclient.exceptions.RestClientIOException;
import org.cloudifysource.restclient.messages.MessagesUtils;
import org.cloudifysource.restclient.messages.RestClientMessageKeys;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;
/**
* Creates all the HTTP requests needed for the RestClient and handles the HTTP responses.
* @author yael
*
*/
public class RestClientExecutor {
private static final Logger logger = Logger.getLogger(RestClientExecutor.class.getName());
private static final String FORWARD_SLASH = "/";
private static final int DEFAULT_TRIALS_NUM = 1;
private static final int GET_TRIALS_NUM = 3;
private final SystemDefaultHttpClient httpClient;
private String urlStr;
/**
* C'tor.
* @param httpClient .
* @param url .
*/
public RestClientExecutor(
final SystemDefaultHttpClient httpClient,
final URL url) {
this.httpClient = httpClient;
this.urlStr = url.toExternalForm();
if (!this.urlStr.endsWith(FORWARD_SLASH)) {
this.urlStr += FORWARD_SLASH;
}
}
/**
* Executes HTTP post over REST on the given (relative) URL with the given postBody.
*
* @param url
* The URL to post to.
* @param postBody
* The content of the post.
* @param responseTypeReference
* The type reference of the response.
* @param <T> The type of the response.
* @return The response object from the REST server.
* @throws org.cloudifysource.restclient.exceptions.RestClientException
* Reporting failure to post the file.
*/
public <T> T postObject(
final String url,
final Object postBody,
final TypeReference<Response<T>> responseTypeReference)
throws RestClientException {
final HttpEntity stringEntity;
String jsonStr;
try {
jsonStr = new ObjectMapper().writeValueAsString(postBody);
stringEntity = new StringEntity(jsonStr, "UTF-8");
} catch (final IOException e) {
throw MessagesUtils.createRestClientIOException(
RestClientMessageKeys.SERIALIZATION_ERROR.getName(),
e,
url);
}
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "executing post request to " + url
+ ", tring to post object " + jsonStr);
}
return post(url, responseTypeReference, stringEntity);
}
/**
*
* @param relativeUrl
* The URL to post to.
* @param fileToPost
* The file to post.
* @param partName
* The name of the request parameter (the posted file) to bind to.
* @param responseTypeReference
* The type reference of the response.
* @param <T> The type of the response.
* @return The response object from the REST server.
* @throws RestClientException
* Reporting failure to post the file.
*/
public <T> T postFile(
final String relativeUrl,
final File fileToPost,
final String partName,
final TypeReference<Response<T>> responseTypeReference)
throws RestClientException {
final MultipartEntity multipartEntity = new MultipartEntity();
final FileBody fileBody = new FileBody(fileToPost);
multipartEntity.addPart(partName, fileBody);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "executing post request to " + relativeUrl
+ ", tring to post file " + fileToPost.getName());
}
return post(relativeUrl, responseTypeReference, multipartEntity);
}
/**
*
* @param relativeUrl
* The URL to send the get request to.
* @param responseTypeReference
* The type reference of the response.
* @param <T> The type of the response.
* @return The response object from the REST server.
* @throws RestClientException .
*/
public <T> T get(
final String relativeUrl,
final TypeReference<Response<T>> responseTypeReference)
throws RestClientException {
String fullUrl = getFullUrl(relativeUrl);
final HttpGet getRequest = new HttpGet(fullUrl);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "execute get request to " + relativeUrl);
}
return executeRequest(getRequest, responseTypeReference);
}
/**
*
* @param relativeUrl
* The URL to send the delete request to.
* @param responseTypeReference
* The type reference of the response.
* @param params
* Request parameters
* @param <T> The type of the response.
* @return The response object from the REST server.
* @throws RestClientException .
*/
public <T> T delete(final String relativeUrl, final Map<String, String> params,
final TypeReference<Response<T>> responseTypeReference) throws RestClientException {
URIBuilder builder;
try {
builder = new URIBuilder(getFullUrl(relativeUrl));
} catch (URISyntaxException e) {
throw MessagesUtils.createRestClientException(RestClientMessageKeys.INVALID_URL.getName(), e,
getFullUrl(relativeUrl));
}
if (params != null) {
for (final Map.Entry<String, String> entry : params.entrySet()) {
builder.addParameter(entry.getKey(), entry.getValue());
}
}
final HttpDelete deleteRequest = new HttpDelete(builder.toString());
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "executing delete request to " + relativeUrl);
}
return executeRequest(deleteRequest, responseTypeReference);
}
/**
*
* @param relativeUrl
* The URL to send the delete request to.
* @param responseTypeReference
* The type reference of the response.
* @param <T> The type of the response.
* @return The response object from the REST server.
* @throws RestClientException .
*/
public <T> T delete(final String relativeUrl, final TypeReference<Response<T>> responseTypeReference)
throws RestClientException {
final HttpDelete deleteRequest = new HttpDelete(getFullUrl(relativeUrl));
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "executing delete request to " + relativeUrl);
}
return executeRequest(deleteRequest, responseTypeReference);
}
/**
*
* @param relativeUrl
* The URL to send the delete request to.
* @param responseTypeReference
* The type reference of the response.
* @param <T> The type of the response.
* @return The response object from the REST server.
* @throws RestClientException .
*/
private <T> T post(final String relativeUrl,
final TypeReference<Response<T>> responseTypeReference,
final HttpEntity entity)
throws RestClientException {
final HttpPost postRequest = new HttpPost(getFullUrl(relativeUrl));
if (entity instanceof StringEntity) {
postRequest.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
}
postRequest.setEntity(entity);
return executeRequest(postRequest, responseTypeReference);
}
/**
* Return the response's body.
* @param response .
* @return the response's body.
* @throws RestClientIOException
* if failed to transform the response into string.
*/
public static String getResponseBody(
final HttpResponse response)
throws RestClientIOException {
InputStream instream = null;
try {
final HttpEntity entity = response.getEntity();
if (entity == null) {
return null;
}
instream = entity.getContent();
return StringUtils.getStringFromStream(instream);
} catch (IOException e) {
// this means we couldn't transform the response into string, very unlikely
throw MessagesUtils.createRestClientIOException(
RestClientMessageKeys.READ_RESPONSE_BODY_FAILURE.getName(),
e);
} finally {
if (instream != null) {
try {
instream.close();
} catch (IOException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning(e.getMessage());
}
}
}
}
}
private <T> T executeRequest(final HttpRequestBase request,
final TypeReference<Response<T>> responseTypeReference) throws RestClientException {
HttpResponse httpResponse = null;
try {
IOException lastException = null;
int numOfTrials = DEFAULT_TRIALS_NUM;
if (HttpGet.METHOD_NAME.equals(request.getMethod())) {
numOfTrials = GET_TRIALS_NUM;
}
for (int i = 0; i < numOfTrials; i++) {
try {
httpResponse = httpClient.execute(request);
lastException = null;
break;
} catch (IOException e) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("Execute get request to " + request.getURI()
+ ". try number " + (i + 1) + " out of " + GET_TRIALS_NUM
+ ", error is " + e.getMessage());
}
lastException = e;
}
}
if (lastException != null) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("Failed executing " + request.getMethod() + " request to " + request.getURI()
+ " : " + lastException.getMessage());
}
throw MessagesUtils.createRestClientIOException(
RestClientMessageKeys.EXECUTION_FAILURE.getName(),
lastException,
request.getURI());
}
String url = request.getURI().toString();
checkForError(httpResponse, url);
return getResponseObject(responseTypeReference, httpResponse, url);
} finally {
request.abort();
}
}
private void checkForError(final HttpResponse response, final String requestUri)
throws RestClientException {
StatusLine statusLine = response.getStatusLine();
final int statusCode = statusLine.getStatusCode();
String reasonPhrase = statusLine.getReasonPhrase();
String responseBody;
if (statusCode != HttpStatus.SC_OK) {
responseBody = getResponseBody(response);
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "[checkForError] - REST request to " + requestUri
+ " failed. status code is: " + statusCode + ", response body is: " + responseBody);
}
try {
// this means we managed to read the response
final Response<Void> entity =
new ObjectMapper().readValue(responseBody, new TypeReference<Response<Void>>() { });
// we also have the response in the proper format.
// remember, we only got here because some sort of error happened on the server.
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "[checkForError] - REST request to " + requestUri
+ " failed. throwing RestClientException: [statusCode "
+ statusCode + " reasonPhrase " + reasonPhrase + " defaultMessage " + entity.getMessage()
+ " messageCode" + entity.getMessageId() + "]");
}
throw MessagesUtils.createRestClientResponseException(statusCode,
reasonPhrase,
entity.getVerbose(),
entity.getMessage(),
entity.getMessageId());
} catch (final IOException e) {
// this means we got the response, but it is not in the correct format.
// so some kind of error happened on the spring side.
if (logger.isLoggable(Level.WARNING)) {
logger.log(Level.WARNING, "[checkForError] - failed to read response. responseBody: "
+ responseBody + ", reasonPhrase:" + reasonPhrase);
}
if (statusCode == CloudifyConstants.HTTP_STATUS_NOT_FOUND) {
throw MessagesUtils.createRestClientHttpException(
e,
statusCode,
reasonPhrase,
responseBody,
RestClientMessageKeys.URL_NOT_FOUND.getName(), requestUri);
} else if (statusCode == CloudifyConstants.HTTP_STATUS_ACCESS_DENIED) {
throw MessagesUtils.createRestClientHttpException(
e,
statusCode,
reasonPhrase,
responseBody,
RestClientMessageKeys.NO_PERMISSION_ACCESS_DENIED.getName());
} else if (statusCode == CloudifyConstants.HTTP_STATUS_UNAUTHORIZED) {
throw MessagesUtils.createRestClientHttpException(
e,
statusCode,
reasonPhrase,
responseBody,
CloudifyErrorMessages.UNAUTHORIZED.getName(),
reasonPhrase,
requestUri);
} else {
throw MessagesUtils.createRestClientHttpException(
e,
statusCode,
reasonPhrase,
responseBody,
RestClientMessageKeys.HTTP_FAILURE.getName(), reasonPhrase, requestUri);
}
}
}
}
private <T> T getResponseObject(
final TypeReference<Response<T>> typeReference,
final HttpResponse httpResponse, final String url)
throws RestClientIOException, RestClientHttpException {
final String responseBody = getResponseBody(httpResponse);
Response<T> response;
try {
response = new ObjectMapper().readValue(responseBody, typeReference);
return response.getResponse();
} catch (IOException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.finer("failed to read the responseBody (of request to " + url + ")."
+ ", error was " + e.getMessage());
}
// this means we got the response, but it is not in the correct format.
// so some kind of error happened on the spring side.
StatusLine statusLine = httpResponse.getStatusLine();
String reasonPhrase = statusLine.getReasonPhrase();
throw MessagesUtils.createRestClientHttpException(
e,
statusLine.getStatusCode(),
reasonPhrase,
responseBody,
RestClientMessageKeys.HTTP_FAILURE.getName(), reasonPhrase, url);
}
}
/**
* Appends the given relative URL to the basic rest-service URL.
*
* @param relativeUrl
* URL to add to the basic URL
* @return full URL as as String
*/
private String getFullUrl(final String relativeUrl) {
String safeRelativeURL = relativeUrl;
if (safeRelativeURL.startsWith(FORWARD_SLASH)) {
safeRelativeURL = safeRelativeURL.substring(1);
}
return urlStr + safeRelativeURL;
}
/**
*
* @param username .
* @param password .
*/
public void setCredentials(final String username, final String password) {
if (StringUtils.notEmpty(username) && StringUtils.notEmpty(password)) {
httpClient.getCredentialsProvider().setCredentials(new AuthScope(AuthScope.ANY),
new UsernamePasswordCredentials(username, password));
}
}
}