/**
* 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 net.elasticgrid.rackspace.common;
import net.elasticgrid.rackspace.cloudservers.internal.CloudServersAPIFault;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.entity.EntityTemplate;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HttpContext;
import org.jibx.runtime.BindingDirectory;
import org.jibx.runtime.IBindingFactory;
import org.jibx.runtime.IUnmarshallingContext;
import org.jibx.runtime.JiBXException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
/**
* This class provides common code to the REST connection classes.
* Logging:
* <table>
* <thead>
* <th>Level</th>
* <th align="left">Information</th>
* </thead>
* <tbody>
* <tr>
* <td>WARNING</td>
* <td>Retries, Expired Authentication before the request is automatically retried</td>
* </tr>
* <tr>
* <td>INFO</td>
* <td>Request URI & Method</td>
* </tr>
* <tr>
* <td>FINEST</td>
* <td>Request Body, Response Body</td>
* </tr>
* </tbody>
* </table>
*
* @author Jerome Bernard
*/
public class RackspaceConnection {
// this is the number of automatic retries
private int maxRetries = 5;
private String userAgent = "Elastic-Grid/";
private HttpClient hc = null;
private int maxConnections = 100;
private String proxyHost = null;
private int proxyPort;
private int connectionManagerTimeout = 0;
private int soTimeout = 0;
private int connectionTimeout = 0;
private final String username;
private final String apiKey;
private String serverManagementURL;
private String storageURL;
private String cdnManagementURL;
private String authToken;
private boolean authenticated = false;
private static final String API_AUTH_URL = "https://auth.api.rackspacecloud.com/v1.0";
private static final Logger logger = Logger.getLogger(RackspaceConnection.class.getName());
/**
* Initializes the Rackspace connection with the Rackspace login information.
*
* @param username the Rackspace username
* @param apiKey the Rackspace API key
* @throws RackspaceException if the credentials are invalid
* @throws IOException if there is a network issue
* @see #authenticate()
*/
public RackspaceConnection(String username, String apiKey) throws RackspaceException, IOException {
this.username = username;
this.apiKey = apiKey;
String version;
try {
Properties props = new Properties();
props.load(this.getClass().getClassLoader().getResourceAsStream("version.properties"));
version = props.getProperty("version");
} catch (Exception ex) {
version = "?";
}
userAgent = userAgent + version + " (" + System.getProperty("os.arch") + "; " + System.getProperty("os.name") + ")";
authenticate();
}
/**
* Authenticate on Rackspace API. Tokens are only valid for 24 hours, so client code should expect token to expire
* and renew them if needed.
*
* @return the auth token, valid for 24 hours
* @throws RackspaceException if the credentials are invalid
* @throws IOException if there is a network issue
*/
public String authenticate() throws RackspaceException, IOException {
logger.info("Authenticating to Rackspace API...");
HttpGet request = new HttpGet(API_AUTH_URL);
request.addHeader("X-Auth-User", username);
request.addHeader("X-Auth-Key", apiKey);
HttpResponse response = getHttpClient().execute(request);
int statusCode = response.getStatusLine().getStatusCode();
switch (statusCode) {
case 204:
if (response.getFirstHeader("X-Server-Management-Url") != null)
serverManagementURL = response.getFirstHeader("X-Server-Management-Url").getValue();
if (response.getFirstHeader("X-Storage-Url") != null)
storageURL = response.getFirstHeader("X-Storage-Url").getValue();
if (response.getFirstHeader("X-CDN-Management-Url") != null)
cdnManagementURL = response.getFirstHeader("X-CDN-Management-Url").getValue();
authToken = response.getFirstHeader("X-Auth-Token").getValue();
authenticated = true;
return authToken;
case 401:
throw new RackspaceException("Invalid credentials: " + response.getStatusLine().getReasonPhrase());
default:
throw new RackspaceException("Unexpected HTTP response");
}
}
/**
* Make a http request and process the response. This method also performs automatic retries.
*
* @param request the HTTP method to use (GET, POST, DELETE, etc)
* @param respType the class that represents the desired/expected return type
* @return the unmarshalled entity
* @throws RackspaceException
* @throws IOException if there is an I/O exception
* @throws HttpException if there is an HTTP exception
* @throws JiBXException if the result can't be unmarshalled
*/
@SuppressWarnings("unchecked")
protected <T> T makeRequest(HttpRequestBase request, Class<T> respType)
throws HttpException, IOException, JiBXException, RackspaceException {
if (!authenticated)
authenticate();
// add auth params, and protocol specific headers
request.addHeader("X-Auth-Token", getAuthToken());
// set accept and content-type headers
request.setHeader("Accept", "application/xml; charset=UTF-8");
request.setHeader("Accept-Encoding", "gzip");
request.setHeader("Content-Type", "application/xml; charset=UTF-8");
// send the request
T result = null;
boolean done = false;
int retries = 0;
boolean doRetry = false;
RackspaceException error = null;
do {
HttpResponse response = null;
if (retries > 0)
logger.log(Level.INFO, "Retry #{0}: querying via {1} {2}",
new Object[]{retries, request.getMethod(), request.getURI()});
else
logger.log(Level.INFO, "Querying via {0} {1}", new Object[]{request.getMethod(), request.getURI()});
if (logger.isLoggable(Level.FINEST) && request instanceof HttpEntityEnclosingRequestBase) {
HttpEntity entity = ((HttpEntityEnclosingRequestBase) request).getEntity();
if (entity instanceof EntityTemplate) {
EntityTemplate template = (EntityTemplate) entity;
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
template.writeTo(baos);
logger.log(Level.FINEST, "Request body:\n{0}", baos.toString());
} finally {
IOUtils.closeQuietly(baos);
}
}
}
InputStream entityStream = null;
HttpEntity entity = null;
if (logger.isLoggable(Level.FINEST)) {
response = getHttpClient().execute(request);
entity = response.getEntity();
try {
entityStream = entity.getContent();
logger.log(Level.FINEST, "Response body on " + request.getURI()
+ " via " + request.getMethod() + ":\n" + IOUtils.toString(entityStream));
} finally {
IOUtils.closeQuietly(entityStream);
}
}
response = getHttpClient().execute(request);
int statusCode = response.getStatusLine().getStatusCode();
entity = response.getEntity();
switch (statusCode) {
case 200:
case 202:
case 203:
try {
entityStream = entity.getContent();
IBindingFactory bindingFactory = BindingDirectory.getFactory(respType);
IUnmarshallingContext unmarshallingCxt = bindingFactory.createUnmarshallingContext();
result = (T) unmarshallingCxt.unmarshalDocument(entityStream, "UTF-8");
} finally {
entity.consumeContent();
IOUtils.closeQuietly(entityStream);
}
done = true;
break;
case 503: // service unavailable
logger.log(Level.WARNING, "Service unavailable on {0} via {1}. Will retry in {2} seconds.",
new Object[]{request.getURI(), request.getMethod(), Math.pow(2.0, retries + 1)});
doRetry = true;
break;
case 401: // unauthorized
logger.warning("Not authenticated or authentication token expired. Authenticating...");
authenticate();
doRetry = true;
break;
case 417:
throw new RackspaceException(new IllegalArgumentException("Some parameters are invalid!")); // TODO: temp hack 'til Rackspace API is fixed!
case 400:
case 500:
default:
try {
entityStream = entity.getContent();
IBindingFactory bindingFactory = BindingDirectory.getFactory(CloudServersAPIFault.class);
IUnmarshallingContext unmarshallingCxt = bindingFactory.createUnmarshallingContext();
CloudServersAPIFault fault = (CloudServersAPIFault) unmarshallingCxt.unmarshalDocument(entityStream, "UTF-8");
done = true;
throw new RackspaceException(fault.getCode(), fault.getMessage(), fault.getDetails());
} catch (JiBXException e) {
response = getHttpClient().execute(request);
entity = response.getEntity();
entityStream = entity.getContent();
logger.log(Level.SEVERE, "Can't unmarshal response from " + request.getURI()
+ " via " + request.getMethod() + ":" + IOUtils.toString(entityStream));
e.printStackTrace();
throw e;
} finally {
entity.consumeContent();
IOUtils.closeQuietly(entityStream);
}
}
if (doRetry) {
retries++;
if (retries > maxRetries) {
throw new HttpException("Number of retries exceeded for " + request.getURI(), error);
}
doRetry = false;
try {
Thread.sleep((int) Math.pow(2.0, retries) * 1000);
} catch (InterruptedException ex) {
// do nothing
}
}
} while (!done);
return result;
}
private void configureHttpClient() {
HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, "UTF-8");
HttpProtocolParams.setUserAgent(params, userAgent);
HttpProtocolParams.setUseExpectContinue(params, true);
// params.setBooleanParameter("http.tcp.nodelay", true);
// params.setBooleanParameter("http.coonection.stalecheck", false);
ConnManagerParams.setTimeout(params, getConnectionManagerTimeout());
ConnManagerParams.setMaxTotalConnections(params, getMaxConnections());
ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(getMaxConnections()));
params.setIntParameter("http.socket.timeout", getSoTimeout());
params.setIntParameter("http.connection.timeout", getConnectionTimeout());
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schemeRegistry);
hc = new DefaultHttpClient(connMgr, params);
((DefaultHttpClient) hc).addRequestInterceptor(new HttpRequestInterceptor() {
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
if (!request.containsHeader("Accept-Encoding")) {
request.addHeader("Accept-Encoding", "gzip");
}
}
});
((DefaultHttpClient) hc).addResponseInterceptor(new HttpResponseInterceptor() {
public void process(HttpResponse response, HttpContext context) throws HttpException, IOException {
HttpEntity entity = response.getEntity();
if (entity == null)
return;
Header ceHeader = entity.getContentEncoding();
if (ceHeader != null) {
for (HeaderElement codec : ceHeader.getElements()) {
if (codec.getName().equalsIgnoreCase("gzip")) {
response.setEntity(new GzipDecompressingEntity(response.getEntity()));
return;
}
}
}
}
});
if (proxyHost != null) {
HttpHost proxy = new HttpHost(proxyHost, proxyPort);
hc.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
logger.info("Proxy Host set to " + proxyHost + ":" + proxyPort);
}
}
public String getAuthToken() {
return authToken;
}
public String getServerManagementURL() {
return serverManagementURL;
}
public String getStorageURL() {
return storageURL;
}
public String getCdnManagementURL() {
return cdnManagementURL;
}
protected HttpClient getHttpClient() {
if (hc == null) {
configureHttpClient();
}
return hc;
}
public void setHttpClient(HttpClient hc) {
this.hc = hc;
}
public int getConnectionManagerTimeout() {
return connectionManagerTimeout;
}
public void setConnectionManagerTimeout(int timeout) {
connectionManagerTimeout = timeout;
hc = null;
}
public int getSoTimeout() {
return soTimeout;
}
public void setSoTimeout(int timeout) {
soTimeout = timeout;
hc = null;
}
public int getConnectionTimeout() {
return connectionTimeout;
}
public void setConnectionTimeout(int timeout) {
connectionTimeout = timeout;
hc = null;
}
public int getMaxConnections() {
return maxConnections;
}
public void setMaxConnections(int maxConnections) {
this.maxConnections = maxConnections;
hc = null;
}
public String getProxyHost() {
return proxyHost;
}
public void setProxyHost(String proxyHost) {
this.proxyHost = proxyHost;
hc = null;
}
public int getProxyPort() {
return proxyPort;
}
public void setProxyPort(int proxyPort) {
this.proxyPort = proxyPort;
hc = null;
}
static class GzipDecompressingEntity extends HttpEntityWrapper {
public GzipDecompressingEntity(final HttpEntity entity) {
super(entity);
}
@Override
public InputStream getContent() throws IOException, IllegalStateException {
// the wrapped entity's getContent() decides about repeatability
InputStream wrappedin = wrappedEntity.getContent();
return new GZIPInputStream(wrappedin);
}
@Override
public long getContentLength() {
// length of ungzipped content is not known
return -1;
}
}
}