Delegate HTTP protocol activity in RESTServiceConnector to RestClient - All HTTP protocol activities are now handled by RestClient - This service is now only responsible for creating requests, and dispatching them to the client - Provides a Simple API for creating, updating, retrieving and deleting objects
Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/de63b94f Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/de63b94f Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/de63b94f Branch: refs/heads/deploy-from-snapshot Commit: de63b94f2dc571e74ce5eaa68c74ed6a9edf420c Parents: 8a93bb8 Author: Miguel Ferreira <miguelferre...@me.com> Authored: Sun Aug 23 15:11:29 2015 +0200 Committer: Miguel Ferreira <miguelferre...@me.com> Committed: Tue Aug 25 17:36:14 2015 +0200 ---------------------------------------------------------------------- .../BasicEncodedRESTValidationStrategy.java | 66 --- .../cloud/utils/rest/RESTServiceConnector.java | 391 +++---------- .../utils/rest/RESTValidationStrategy.java | 159 ------ .../utils/rest/HttpUriRequestMethodMatcher.java | 44 ++ .../rest/HttpUriRequestPayloadMatcher.java | 62 +++ .../utils/rest/RESTServiceConnectorTest.java | 553 ++++++++----------- 6 files changed, 430 insertions(+), 845 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cloudstack/blob/de63b94f/utils/src/main/java/com/cloud/utils/rest/BasicEncodedRESTValidationStrategy.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/rest/BasicEncodedRESTValidationStrategy.java b/utils/src/main/java/com/cloud/utils/rest/BasicEncodedRESTValidationStrategy.java deleted file mode 100644 index bf001cd..0000000 --- a/utils/src/main/java/com/cloud/utils/rest/BasicEncodedRESTValidationStrategy.java +++ /dev/null @@ -1,66 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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.cloud.utils.rest; - -import java.io.IOException; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpException; -import org.apache.commons.httpclient.HttpMethodBase; - -/** - * Base 64 encoded authorization strategy. This implementation as opposed to - * {@link RESTValidationStrategy} doesn't do a login after auth error, but instead - * includes the encoded credentials in each request, instead of a cookie. - */ -public class BasicEncodedRESTValidationStrategy extends RESTValidationStrategy { - - public BasicEncodedRESTValidationStrategy(final String host, final String adminuser, final String adminpass) { - super(); - this.host = host; - user = adminuser; - password = adminpass; - } - - public BasicEncodedRESTValidationStrategy() { - } - - @Override - public void executeMethod(final HttpMethodBase method, final HttpClient client, - final String protocol) - throws CloudstackRESTException, HttpException, IOException { - if (host == null || host.isEmpty() || user == null || user.isEmpty() || password == null || password.isEmpty()) { - throw new CloudstackRESTException("Hostname/credentials are null or empty"); - } - - final String encodedCredentials = encodeCredentials(); - method.setRequestHeader("Authorization", "Basic " + encodedCredentials); - client.executeMethod(method); - } - - private String encodeCredentials() { - final String authString = user + ":" + password; - final byte[] authEncBytes = Base64.encodeBase64(authString.getBytes()); - final String authStringEnc = new String(authEncBytes); - return authStringEnc; - } - -} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/de63b94f/utils/src/main/java/com/cloud/utils/rest/RESTServiceConnector.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/rest/RESTServiceConnector.java b/utils/src/main/java/com/cloud/utils/rest/RESTServiceConnector.java index 01aca89..fc5bae3 100644 --- a/utils/src/main/java/com/cloud/utils/rest/RESTServiceConnector.java +++ b/utils/src/main/java/com/cloud/utils/rest/RESTServiceConnector.java @@ -20,371 +20,146 @@ package com.cloud.utils.rest; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.lang.reflect.Type; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.Socket; -import java.net.URL; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; import java.util.Map; -import java.util.Map.Entry; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import org.apache.cloudstack.utils.security.SSLUtils; -import org.apache.cloudstack.utils.security.SecureSSLSocketFactory; -import org.apache.commons.httpclient.ConnectTimeoutException; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpException; -import org.apache.commons.httpclient.HttpMethod; -import org.apache.commons.httpclient.HttpMethodBase; -import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; -import org.apache.commons.httpclient.NameValuePair; -import org.apache.commons.httpclient.cookie.CookiePolicy; -import org.apache.commons.httpclient.methods.DeleteMethod; -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.methods.PutMethod; -import org.apache.commons.httpclient.methods.StringRequestEntity; -import org.apache.commons.httpclient.params.HttpConnectionParams; -import org.apache.commons.httpclient.protocol.Protocol; -import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; -import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.util.EntityUtils; import org.apache.log4j.Logger; +import com.google.common.base.Optional; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializer; -import com.google.gson.reflect.TypeToken; /** - * This abstraction encapsulates client side code for REST service communication. It encapsulates access in a delegate validation strategy. There may different implementations - * extending {@link RESTValidationStrategy}, and any of them should mention the needed data to work. + * This abstraction encapsulates client side code for REST service communication. It encapsulates access in a REST client. There can be different implementations of that client + * implementing {@link RestClient}, and any of them should mention the needed data to work. * - * This connector allows the use of {@link JsonDeserializer} for specific classes. You can provide in the constructor a list of classes and a list of deserializers for these - * classes. These should be a correlated so that Nth deserializer is correctly mapped to Nth class. + * This connector allows the use of {@link JsonDeserializer} for specific classes. You can provide in the constructor a map of classes to deserializers for these classes. */ public class RESTServiceConnector { - private static final String HTTPS = "https"; - protected static final String GET_METHOD_TYPE = "get"; - protected static final String DELETE_METHOD_TYPE = "delete"; - protected static final String PUT_METHOD_TYPE = "put"; - protected static final String POST_METHOD_TYPE = "post"; - private static final String TEXT_HTML_CONTENT_TYPE = "text/html"; - private static final String JSON_CONTENT_TYPE = "application/json"; - private static final String CONTENT_TYPE = "Content-Type"; - private static final int BODY_RESP_MAX_LEN = 1024; - private static final int HTTPS_PORT = 443; private static final Logger s_logger = Logger.getLogger(RESTServiceConnector.class); - protected final static String protocol = HTTPS; - - private final static MultiThreadedHttpConnectionManager s_httpClientManager = new MultiThreadedHttpConnectionManager(); - - protected RESTValidationStrategy validation; - - private final HttpClient client; + private static final Optional<String> ABSENT = Optional.absent(); + private final RestClient client; private final Gson gson; - /** - * Getter that may be needed only for test purpose - * - * @return - */ - public Gson getGson() { - return gson; + private RESTServiceConnector(final Builder builder) { + client = builder.client; + gson = setGsonDeserializer(builder.classToDeserializerMap); } - public RESTServiceConnector(final RESTValidationStrategy validationStrategy) { - this(validationStrategy, null, null); - } - - public RESTServiceConnector(final RESTValidationStrategy validationStrategy, final List<Class<?>> classList, final List<JsonDeserializer<?>> deserializerList) { - validation = validationStrategy; - client = createHttpClient(); - client.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY); - - try { - // Cast to ProtocolSocketFactory to avoid the deprecated constructor with the SecureProtocolSocketFactory parameter - Protocol.registerProtocol(HTTPS, new Protocol(HTTPS, (ProtocolSocketFactory) new TrustingProtocolSocketFactory(), HTTPS_PORT)); - } catch (final IOException e) { - s_logger.warn("Failed to register the TrustingProtocolSocketFactory, falling back to default SSLSocketFactory", e); - } - + private static Gson setGsonDeserializer(final Map<Class<?>, JsonDeserializer<?>> classToDeserializerMap) { final GsonBuilder gsonBuilder = new GsonBuilder(); - if (classList != null && deserializerList != null) { - for (int i = 0; i < classList.size() && i < deserializerList.size(); i++) { - gsonBuilder.registerTypeAdapter(classList.get(i), deserializerList.get(i)); - } + for (final Map.Entry<Class<?>, JsonDeserializer<?>> entry : classToDeserializerMap.entrySet()) { + gsonBuilder.registerTypeAdapter(entry.getKey(), entry.getValue()); } - gson = gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + return gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); } - public HttpClient createHttpClient() { - return new HttpClient(s_httpClientManager); + public static Builder create() { + return new Builder(); } - public HttpMethod createMethod(final String type, final String uri) throws CloudstackRESTException { - String url; - try { - url = new URL(protocol, validation.getHost(), uri).toString(); - } catch (final MalformedURLException e) { - s_logger.error("Unable to build REST Service URL", e); - throw new CloudstackRESTException("Unable to build Nicira API URL", e); - } - - if (POST_METHOD_TYPE.equalsIgnoreCase(type)) { - return new PostMethod(url); - } else if (GET_METHOD_TYPE.equalsIgnoreCase(type)) { - return new GetMethod(url); - } else if (DELETE_METHOD_TYPE.equalsIgnoreCase(type)) { - return new DeleteMethod(url); - } else if (PUT_METHOD_TYPE.equalsIgnoreCase(type)) { - return new PutMethod(url); - } else { - throw new CloudstackRESTException("Requesting unknown method type"); - } + public <T> void executeUpdateObject(final T newObject, final String path, final Map<String, String> parameters) throws CloudstackRESTException { + s_logger.debug("Executing update object on " + path); + client.closeResponse(createAndExecuteRequest(HttpMethods.PUT, path, parameters, Optional.fromNullable(gson.toJson(newObject)))); } - public void setControllerAddress(final String address) { - validation.setHost(address); + public <T> void executeUpdateObject(final T newObject, final String path) throws CloudstackRESTException { + executeUpdateObject(newObject, path, new HashMap<String, String>()); } - public void setAdminCredentials(final String username, final String password) { - validation.setUser(username); - validation.setPassword(password); + @SuppressWarnings("unchecked") + public <T> T executeCreateObject(final T newObject, final String path, final Map<String, String> parameters) throws CloudstackRESTException { + s_logger.debug("Executing create object on " + path); + final CloseableHttpResponse response = createAndExecuteRequest(HttpMethods.POST, path, parameters, Optional.fromNullable(gson.toJson(newObject))); + return (T) readResponseBody(response, newObject.getClass()); } - public <T> void executeUpdateObject(final T newObject, final String uri, final Map<String, String> parameters) throws CloudstackRESTException { - - final PutMethod pm = (PutMethod) createMethod(PUT_METHOD_TYPE, uri); - pm.setRequestHeader(CONTENT_TYPE, JSON_CONTENT_TYPE); - try { - pm.setRequestEntity(new StringRequestEntity(gson.toJson(newObject), JSON_CONTENT_TYPE, null)); - } catch (final UnsupportedEncodingException e) { - throw new CloudstackRESTException("Failed to encode json request body", e); - } - - executeMethod(pm); - - if (pm.getStatusCode() != HttpStatus.SC_OK) { - final String errorMessage = responseToErrorMessage(pm); - pm.releaseConnection(); - s_logger.error("Failed to update object : " + errorMessage); - throw new CloudstackRESTException("Failed to update object : " + errorMessage); - } - pm.releaseConnection(); + public <T> T executeCreateObject(final T newObject, final String uri) throws CloudstackRESTException { + return executeCreateObject(newObject, uri, new HashMap<String, String>()); } - @SuppressWarnings("unchecked") - public <T> T executeCreateObject(final T newObject, final Type returnObjectType, final String uri, final Map<String, String> parameters) throws CloudstackRESTException { - - final PostMethod pm = (PostMethod) createMethod(POST_METHOD_TYPE, uri); - pm.setRequestHeader(CONTENT_TYPE, JSON_CONTENT_TYPE); - try { - pm.setRequestEntity(new StringRequestEntity(gson.toJson(newObject), JSON_CONTENT_TYPE, null)); - } catch (final UnsupportedEncodingException e) { - throw new CloudstackRESTException("Failed to encode json request body", e); - } - - executeMethod(pm); - - if (pm.getStatusCode() != HttpStatus.SC_CREATED) { - final String errorMessage = responseToErrorMessage(pm); - pm.releaseConnection(); - s_logger.error("Failed to create object : " + errorMessage); - throw new CloudstackRESTException("Failed to create object : " + errorMessage); - } - - T result; - try { - result = (T) gson.fromJson(pm.getResponseBodyAsString(), TypeToken.get(newObject.getClass()).getType()); - } catch (final IOException e) { - throw new CloudstackRESTException("Failed to decode json response body", e); - } finally { - pm.releaseConnection(); - } - - return result; + public void executeDeleteObject(final String path) throws CloudstackRESTException { + s_logger.debug("Executing delete object on " + path); + client.closeResponse(createAndExecuteRequest(HttpMethods.DELETE, path, new HashMap<String, String>(), ABSENT)); } - public void executeDeleteObject(final String uri) throws CloudstackRESTException { - final DeleteMethod dm = (DeleteMethod) createMethod(DELETE_METHOD_TYPE, uri); - dm.setRequestHeader(CONTENT_TYPE, JSON_CONTENT_TYPE); - - executeMethod(dm); - - if (dm.getStatusCode() != HttpStatus.SC_NO_CONTENT) { - final String errorMessage = responseToErrorMessage(dm); - dm.releaseConnection(); - s_logger.error("Failed to delete object : " + errorMessage); - throw new CloudstackRESTException("Failed to delete object : " + errorMessage); - } - dm.releaseConnection(); + public <T> T executeRetrieveObject(final Type returnObjectType, final String path, final Map<String, String> parameters) throws CloudstackRESTException { + s_logger.debug("Executing retrieve object on " + path); + final CloseableHttpResponse response = createAndExecuteRequest(HttpMethods.GET, path, parameters, ABSENT); + return readResponseBody(response, returnObjectType); } - @SuppressWarnings("unchecked") - public <T> T executeRetrieveObject(final Type returnObjectType, final String uri, final Map<String, String> parameters) throws CloudstackRESTException { - final GetMethod gm = (GetMethod) createMethod(GET_METHOD_TYPE, uri); - gm.setRequestHeader(CONTENT_TYPE, JSON_CONTENT_TYPE); - if (parameters != null && !parameters.isEmpty()) { - final List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(parameters.size()); - for (final Entry<String, String> e : parameters.entrySet()) { - nameValuePairs.add(new NameValuePair(e.getKey(), e.getValue())); - } - gm.setQueryString(nameValuePairs.toArray(new NameValuePair[0])); - } - - executeMethod(gm); + public <T> T executeRetrieveObject(final Type returnObjectType, final String path) throws CloudstackRESTException { + return executeRetrieveObject(returnObjectType, path, new HashMap<String, String>()); + } - if (gm.getStatusCode() != HttpStatus.SC_OK) { - final String errorMessage = responseToErrorMessage(gm); - gm.releaseConnection(); - s_logger.error("Failed to retrieve object : " + errorMessage); - throw new CloudstackRESTException("Failed to retrieve object : " + errorMessage); - } + private CloseableHttpResponse createAndExecuteRequest(final HttpMethods method, final String path, final Map<String, String> parameters, final Optional<String> jsonPayLoad) + throws CloudstackRESTException { + final HttpUriRequest httpRequest = HttpUriRequestBuilder.create() + .path(path) + .parameters(parameters) + .jsonPayload(jsonPayLoad) + .method(method) + .build(); + if (jsonPayLoad.isPresent()) { + s_logger.debug("Built request '" + httpRequest + "' with payload: " + jsonPayLoad); + } + return executeRequest(httpRequest); + } - T returnValue; - try { - returnValue = (T) gson.fromJson(gm.getResponseBodyAsString(), returnObjectType); - } catch (final IOException e) { - s_logger.error("IOException while retrieving response body", e); - throw new CloudstackRESTException(e); - } finally { - gm.releaseConnection(); - } - return returnValue; + private CloseableHttpResponse executeRequest(final HttpUriRequest httpRequest) throws CloudstackRESTException { + final CloseableHttpResponse response = client.execute(httpRequest); + s_logger.debug("Executed request: " + httpRequest + " status was " + response.getStatusLine().toString()); + return response; } - public void executeMethod(final HttpMethodBase method) throws CloudstackRESTException { + private <T> T readResponseBody(final CloseableHttpResponse response, final Type type) throws CloudstackRESTException { + final HttpEntity entity = response.getEntity(); try { - validation.executeMethod(method, client, protocol); - } catch (final HttpException e) { - s_logger.error("HttpException caught while trying to connect to the REST Service", e); - method.releaseConnection(); - throw new CloudstackRESTException("API call to REST Service Failed", e); + final String stringEntity = EntityUtils.toString(entity); + s_logger.debug("Response entity: " + stringEntity); + EntityUtils.consumeQuietly(entity); + return gson.fromJson(stringEntity, type); } catch (final IOException e) { - s_logger.error("IOException caught while trying to connect to the REST Service", e); - method.releaseConnection(); - throw new CloudstackRESTException("API call to Nicira REST Service Failed", e); - } - } - - private static String responseToErrorMessage(final HttpMethodBase method) { - assert method.isRequestSent() : "no use getting an error message unless the request is sent"; - - if (TEXT_HTML_CONTENT_TYPE.equals(method.getResponseHeader(CONTENT_TYPE).getValue())) { - // The error message is the response content - // Safety margin of 1024 characters, anything longer is probably useless - // and will clutter the logs - try { - return method.getResponseBodyAsString(BODY_RESP_MAX_LEN); - } catch (final IOException e) { - s_logger.debug("Error while loading response body", e); - } + throw new CloudstackRESTException("Could not deserialize response to JSON. Entity: " + entity, e); + } finally { + client.closeResponse(response); } - // The default - return method.getStatusText(); } - /* - * Some controllers use a self-signed certificate. The TrustingProtocolSocketFactory will accept any provided certificate when making an SSL connection to the SDN Manager - */ - private class TrustingProtocolSocketFactory implements SecureProtocolSocketFactory { - - private SSLSocketFactory ssf; - - public TrustingProtocolSocketFactory() throws IOException { - // Create a trust manager that does not validate certificate chains - final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { - @Override - public X509Certificate[] getAcceptedIssuers() { - return null; - } - - @Override - public void checkClientTrusted(final X509Certificate[] certs, final String authType) { - // Trust always - } - - @Override - public void checkServerTrusted(final X509Certificate[] certs, final String authType) { - // Trust always - } - } }; - - try { - // Install the all-trusting trust manager - final SSLContext sc = SSLUtils.getSSLContext(); - sc.init(null, trustAllCerts, new java.security.SecureRandom()); - ssf = new SecureSSLSocketFactory(sc); - } catch (final KeyManagementException e) { - throw new IOException(e); - } catch (final NoSuchAlgorithmException e) { - throw new IOException(e); - } - } + public static class Builder { + private RestClient client; + final private Map<Class<?>, JsonDeserializer<?>> classToDeserializerMap = new HashMap<Class<?>, JsonDeserializer<?>>(); - @Override - public Socket createSocket(final String host, final int port) throws IOException { - final SSLSocket socket = (SSLSocket) ssf.createSocket(host, port); - socket.setEnabledProtocols(SSLUtils.getSupportedProtocols(socket.getEnabledProtocols())); - return socket; + public Builder client(final RestClient client) { + this.client = client; + return this; } - @Override - public Socket createSocket(final String address, final int port, final InetAddress localAddress, final int localPort) throws IOException, UnknownHostException { - final Socket socket = ssf.createSocket(address, port, localAddress, localPort); - if (socket instanceof SSLSocket) { - ((SSLSocket) socket).setEnabledProtocols(SSLUtils.getSupportedProtocols(((SSLSocket) socket).getEnabledProtocols())); - } - return socket; + public Builder classToDeserializerMap(final Map<Class<?>, JsonDeserializer<?>> classToDeserializerMap) { + this.classToDeserializerMap.clear(); + this.classToDeserializerMap.putAll(classToDeserializerMap); + return this; } - @Override - public Socket createSocket(final Socket socket, final String host, final int port, final boolean autoClose) throws IOException, UnknownHostException { - final Socket s = ssf.createSocket(socket, host, port, autoClose); - if (s instanceof SSLSocket) { - ((SSLSocket) s).setEnabledProtocols(SSLUtils.getSupportedProtocols(((SSLSocket) s).getEnabledProtocols())); - } - return s; + public Builder classToDeserializerEntry(final Class<?> clazz, final JsonDeserializer<?> deserializer) { + classToDeserializerMap.put(clazz, deserializer); + return this; } - @Override - public Socket createSocket(final String host, final int port, final InetAddress localAddress, final int localPort, final HttpConnectionParams params) throws IOException, - UnknownHostException, ConnectTimeoutException { - final int timeout = params.getConnectionTimeout(); - if (timeout == 0) { - final Socket socket = createSocket(host, port, localAddress, localPort); - if (socket instanceof SSLSocket) { - ((SSLSocket) socket).setEnabledProtocols(SSLUtils.getSupportedProtocols(((SSLSocket) socket).getEnabledProtocols())); - } - return socket; - } else { - final Socket s = ssf.createSocket(); - if (s instanceof SSLSocket) { - ((SSLSocket) s).setEnabledProtocols(SSLUtils.getSupportedProtocols(((SSLSocket) s).getEnabledProtocols())); - } - s.bind(new InetSocketAddress(localAddress, localPort)); - s.connect(new InetSocketAddress(host, port), timeout); - return s; - } + public RESTServiceConnector build() { + return new RESTServiceConnector(this); } } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/de63b94f/utils/src/main/java/com/cloud/utils/rest/RESTValidationStrategy.java ---------------------------------------------------------------------- diff --git a/utils/src/main/java/com/cloud/utils/rest/RESTValidationStrategy.java b/utils/src/main/java/com/cloud/utils/rest/RESTValidationStrategy.java deleted file mode 100644 index 8ec920a..0000000 --- a/utils/src/main/java/com/cloud/utils/rest/RESTValidationStrategy.java +++ /dev/null @@ -1,159 +0,0 @@ -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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.cloud.utils.rest; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; - -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpException; -import org.apache.commons.httpclient.HttpMethodBase; -import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.log4j.Logger; - -/** - * Basic authentication strategy. This strategy needs user and password for authentication. - * - * A login URL is needed which will be used for login and getting the cookie to be used in next requests. If an executeMethod request fails due to authorization it will try to - * login, get the cookie and repeat the attempt to execute the method. - */ -public class RESTValidationStrategy { - - private static final Logger s_logger = Logger.getLogger(RESTValidationStrategy.class); - - protected String host; - protected String user; - protected String password; - protected String serverVersion; - protected String loginUrl; - - public RESTValidationStrategy(final String host, final String user, final String password, final String serverVersion, final String loginUrl) { - super(); - this.host = host; - this.user = user; - this.password = password; - this.serverVersion = serverVersion; - this.loginUrl = loginUrl; - } - - public RESTValidationStrategy(final String loginUrl) { - this.loginUrl = loginUrl; - } - - public RESTValidationStrategy() { - } - - public String getUser() { - return user; - } - - public void setUser(final String user) { - this.user = user; - } - - public String getPassword() { - return password; - } - - public void setPassword(final String password) { - this.password = password; - } - - public String getLoginUrl() { - return loginUrl; - } - - public void setLoginUrl(final String loginUrl) { - this.loginUrl = loginUrl; - } - - public String getHost() { - return host; - } - - public void setHost(final String host) { - this.host = host; - } - - public void executeMethod(final HttpMethodBase method, final HttpClient client, final String protocol) throws CloudstackRESTException, HttpException, IOException { - if (host == null || host.isEmpty() || user == null || user.isEmpty() || password == null || password.isEmpty()) { - throw new CloudstackRESTException("Hostname/credentials are null or empty"); - } - - client.executeMethod(method); - if (method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) { - method.releaseConnection(); - // login and try again - login(protocol, client); - client.executeMethod(method); - } - } - - /** - * Logs against the REST server. The cookie is stored in the <code>_authcookie<code> variable. - * <p> - * The method returns false if the login failed or the connection could not be made. - * - */ - protected void login(final String protocol, final HttpClient client) throws CloudstackRESTException { - String url; - - if (host == null || host.isEmpty() || user == null || user.isEmpty() || password == null || password.isEmpty()) { - throw new CloudstackRESTException("Hostname/credentials are null or empty"); - } - - try { - url = new URL(protocol, host, loginUrl).toString(); - } catch (final MalformedURLException e) { - s_logger.error("Unable to build Nicira API URL", e); - throw new CloudstackRESTException("Unable to build Nicira API URL", e); - } - - final PostMethod pm = new PostMethod(url); - pm.addParameter("username", user); - pm.addParameter("password", password); - - try { - client.executeMethod(pm); - } catch (final HttpException e) { - throw new CloudstackRESTException("REST Service API login failed ", e); - } catch (final IOException e) { - throw new CloudstackRESTException("REST Service API login failed ", e); - } finally { - pm.releaseConnection(); - } - - if (pm.getStatusCode() != HttpStatus.SC_OK) { - s_logger.error("REST Service API login failed : " + pm.getStatusText()); - throw new CloudstackRESTException("REST Service API login failed " + pm.getStatusText()); - } - - // Extract the version for later use - if (pm.getResponseHeader("Server") != null) { - serverVersion = pm.getResponseHeader("Server").getValue(); - s_logger.debug("Server reports version " + serverVersion); - } - - // Success; the cookie required for login is kept in _client - } - -} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/de63b94f/utils/src/test/java/com/cloud/utils/rest/HttpUriRequestMethodMatcher.java ---------------------------------------------------------------------- diff --git a/utils/src/test/java/com/cloud/utils/rest/HttpUriRequestMethodMatcher.java b/utils/src/test/java/com/cloud/utils/rest/HttpUriRequestMethodMatcher.java new file mode 100644 index 0000000..f2d091f --- /dev/null +++ b/utils/src/test/java/com/cloud/utils/rest/HttpUriRequestMethodMatcher.java @@ -0,0 +1,44 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.cloud.utils.rest; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Matchers.argThat; + +import org.apache.http.client.methods.HttpUriRequest; +import org.hamcrest.FeatureMatcher; +import org.hamcrest.Matcher; + +public class HttpUriRequestMethodMatcher extends FeatureMatcher<HttpUriRequest, String> { + + public static HttpUriRequest aMethod(final String method) { + return argThat(new HttpUriRequestMethodMatcher(equalTo(method), "method", "method")); + } + + public HttpUriRequestMethodMatcher(final Matcher<? super String> subMatcher, final String featureDescription, final String featureName) { + super(subMatcher, featureDescription, featureName); + } + + @Override + protected String featureValueOf(final HttpUriRequest actual) { + return actual.getMethod(); + } + +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/de63b94f/utils/src/test/java/com/cloud/utils/rest/HttpUriRequestPayloadMatcher.java ---------------------------------------------------------------------- diff --git a/utils/src/test/java/com/cloud/utils/rest/HttpUriRequestPayloadMatcher.java b/utils/src/test/java/com/cloud/utils/rest/HttpUriRequestPayloadMatcher.java new file mode 100644 index 0000000..724f495 --- /dev/null +++ b/utils/src/test/java/com/cloud/utils/rest/HttpUriRequestPayloadMatcher.java @@ -0,0 +1,62 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.cloud.utils.rest; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Matchers.argThat; + +import java.io.IOException; + +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.ParseException; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.util.EntityUtils; +import org.hamcrest.FeatureMatcher; +import org.hamcrest.Matcher; + +public class HttpUriRequestPayloadMatcher extends FeatureMatcher<HttpUriRequest, String> { + + public static HttpUriRequest aPayload(final String payload) { + return argThat(hasPayload(payload)); + } + + public static HttpUriRequestPayloadMatcher hasPayload(final String payload) { + return new HttpUriRequestPayloadMatcher(equalTo(payload), "payload", "payload"); + } + + public HttpUriRequestPayloadMatcher(final Matcher<? super String> subMatcher, final String featureDescription, final String featureName) { + super(subMatcher, featureDescription, featureName); + } + + @Override + protected String featureValueOf(final HttpUriRequest actual) { + String payload = ""; + if (actual instanceof HttpEntityEnclosingRequest) { + try { + payload = EntityUtils.toString(((HttpEntityEnclosingRequest) actual).getEntity()); + } catch (final ParseException e) { + throw new IllegalArgumentException("Couldn't read request's entity payload.", e); + } catch (final IOException e) { + throw new IllegalArgumentException("Couldn't read request's entity payload.", e); + } + } + return payload; + } +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/de63b94f/utils/src/test/java/com/cloud/utils/rest/RESTServiceConnectorTest.java ---------------------------------------------------------------------- diff --git a/utils/src/test/java/com/cloud/utils/rest/RESTServiceConnectorTest.java b/utils/src/test/java/com/cloud/utils/rest/RESTServiceConnectorTest.java index b177fe1..e26ee6a 100644 --- a/utils/src/test/java/com/cloud/utils/rest/RESTServiceConnectorTest.java +++ b/utils/src/test/java/com/cloud/utils/rest/RESTServiceConnectorTest.java @@ -19,376 +19,305 @@ package com.cloud.utils.rest; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.io.IOException; -import java.util.Collections; - -import org.apache.commons.httpclient.Header; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpException; -import org.apache.commons.httpclient.HttpMethod; -import org.apache.commons.httpclient.methods.DeleteMethod; -import org.apache.commons.httpclient.methods.GetMethod; -import org.apache.commons.httpclient.methods.PostMethod; -import org.apache.commons.httpclient.methods.PutMethod; -import org.apache.commons.httpclient.params.HttpClientParams; -import org.apache.http.HttpStatus; -import org.junit.Before; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.CollectionType; import org.junit.Test; -public class RESTServiceConnectorTest { - protected static final String UUID = "aaaa"; - protected static final String UUID_JSON_RESPONSE = "{\"uuid\" : \"aaaa\"}"; - - RESTServiceConnector connector; - HttpClient client = mock(HttpClient.class); - HttpMethod method; - String type; - String uri; - - @Before - public void setUp() { - final HttpClientParams hmp = mock(HttpClientParams.class); - when(client.getParams()).thenReturn(hmp); - connector = new RESTServiceConnector(null) { - @Override - public HttpClient createHttpClient() { - return client; - } - - @Override - public HttpMethod createMethod(final String newType, final String newUri) { - type = newType; - uri = newUri; - return method; - } - }; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; - connector.validation = new RESTValidationStrategy(); - connector.setAdminCredentials("admin", "adminpass"); - connector.setControllerAddress("localhost"); - } - - @Test(expected = CloudstackRESTException.class) - public void testExecuteLoginWithoutHostname() throws CloudstackRESTException { - connector.setControllerAddress(null); - connector.validation.login(RESTServiceConnector.protocol, client); +public class RESTServiceConnectorTest { + private static final BasicStatusLine HTTP_200_STATUS_LINE = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"); + private static final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + private static final Map<String, String> DEFAULT_TEST_PARAMETERS = new HashMap<String, String>(); + static { + DEFAULT_TEST_PARAMETERS.put("arg1", "val1"); + DEFAULT_TEST_PARAMETERS.put("arg2", "val2"); } - @Test(expected = CloudstackRESTException.class) - public void testExecuteLoginWithoutCredentials() throws CloudstackRESTException { - method = mock(PutMethod.class); - connector.setAdminCredentials(null, null); - connector.validation.login(RESTServiceConnector.protocol, client); - } + @Test + public void testExecuteUpdateObject() throws Exception { + final TestPojo newObject = new TestPojo(); + newObject.setField("newValue"); + final String newObjectJson = gson.toJson(newObject); + final CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getStatusLine()).thenReturn(HTTP_200_STATUS_LINE); + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class), any(HttpClientContext.class))).thenReturn(response); + final RestClient restClient = new BasicRestClient(httpClient, HttpClientContext.create(), "localhost"); + final RESTServiceConnector connector = new RESTServiceConnector.Builder().client(restClient).build(); - @Test(expected = CloudstackRESTException.class) - public void testExecuteUpdateObjectWithoutHostname() throws CloudstackRESTException { - method = mock(PutMethod.class); - connector.setControllerAddress(null); - connector.executeUpdateObject(new String(), "/", Collections.<String, String> emptyMap()); - } + connector.executeUpdateObject(newObject, "/somepath"); - @Test(expected = CloudstackRESTException.class) - public void testExecuteUpdateObjectWithoutCredentials() throws CloudstackRESTException { - method = mock(PutMethod.class); - connector.setAdminCredentials(null, null); - connector.executeUpdateObject(new String(), "/", Collections.<String, String> emptyMap()); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestMethodMatcher.aMethod("PUT"), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestPayloadMatcher.aPayload(newObjectJson), any(HttpClientContext.class)); } - @Test(expected = CloudstackRESTException.class) - public void testExecuteCreateObjectWithoutHostname() throws CloudstackRESTException { - method = mock(PostMethod.class); - connector.setControllerAddress(null); - connector.executeCreateObject(new String(), String.class, "/", Collections.<String, String> emptyMap()); - } + @Test + public void testExecuteUpdateObjectWithParameters() throws Exception { + final TestPojo newObject = new TestPojo(); + newObject.setField("newValue"); + final String newObjectJson = gson.toJson(newObject); + final CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getStatusLine()).thenReturn(HTTP_200_STATUS_LINE); + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class), any(HttpClientContext.class))).thenReturn(response); + final RestClient restClient = new BasicRestClient(httpClient, HttpClientContext.create(), "localhost"); - @Test(expected = CloudstackRESTException.class) - public void testExecuteCreateObjectWithoutCredentials() throws CloudstackRESTException { - method = mock(PostMethod.class); - connector.setAdminCredentials(null, null); - connector.executeCreateObject(new String(), String.class, "/", Collections.<String, String> emptyMap()); - } + final RESTServiceConnector connector = new RESTServiceConnector.Builder().client(restClient).build(); - @Test(expected = CloudstackRESTException.class) - public void testExecuteDeleteObjectWithoutHostname() throws CloudstackRESTException { - method = mock(DeleteMethod.class); - connector.setControllerAddress(null); - connector.executeDeleteObject("/"); - } + connector.executeUpdateObject(newObject, "/somepath", DEFAULT_TEST_PARAMETERS); - @Test(expected = CloudstackRESTException.class) - public void testExecuteDeleteObjectWithoutCredentials() throws CloudstackRESTException { - method = mock(DeleteMethod.class); - connector.setAdminCredentials(null, null); - connector.executeDeleteObject("/"); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestMethodMatcher.aMethod("PUT"), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestPayloadMatcher.aPayload(newObjectJson), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestQueryMatcher.aQueryThatContains("arg2=val2"), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestQueryMatcher.aQueryThatContains("arg1=val1"), any(HttpClientContext.class)); } - @Test(expected = CloudstackRESTException.class) - public void testExecuteRetrieveObjectWithoutHostname() throws CloudstackRESTException { - method = mock(GetMethod.class); - connector.setControllerAddress(null); - connector.executeRetrieveObject(String.class, "/", Collections.<String, String> emptyMap()); + @Test + public void testExecuteCreateObject() throws Exception { + final TestPojo newObject = new TestPojo(); + newObject.setField("newValue"); + final String newObjectJson = gson.toJson(newObject); + final CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getEntity()).thenReturn(new StringEntity(newObjectJson)); + when(response.getStatusLine()).thenReturn(HTTP_200_STATUS_LINE); + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class), any(HttpClientContext.class))).thenReturn(response); + final RestClient restClient = new BasicRestClient(httpClient, HttpClientContext.create(), "localhost"); + final RESTServiceConnector connector = new RESTServiceConnector.Builder().client(restClient).build(); + + final TestPojo object = connector.executeCreateObject(newObject, "/somepath"); + + assertThat(object, notNullValue()); + assertThat(object, equalTo(newObject)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestMethodMatcher.aMethod("POST"), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestPayloadMatcher.aPayload(newObjectJson), any(HttpClientContext.class)); + verify(response).close(); } - @Test(expected = CloudstackRESTException.class) - public void testExecuteRetrieveObjectWithoutCredentials() throws CloudstackRESTException { - method = mock(GetMethod.class); - connector.setAdminCredentials(null, null); - connector.executeRetrieveObject(String.class, "/", Collections.<String, String> emptyMap()); + @Test + public void testExecuteCreateObjectWithParameters() throws Exception { + final TestPojo newObject = new TestPojo(); + newObject.setField("newValue"); + final String newObjectJson = gson.toJson(newObject); + final CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getEntity()).thenReturn(new StringEntity(newObjectJson)); + when(response.getStatusLine()).thenReturn(HTTP_200_STATUS_LINE); + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class), any(HttpClientContext.class))).thenReturn(response); + final RestClient restClient = new BasicRestClient(httpClient, HttpClientContext.create(), "localhost"); + final RESTServiceConnector connector = new RESTServiceConnector.Builder().client(restClient).build(); + + final TestPojo object = connector.executeCreateObject(newObject, "/somepath", DEFAULT_TEST_PARAMETERS); + + assertThat(object, notNullValue()); + assertThat(object, equalTo(newObject)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestMethodMatcher.aMethod("POST"), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestPayloadMatcher.aPayload(newObjectJson), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestQueryMatcher.aQueryThatContains("arg2=val2"), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestQueryMatcher.aQueryThatContains("arg1=val1"), any(HttpClientContext.class)); + verify(response).close(); } @Test - public void testExecuteMethod() throws CloudstackRESTException { - final GetMethod gm = mock(GetMethod.class); + public void testExecuteDeleteObject() throws Exception { + final HttpEntity entity = mock(HttpEntity.class); + final CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getEntity()).thenReturn(entity); + when(response.getStatusLine()).thenReturn(HTTP_200_STATUS_LINE); + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class), any(HttpClientContext.class))).thenReturn(response); + final RestClient restClient = new BasicRestClient(httpClient, HttpClientContext.create(), "localhost"); + final RESTServiceConnector connector = new RESTServiceConnector.Builder().client(restClient).build(); - when(gm.getStatusCode()).thenReturn(HttpStatus.SC_OK); - connector.executeMethod(gm); - verify(gm, times(1)).getStatusCode(); - } + connector.executeDeleteObject("/somepath"); - @Test(expected = CloudstackRESTException.class) - public void testExecuteMethodWithLogin() throws CloudstackRESTException, HttpException, IOException { - final GetMethod gm = mock(GetMethod.class); - when(client.executeMethod((HttpMethod) any())).thenThrow(new HttpException()); - when(gm.getStatusCode()).thenReturn(HttpStatus.SC_UNAUTHORIZED).thenReturn(HttpStatus.SC_UNAUTHORIZED); - connector.executeMethod(gm); - verify(gm, times(1)).getStatusCode(); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestMethodMatcher.aMethod("DELETE"), any(HttpClientContext.class)); + verify(response).close(); } - /* - * Bit of a roundabout way to ensure that login is called after an un authorized result It not possible to properly mock login() - */ - public void testExecuteMethodWithLoginSucced2ndAttempt() throws CloudstackRESTException { - // Prepare - final GetMethod gm = mock(GetMethod.class); - when(gm.getStatusCode()).thenReturn(HttpStatus.SC_UNAUTHORIZED).thenReturn(HttpStatus.SC_UNAUTHORIZED); - - final RESTValidationStrategy previousValidationStrategy = connector.validation; - connector.validation = new RESTValidationStrategy() { - @Override - protected void login(final String protocol, final HttpClient client) throws CloudstackRESTException { - // Do nothing - } - }; - connector.setAdminCredentials("admin", "adminpass"); - connector.setControllerAddress("localhost"); - - // Execute - connector.executeMethod(gm); - // Leave mock object as is was - connector.validation = previousValidationStrategy; - - // Assert/verify - verify(gm, times(2)).getStatusCode(); + @Test + public void testExecuteRetrieveObject() throws Exception { + final TestPojo existingObject = new TestPojo(); + existingObject.setField("existingValue"); + final String newObjectJson = gson.toJson(existingObject); + final CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getEntity()).thenReturn(new StringEntity(newObjectJson)); + when(response.getStatusLine()).thenReturn(HTTP_200_STATUS_LINE); + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class), any(HttpClientContext.class))).thenReturn(response); + final RestClient restClient = new BasicRestClient(httpClient, HttpClientContext.create(), "localhost"); + final RESTServiceConnector connector = new RESTServiceConnector.Builder().client(restClient).build(); + + final TestPojo object = connector.executeRetrieveObject(TestPojo.class, "/somepath"); + + assertThat(object, notNullValue()); + assertThat(object, equalTo(existingObject)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestMethodMatcher.aMethod("GET"), any(HttpClientContext.class)); + verify(response).close(); } @Test - public void testExecuteCreateObject() throws CloudstackRESTException, IOException { - JsonEntity ls = new JsonEntity(); - method = mock(PostMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_CREATED); - when(method.getResponseBodyAsString()).thenReturn(UUID_JSON_RESPONSE); - ls = connector.executeCreateObject(ls, JsonEntity.class, "/", Collections.<String, String> emptyMap()); - assertTrue(UUID.equals(ls.getUuid())); - verify(method, times(1)).releaseConnection(); - + public void testExecuteRetrieveObjectWithParameters() throws Exception { + final TestPojo existingObject = new TestPojo(); + existingObject.setField("existingValue"); + final String newObjectJson = gson.toJson(existingObject); + final CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getEntity()).thenReturn(new StringEntity(newObjectJson)); + when(response.getStatusLine()).thenReturn(HTTP_200_STATUS_LINE); + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class), any(HttpClientContext.class))).thenReturn(response); + final RestClient restClient = new BasicRestClient(httpClient, HttpClientContext.create(), "localhost"); + final RESTServiceConnector connector = new RESTServiceConnector.Builder().client(restClient).build(); + + final TestPojo object = connector.executeRetrieveObject(TestPojo.class, "/somepath", DEFAULT_TEST_PARAMETERS); + + assertThat(object, notNullValue()); + assertThat(object, equalTo(existingObject)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestMethodMatcher.aMethod("GET"), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestQueryMatcher.aQueryThatContains("arg2=val2"), any(HttpClientContext.class)); + verify(httpClient).execute(any(HttpHost.class), HttpUriRequestQueryMatcher.aQueryThatContains("arg1=val1"), any(HttpClientContext.class)); + verify(response).close(); + } + + @Test(expected = JsonParseException.class) + public void testCustomDeserializerTypeMismatch() throws Exception { + final CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getStatusLine()).thenReturn(HTTP_200_STATUS_LINE); + when(response.getEntity()).thenReturn(new StringEntity("[{somethig_not_type : \"WrongType\"}]")); + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class), any(HttpClientContext.class))).thenReturn(response); + final RestClient restClient = new BasicRestClient(httpClient, HttpClientContext.create(), "localhost"); + final RESTServiceConnector connector = new RESTServiceConnector.Builder() + .client(restClient) + .classToDeserializerEntry(TestPojo.class, new TestPojoDeserializer()) + .build(); + + connector.executeRetrieveObject(TestPojo.class, "/somepath"); } - @Test(expected = CloudstackRESTException.class) - public void testExecuteCreateObjectFailure() throws CloudstackRESTException, IOException { - JsonEntity ls = new JsonEntity(); - method = mock(PostMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); - final Header header = mock(Header.class); - when(header.getValue()).thenReturn("text/html"); - when(method.getResponseHeader("Content-Type")).thenReturn(header); - when(method.getResponseBodyAsString()).thenReturn("Off to timbuktu, won't be back later."); - when(method.isRequestSent()).thenReturn(true); - try { - ls = connector.executeCreateObject(ls, JsonEntity.class, "/", Collections.<String, String> emptyMap()); - } finally { - verify(method, times(1)).releaseConnection(); + @Test + public void testCustomDeserializerForCustomLists() throws Exception { + final CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getStatusLine()).thenReturn(HTTP_200_STATUS_LINE); + when(response.getEntity()).thenReturn(new StringEntity("{results: [{field : \"SomeValue\"}], results_count: 1}")); + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class), any(HttpClientContext.class))).thenReturn(response); + final RestClient restClient = new BasicRestClient(httpClient, HttpClientContext.create(), "localhost"); + final Class<? extends CollectionType> clazzListOfTestPojo = new ObjectMapper().getTypeFactory().constructCollectionType(List.class, TestPojo.class).getClass(); + final RESTServiceConnector connector = new RESTServiceConnector.Builder() + .client(restClient) + .classToDeserializerEntry(clazzListOfTestPojo, new CustomListDeserializer<TestPojoDeserializer>()) + .build(); + + connector.executeRetrieveObject(TestPojo.class, "/somepath"); + } + + class NiciraList<T> { + private List<T> results; + private int resultCount; + + public List<T> getResults() { + return results; } - } - @Test(expected = CloudstackRESTException.class) - public void testExecuteCreateObjectException() throws CloudstackRESTException, IOException { - JsonEntity ls = new JsonEntity(); - when(client.executeMethod((HttpMethod) any())).thenThrow(new HttpException()); - method = mock(PostMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); - final Header header = mock(Header.class); - when(header.getValue()).thenReturn("text/html"); - when(method.getResponseHeader("Content-Type")).thenReturn(header); - when(method.getResponseBodyAsString()).thenReturn("Off to timbuktu, won't be back later."); - try { - ls = connector.executeCreateObject(ls, JsonEntity.class, "/", Collections.<String, String> emptyMap()); - } finally { - verify(method, times(1)).releaseConnection(); + public void setResults(final List<T> results) { + this.results = results; } - } - - @Test - public void testExecuteUpdateObject() throws CloudstackRESTException, IOException { - final JsonEntity ls = new JsonEntity(); - method = mock(PutMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_OK); - connector.executeUpdateObject(ls, "/", Collections.<String, String> emptyMap()); - verify(method, times(1)).releaseConnection(); - verify(client, times(1)).executeMethod(method); - } - @Test(expected = CloudstackRESTException.class) - public void testExecuteUpdateObjectFailure() throws CloudstackRESTException, IOException { - final JsonEntity ls = new JsonEntity(); - method = mock(PutMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); - final Header header = mock(Header.class); - when(header.getValue()).thenReturn("text/html"); - when(method.getResponseHeader("Content-Type")).thenReturn(header); - when(method.getResponseBodyAsString()).thenReturn("Off to timbuktu, won't be back later."); - when(method.isRequestSent()).thenReturn(true); - try { - connector.executeUpdateObject(ls, "/", Collections.<String, String> emptyMap()); - } finally { - verify(method, times(1)).releaseConnection(); + public int getResultCount() { + return resultCount; } - } - @Test(expected = CloudstackRESTException.class) - public void testExecuteUpdateObjectException() throws CloudstackRESTException, IOException { - final JsonEntity ls = new JsonEntity(); - method = mock(PutMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_OK); - when(client.executeMethod((HttpMethod) any())).thenThrow(new IOException()); - try { - connector.executeUpdateObject(ls, "/", Collections.<String, String> emptyMap()); - } finally { - verify(method, times(1)).releaseConnection(); + public void setResultCount(final int resultCount) { + this.resultCount = resultCount; } - } - @Test - public void testExecuteDeleteObject() throws CloudstackRESTException, IOException { - method = mock(DeleteMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_NO_CONTENT); - connector.executeDeleteObject("/"); - verify(method, times(1)).releaseConnection(); - verify(client, times(1)).executeMethod(method); } - @Test(expected = CloudstackRESTException.class) - public void testExecuteDeleteObjectFailure() throws CloudstackRESTException, IOException { - method = mock(DeleteMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); - final Header header = mock(Header.class); - when(header.getValue()).thenReturn("text/html"); - when(method.getResponseHeader("Content-Type")).thenReturn(header); - when(method.getResponseBodyAsString()).thenReturn("Off to timbuktu, won't be back later."); - when(method.isRequestSent()).thenReturn(true); - try { - connector.executeDeleteObject("/"); - } finally { - verify(method, times(1)).releaseConnection(); - } - } + class TestPojo { + private String field; - @Test(expected = CloudstackRESTException.class) - public void testExecuteDeleteObjectException() throws CloudstackRESTException, IOException { - method = mock(DeleteMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_NO_CONTENT); - when(client.executeMethod((HttpMethod) any())).thenThrow(new HttpException()); - try { - connector.executeDeleteObject("/"); - } finally { - verify(method, times(1)).releaseConnection(); + public String getField() { + return field; } - } - - @Test - public void testExecuteRetrieveObject() throws CloudstackRESTException, IOException { - method = mock(GetMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_OK); - when(method.getResponseBodyAsString()).thenReturn(UUID_JSON_RESPONSE); - connector.executeRetrieveObject(JsonEntity.class, "/", Collections.<String, String> emptyMap()); - verify(method, times(1)).releaseConnection(); - verify(client, times(1)).executeMethod(method); - } - @Test(expected = CloudstackRESTException.class) - public void testExecuteRetrieveObjectFailure() throws CloudstackRESTException, IOException { - method = mock(GetMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_INTERNAL_SERVER_ERROR); - when(method.getResponseBodyAsString()).thenReturn(UUID_JSON_RESPONSE); - final Header header = mock(Header.class); - when(header.getValue()).thenReturn("text/html"); - when(method.getResponseHeader("Content-Type")).thenReturn(header); - when(method.getResponseBodyAsString()).thenReturn("Off to timbuktu, won't be back later."); - when(method.isRequestSent()).thenReturn(true); - try { - connector.executeRetrieveObject(JsonEntity.class, "/", Collections.<String, String> emptyMap()); - } finally { - verify(method, times(1)).releaseConnection(); + public void setField(final String field) { + this.field = field; } - } - @Test(expected = CloudstackRESTException.class) - public void testExecuteRetrieveObjectException() throws CloudstackRESTException, IOException { - method = mock(GetMethod.class); - when(method.getStatusCode()).thenReturn(HttpStatus.SC_OK); - when(method.getResponseBodyAsString()).thenReturn(UUID_JSON_RESPONSE); - when(client.executeMethod((HttpMethod) any())).thenThrow(new HttpException()); - try { - connector.executeRetrieveObject(JsonEntity.class, "/", Collections.<String, String> emptyMap()); - } finally { - verify(method, times(1)).releaseConnection(); + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); } - } -} - -class JsonEntity { - private String displayName; - private String uuid; - private String href; - private String schema; + @Override + public boolean equals(final Object obj) { + return EqualsBuilder.reflectionEquals(this, obj); + } - public String getDisplayName() { - return displayName; } - public void setDisplayName(final String displayName) { - this.displayName = displayName; - } + private final class TestPojoDeserializer implements JsonDeserializer<TestPojo> { + @Override + public TestPojo deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { + final JsonObject jsonObject = json.getAsJsonObject(); - public String getUuid() { - return uuid; - } + if (!jsonObject.has("type")) { + throw new JsonParseException("Deserializing as a TestPojo, but no type present in the json object"); + } - public void setUuid(final String uuid) { - this.uuid = uuid; + return context.deserialize(jsonObject, TestPojo.class); + } } - public String getHref() { - return href; - } + private final class CustomListDeserializer<T> implements JsonDeserializer<T> { + private final Gson standardGson = new GsonBuilder().create(); - public void setHref(final String href) { - this.href = href; - } + @Override + public T deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { + final JsonObject jsonObject = json.getAsJsonObject(); - public String getSchema() { - return schema; - } + System.err.println(json.toString()); - public void setSchema(final String schema) { - this.schema = schema; + if (jsonObject.has("results")) { + final JsonArray results = jsonObject.getAsJsonArray("results"); + return context.deserialize(results, typeOfT); + } else { + return standardGson.fromJson(jsonObject, typeOfT); + } + } } -} \ No newline at end of file +}