Revision f8be9cce main/java/net/elasticgrid/rackspace/common/RackspaceConnection.java
b/main/java/net/elasticgrid/rackspace/common/RackspaceConnection.java | ||
---|---|---|
1 |
/** |
|
2 |
* Licensed to the Apache Software Foundation (ASF) under one |
|
3 |
* or more contributor license agreements. See the NOTICE file |
|
4 |
* distributed with this work for additional information |
|
5 |
* regarding copyright ownership. The ASF licenses this file |
|
6 |
* to you under the Apache License, Version 2.0 (the |
|
7 |
* "License"); you may not use this file except in compliance |
|
8 |
* with the License. You may obtain a copy of the License at |
|
9 |
* |
|
10 |
* http://www.apache.org/licenses/LICENSE-2.0 |
|
11 |
* |
|
12 |
* Unless required by applicable law or agreed to in writing, |
|
13 |
* software distributed under the License is distributed on an |
|
14 |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
|
15 |
* KIND, either express or implied. See the License for the |
|
16 |
* specific language governing permissions and limitations |
|
17 |
* under the License. |
|
18 |
*/ |
|
19 |
package net.elasticgrid.rackspace.common; |
|
20 |
|
|
21 |
import net.elasticgrid.rackspace.cloudservers.internal.CloudServersAPIFault; |
|
22 |
import org.apache.commons.io.IOUtils; |
|
23 |
import org.apache.http.Header; |
|
24 |
import org.apache.http.HeaderElement; |
|
25 |
import org.apache.http.HttpEntity; |
|
26 |
import org.apache.http.HttpException; |
|
27 |
import org.apache.http.HttpHost; |
|
28 |
import org.apache.http.HttpRequest; |
|
29 |
import org.apache.http.HttpRequestInterceptor; |
|
30 |
import org.apache.http.HttpResponse; |
|
31 |
import org.apache.http.HttpResponseInterceptor; |
|
32 |
import org.apache.http.HttpVersion; |
|
33 |
import org.apache.http.client.HttpClient; |
|
34 |
import org.apache.http.client.methods.HttpGet; |
|
35 |
import org.apache.http.client.methods.HttpRequestBase; |
|
36 |
import org.apache.http.client.methods.HttpPost; |
|
37 |
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; |
|
38 |
import org.apache.http.conn.ClientConnectionManager; |
|
39 |
import org.apache.http.conn.params.ConnManagerParams; |
|
40 |
import org.apache.http.conn.params.ConnRoutePNames; |
|
41 |
import org.apache.http.conn.params.ConnPerRouteBean; |
|
42 |
import org.apache.http.conn.scheme.PlainSocketFactory; |
|
43 |
import org.apache.http.conn.scheme.Scheme; |
|
44 |
import org.apache.http.conn.scheme.SchemeRegistry; |
|
45 |
import org.apache.http.conn.ssl.SSLSocketFactory; |
|
46 |
import org.apache.http.entity.HttpEntityWrapper; |
|
47 |
import org.apache.http.entity.EntityTemplate; |
|
48 |
import org.apache.http.impl.client.DefaultHttpClient; |
|
49 |
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; |
|
50 |
import org.apache.http.params.BasicHttpParams; |
|
51 |
import org.apache.http.params.HttpParams; |
|
52 |
import org.apache.http.params.HttpProtocolParams; |
|
53 |
import org.apache.http.protocol.HttpContext; |
|
54 |
import org.jibx.runtime.BindingDirectory; |
|
55 |
import org.jibx.runtime.IBindingFactory; |
|
56 |
import org.jibx.runtime.IUnmarshallingContext; |
|
57 |
import org.jibx.runtime.JiBXException; |
|
58 |
import java.io.IOException; |
|
59 |
import java.io.InputStream; |
|
60 |
import java.io.ByteArrayOutputStream; |
|
61 |
import java.util.Properties; |
|
62 |
import java.util.logging.Level; |
|
63 |
import java.util.logging.Logger; |
|
64 |
import java.util.zip.GZIPInputStream; |
|
65 |
|
|
66 |
/** |
|
67 |
* This class provides common code to the REST connection classes. |
|
68 |
* Logging: |
|
69 |
* <table> |
|
70 |
* <thead> |
|
71 |
* <th>Level</th> |
|
72 |
* <th align="left">Information</th> |
|
73 |
* </thead> |
|
74 |
* <tbody> |
|
75 |
* <tr> |
|
76 |
* <td>WARNING</td> |
|
77 |
* <td>Retries, Expired Authentication before the request is automatically retried</td> |
|
78 |
* </tr> |
|
79 |
* <tr> |
|
80 |
* <td>INFO</td> |
|
81 |
* <td>Request URI & Method</td> |
|
82 |
* </tr> |
|
83 |
* <tr> |
|
84 |
* <td>FINEST</td> |
|
85 |
* <td>Request Body, Response Body</td> |
|
86 |
* </tr> |
|
87 |
* </tbody> |
|
88 |
* </table> |
|
89 |
* |
|
90 |
* @author Jerome Bernard |
|
91 |
*/ |
|
92 |
public class RackspaceConnection { |
|
93 |
// this is the number of automatic retries |
|
94 |
private int maxRetries = 5; |
|
95 |
private String userAgent = "Elastic-Grid/"; |
|
96 |
private HttpClient hc = null; |
|
97 |
private int maxConnections = 100; |
|
98 |
private String proxyHost = null; |
|
99 |
private int proxyPort; |
|
100 |
private int connectionManagerTimeout = 0; |
|
101 |
private int soTimeout = 0; |
|
102 |
private int connectionTimeout = 0; |
|
103 |
|
|
104 |
private final String username; |
|
105 |
private final String apiKey; |
|
106 |
private String serverManagementURL; |
|
107 |
private String storageURL; |
|
108 |
private String cdnManagementURL; |
|
109 |
private String authToken; |
|
110 |
|
|
111 |
private boolean authenticated = false; |
|
112 |
|
|
113 |
private static final String API_AUTH_URL = "https://auth.api.rackspacecloud.com/v1.0"; |
|
114 |
|
|
115 |
private static final Logger logger = Logger.getLogger(RackspaceConnection.class.getName()); |
|
116 |
|
|
117 |
/** |
|
118 |
* Initializes the Rackspace connection with the Rackspace login information. |
|
119 |
* |
|
120 |
* @param username the Rackspace username |
|
121 |
* @param apiKey the Rackspace API key |
|
122 |
* @throws RackspaceException if the credentials are invalid |
|
123 |
* @throws IOException if there is a network issue |
|
124 |
* @see #authenticate() |
|
125 |
*/ |
|
126 |
public RackspaceConnection(String username, String apiKey) throws RackspaceException, IOException { |
|
127 |
this.username = username; |
|
128 |
this.apiKey = apiKey; |
|
129 |
String version; |
|
130 |
try { |
|
131 |
Properties props = new Properties(); |
|
132 |
props.load(this.getClass().getClassLoader().getResourceAsStream("version.properties")); |
|
133 |
version = props.getProperty("version"); |
|
134 |
} catch (Exception ex) { |
|
135 |
version = "?"; |
|
136 |
} |
|
137 |
userAgent = userAgent + version + " (" + System.getProperty("os.arch") + "; " + System.getProperty("os.name") + ")"; |
|
138 |
authenticate(); |
|
139 |
} |
|
140 |
|
|
141 |
/** |
|
142 |
* Authenticate on Rackspace API. Tokens are only valid for 24 hours, so client code should expect token to expire |
|
143 |
* and renew them if needed. |
|
144 |
* |
|
145 |
* @return the auth token, valid for 24 hours |
|
146 |
* @throws RackspaceException if the credentials are invalid |
|
147 |
* @throws IOException if there is a network issue |
|
148 |
*/ |
|
149 |
public String authenticate() throws RackspaceException, IOException { |
|
150 |
logger.info("Authenticating to Rackspace API..."); |
|
151 |
HttpGet request = new HttpGet(API_AUTH_URL); |
|
152 |
request.addHeader("X-Auth-User", username); |
|
153 |
request.addHeader("X-Auth-Key", apiKey); |
|
154 |
HttpResponse response = getHttpClient().execute(request); |
|
155 |
int statusCode = response.getStatusLine().getStatusCode(); |
|
156 |
switch (statusCode) { |
|
157 |
case 204: |
|
158 |
if (response.getFirstHeader("X-Server-Management-Url") != null) |
|
159 |
serverManagementURL = response.getFirstHeader("X-Server-Management-Url").getValue(); |
|
160 |
if (response.getFirstHeader("X-Storage-Url") != null) |
|
161 |
storageURL = response.getFirstHeader("X-Storage-Url").getValue(); |
|
162 |
if (response.getFirstHeader("X-CDN-Management-Url") != null) |
|
163 |
cdnManagementURL = response.getFirstHeader("X-CDN-Management-Url").getValue(); |
|
164 |
authToken = response.getFirstHeader("X-Auth-Token").getValue(); |
|
165 |
authenticated = true; |
|
166 |
return authToken; |
|
167 |
case 401: |
|
168 |
throw new RackspaceException("Invalid credentials: " + response.getStatusLine().getReasonPhrase()); |
|
169 |
default: |
|
170 |
throw new RackspaceException("Unexpected HTTP response"); |
|
171 |
} |
|
172 |
} |
|
173 |
|
|
174 |
/** |
|
175 |
* Make a http request and process the response. This method also performs automatic retries. |
|
176 |
* |
|
177 |
* @param request the HTTP method to use (GET, POST, DELETE, etc) |
|
178 |
* @param respType the class that represents the desired/expected return type |
|
179 |
* @return the unmarshalled entity |
|
180 |
* @throws RackspaceException |
|
181 |
* @throws IOException if there is an I/O exception |
|
182 |
* @throws HttpException if there is an HTTP exception |
|
183 |
* @throws JiBXException if the result can't be unmarshalled |
|
184 |
*/ |
|
185 |
@SuppressWarnings("unchecked") |
|
186 |
protected <T> T makeRequest(HttpRequestBase request, Class<T> respType) |
|
187 |
throws HttpException, IOException, JiBXException, RackspaceException { |
|
188 |
|
|
189 |
if (!authenticated) |
|
190 |
authenticate(); |
|
191 |
|
|
192 |
// add auth params, and protocol specific headers |
|
193 |
request.addHeader("X-Auth-Token", getAuthToken()); |
|
194 |
|
|
195 |
// set accept and content-type headers |
|
196 |
request.setHeader("Accept", "application/xml; charset=UTF-8"); |
|
197 |
request.setHeader("Accept-Encoding", "gzip"); |
|
198 |
request.setHeader("Content-Type", "application/xml; charset=UTF-8"); |
|
199 |
|
|
200 |
// send the request |
|
201 |
T result = null; |
|
202 |
boolean done = false; |
|
203 |
int retries = 0; |
|
204 |
boolean doRetry = false; |
|
205 |
RackspaceException error = null; |
|
206 |
do { |
|
207 |
HttpResponse response = null; |
|
208 |
if (retries > 0) |
|
209 |
logger.log(Level.INFO, "Retry #{0}: querying via {1} {2}", |
|
210 |
new Object[]{retries, request.getMethod(), request.getURI()}); |
|
211 |
else |
|
212 |
logger.log(Level.INFO, "Querying via {0} {1}", new Object[]{request.getMethod(), request.getURI()}); |
|
213 |
|
|
214 |
if (logger.isLoggable(Level.FINEST) && request instanceof HttpEntityEnclosingRequestBase) { |
|
215 |
HttpEntity entity = ((HttpEntityEnclosingRequestBase) request).getEntity(); |
|
216 |
if (entity instanceof EntityTemplate) { |
|
217 |
EntityTemplate template = (EntityTemplate) entity; |
|
218 |
ByteArrayOutputStream baos = null; |
|
219 |
try { |
|
220 |
baos = new ByteArrayOutputStream(); |
|
221 |
template.writeTo(baos); |
|
222 |
logger.log(Level.FINEST, "Request body:\n{0}", baos.toString()); |
|
223 |
} finally { |
|
224 |
IOUtils.closeQuietly(baos); |
|
225 |
} |
|
226 |
} |
|
227 |
} |
|
228 |
|
|
229 |
InputStream entityStream = null; |
|
230 |
HttpEntity entity = null; |
|
231 |
|
|
232 |
if (logger.isLoggable(Level.FINEST)) { |
|
233 |
response = getHttpClient().execute(request); |
|
234 |
entity = response.getEntity(); |
|
235 |
try { |
|
236 |
entityStream = entity.getContent(); |
|
237 |
logger.log(Level.FINEST, "Response body on " + request.getURI() |
|
238 |
+ " via " + request.getMethod() + ":\n" + IOUtils.toString(entityStream)); |
|
239 |
} finally { |
|
240 |
IOUtils.closeQuietly(entityStream); |
|
241 |
} |
|
242 |
} |
|
243 |
|
|
244 |
response = getHttpClient().execute(request); |
|
245 |
int statusCode = response.getStatusLine().getStatusCode(); |
|
246 |
entity = response.getEntity(); |
|
247 |
|
|
248 |
switch (statusCode) { |
|
249 |
case 200: |
|
250 |
case 202: |
|
251 |
case 203: |
|
252 |
try { |
|
253 |
entityStream = entity.getContent(); |
|
254 |
IBindingFactory bindingFactory = BindingDirectory.getFactory(respType); |
|
255 |
IUnmarshallingContext unmarshallingCxt = bindingFactory.createUnmarshallingContext(); |
|
256 |
result = (T) unmarshallingCxt.unmarshalDocument(entityStream, "UTF-8"); |
|
257 |
} finally { |
|
258 |
entity.consumeContent(); |
|
259 |
IOUtils.closeQuietly(entityStream); |
|
260 |
} |
|
261 |
done = true; |
|
262 |
break; |
|
263 |
case 503: // service unavailable |
|
264 |
logger.log(Level.WARNING, "Service unavailable on {0} via {1}. Will retry in {2} seconds.", |
|
265 |
new Object[]{request.getURI(), request.getMethod(), Math.pow(2.0, retries + 1)}); |
|
266 |
doRetry = true; |
|
267 |
break; |
|
268 |
case 401: // unauthorized |
|
269 |
logger.warning("Not authenticated or authentication token expired. Authenticating..."); |
|
270 |
authenticate(); |
|
271 |
doRetry = true; |
|
272 |
break; |
|
273 |
case 417: |
|
274 |
throw new RackspaceException(new IllegalArgumentException("Some parameters are invalid!")); // TODO: temp hack 'til Rackspace API is fixed! |
|
275 |
case 400: |
|
276 |
case 500: |
|
277 |
default: |
|
278 |
try { |
|
279 |
entityStream = entity.getContent(); |
|
280 |
IBindingFactory bindingFactory = BindingDirectory.getFactory(CloudServersAPIFault.class); |
|
281 |
IUnmarshallingContext unmarshallingCxt = bindingFactory.createUnmarshallingContext(); |
|
282 |
CloudServersAPIFault fault = (CloudServersAPIFault) unmarshallingCxt.unmarshalDocument(entityStream, "UTF-8"); |
|
283 |
done = true; |
|
284 |
throw new RackspaceException(fault.getCode(), fault.getMessage(), fault.getDetails()); |
|
285 |
} catch (JiBXException e) { |
|
286 |
response = getHttpClient().execute(request); |
|
287 |
entity = response.getEntity(); |
|
288 |
entityStream = entity.getContent(); |
|
289 |
logger.log(Level.SEVERE, "Can't unmarshal response from " + request.getURI() |
|
290 |
+ " via " + request.getMethod() + ":" + IOUtils.toString(entityStream)); |
|
291 |
e.printStackTrace(); |
|
292 |
throw e; |
|
293 |
} finally { |
|
294 |
entity.consumeContent(); |
|
295 |
IOUtils.closeQuietly(entityStream); |
|
296 |
} |
|
297 |
} |
|
298 |
|
|
299 |
if (doRetry) { |
|
300 |
retries++; |
|
301 |
if (retries > maxRetries) { |
|
302 |
throw new HttpException("Number of retries exceeded for " + request.getURI(), error); |
|
303 |
} |
|
304 |
doRetry = false; |
|
305 |
try { |
|
306 |
Thread.sleep((int) Math.pow(2.0, retries) * 1000); |
|
307 |
} catch (InterruptedException ex) { |
|
308 |
// do nothing |
|
309 |
} |
|
310 |
} |
|
311 |
} while (!done); |
|
312 |
|
|
313 |
return result; |
|
314 |
} |
|
315 |
|
|
316 |
private void configureHttpClient() { |
|
317 |
HttpParams params = new BasicHttpParams(); |
|
318 |
|
|
319 |
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); |
|
320 |
HttpProtocolParams.setContentCharset(params, "UTF-8"); |
|
321 |
HttpProtocolParams.setUserAgent(params, userAgent); |
|
322 |
HttpProtocolParams.setUseExpectContinue(params, true); |
|
323 |
|
|
324 |
// params.setBooleanParameter("http.tcp.nodelay", true); |
|
325 |
// params.setBooleanParameter("http.coonection.stalecheck", false); |
|
326 |
ConnManagerParams.setTimeout(params, getConnectionManagerTimeout()); |
|
327 |
ConnManagerParams.setMaxTotalConnections(params, getMaxConnections()); |
|
328 |
ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(getMaxConnections())); |
|
329 |
params.setIntParameter("http.socket.timeout", getSoTimeout()); |
|
330 |
params.setIntParameter("http.connection.timeout", getConnectionTimeout()); |
|
331 |
|
|
332 |
SchemeRegistry schemeRegistry = new SchemeRegistry(); |
|
333 |
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); |
|
334 |
schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443)); |
|
335 |
ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schemeRegistry); |
|
336 |
hc = new DefaultHttpClient(connMgr, params); |
|
337 |
|
|
338 |
((DefaultHttpClient) hc).addRequestInterceptor(new HttpRequestInterceptor() { |
|
339 |
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException { |
|
340 |
if (!request.containsHeader("Accept-Encoding")) { |
|
341 |
request.addHeader("Accept-Encoding", "gzip"); |
|
342 |
} |
|
343 |
} |
|
344 |
}); |
|
345 |
((DefaultHttpClient) hc).addResponseInterceptor(new HttpResponseInterceptor() { |
|
346 |
public void process(HttpResponse response, HttpContext context) throws HttpException, IOException { |
|
347 |
HttpEntity entity = response.getEntity(); |
|
348 |
if (entity == null) |
|
349 |
return; |
|
350 |
Header ceHeader = entity.getContentEncoding(); |
|
351 |
if (ceHeader != null) { |
|
352 |
for (HeaderElement codec : ceHeader.getElements()) { |
|
353 |
if (codec.getName().equalsIgnoreCase("gzip")) { |
|
354 |
response.setEntity(new GzipDecompressingEntity(response.getEntity())); |
|
355 |
return; |
|
356 |
} |
|
357 |
} |
|
358 |
} |
|
359 |
} |
|
360 |
}); |
|
361 |
|
|
362 |
if (proxyHost != null) { |
|
363 |
HttpHost proxy = new HttpHost(proxyHost, proxyPort); |
|
364 |
hc.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); |
|
365 |
logger.info("Proxy Host set to " + proxyHost + ":" + proxyPort); |
|
366 |
} |
|
367 |
} |
|
368 |
|
|
369 |
public String getAuthToken() { |
|
370 |
return authToken; |
|
371 |
} |
|
372 |
|
|
373 |
public String getServerManagementURL() { |
|
374 |
return serverManagementURL; |
|
375 |
} |
|
376 |
|
|
377 |
public String getStorageURL() { |
|
378 |
return storageURL; |
|
379 |
} |
|
380 |
|
|
381 |
public String getCdnManagementURL() { |
|
382 |
return cdnManagementURL; |
|
383 |
} |
|
384 |
|
|
385 |
protected HttpClient getHttpClient() { |
|
386 |
if (hc == null) { |
|
387 |
configureHttpClient(); |
|
388 |
} |
|
389 |
return hc; |
|
390 |
} |
|
391 |
|
|
392 |
public void setHttpClient(HttpClient hc) { |
|
393 |
this.hc = hc; |
|
394 |
} |
|
395 |
|
|
396 |
public int getConnectionManagerTimeout() { |
|
397 |
return connectionManagerTimeout; |
|
398 |
} |
|
399 |
|
|
400 |
public void setConnectionManagerTimeout(int timeout) { |
|
401 |
connectionManagerTimeout = timeout; |
|
402 |
hc = null; |
|
403 |
} |
|
404 |
|
|
405 |
public int getSoTimeout() { |
|
406 |
return soTimeout; |
|
407 |
} |
|
408 |
|
|
409 |
public void setSoTimeout(int timeout) { |
|
410 |
soTimeout = timeout; |
|
411 |
hc = null; |
|
412 |
} |
|
413 |
|
|
414 |
public int getConnectionTimeout() { |
|
415 |
return connectionTimeout; |
|
416 |
} |
|
417 |
|
|
418 |
public void setConnectionTimeout(int timeout) { |
|
419 |
connectionTimeout = timeout; |
|
420 |
hc = null; |
|
421 |
} |
|
422 |
|
|
423 |
public int getMaxConnections() { |
|
424 |
return maxConnections; |
|
425 |
} |
|
426 |
|
|
427 |
public void setMaxConnections(int maxConnections) { |
|
428 |
this.maxConnections = maxConnections; |
|
429 |
hc = null; |
|
430 |
} |
|
431 |
|
|
432 |
public String getProxyHost() { |
|
433 |
return proxyHost; |
|
434 |
} |
|
435 |
|
|
436 |
public void setProxyHost(String proxyHost) { |
|
437 |
this.proxyHost = proxyHost; |
|
438 |
hc = null; |
|
439 |
} |
|
440 |
|
|
441 |
public int getProxyPort() { |
|
442 |
return proxyPort; |
|
443 |
} |
|
444 |
|
|
445 |
public void setProxyPort(int proxyPort) { |
|
446 |
this.proxyPort = proxyPort; |
|
447 |
hc = null; |
|
448 |
} |
|
449 |
|
|
450 |
static class GzipDecompressingEntity extends HttpEntityWrapper { |
|
451 |
|
|
452 |
public GzipDecompressingEntity(final HttpEntity entity) { |
|
453 |
super(entity); |
|
454 |
} |
|
455 |
|
|
456 |
@Override |
|
457 |
public InputStream getContent() throws IOException, IllegalStateException { |
|
458 |
// the wrapped entity's getContent() decides about repeatability |
|
459 |
InputStream wrappedin = wrappedEntity.getContent(); |
|
460 |
return new GZIPInputStream(wrappedin); |
|
461 |
} |
|
462 |
|
|
463 |
@Override |
|
464 |
public long getContentLength() { |
|
465 |
// length of ungzipped content is not known |
|
466 |
return -1; |
|
467 |
} |
|
468 |
|
|
469 |
} |
|
470 |
} |
|
1 |
/** |
|
2 |
* Licensed to the Apache Software Foundation (ASF) under one |
|
3 |
* or more contributor license agreements. See the NOTICE file |
|
4 |
* distributed with this work for additional information |
|
5 |
* regarding copyright ownership. The ASF licenses this file |
|
6 |
* to you under the Apache License, Version 2.0 (the |
|
7 |
* "License"); you may not use this file except in compliance |
|
8 |
* with the License. You may obtain a copy of the License at |
|
9 |
* |
|
10 |
* http://www.apache.org/licenses/LICENSE-2.0 |
|
11 |
* |
|
12 |
* Unless required by applicable law or agreed to in writing, |
|
13 |
* software distributed under the License is distributed on an |
|
14 |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
|
15 |
* KIND, either express or implied. See the License for the |
|
16 |
* specific language governing permissions and limitations |
|
17 |
* under the License. |
|
18 |
*/ |
|
19 |
package net.elasticgrid.rackspace.common; |
|
20 |
|
|
21 |
import net.elasticgrid.rackspace.cloudservers.internal.CloudServersAPIFault; |
|
22 |
import org.apache.commons.io.IOUtils; |
|
23 |
import org.apache.http.Header; |
|
24 |
import org.apache.http.HeaderElement; |
|
25 |
import org.apache.http.HttpEntity; |
|
26 |
import org.apache.http.HttpException; |
|
27 |
import org.apache.http.HttpHost; |
|
28 |
import org.apache.http.HttpRequest; |
|
29 |
import org.apache.http.HttpRequestInterceptor; |
|
30 |
import org.apache.http.HttpResponse; |
|
31 |
import org.apache.http.HttpResponseInterceptor; |
|
32 |
import org.apache.http.HttpVersion; |
|
33 |
import org.apache.http.client.HttpClient; |
|
34 |
import org.apache.http.client.methods.HttpGet; |
|
35 |
import org.apache.http.client.methods.HttpRequestBase; |
|
36 |
import org.apache.http.client.methods.HttpPost; |
|
37 |
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; |
|
38 |
import org.apache.http.conn.ClientConnectionManager; |
|
39 |
import org.apache.http.conn.params.ConnManagerParams; |
|
40 |
import org.apache.http.conn.params.ConnRoutePNames; |
|
41 |
import org.apache.http.conn.params.ConnPerRouteBean; |
|
42 |
import org.apache.http.conn.scheme.PlainSocketFactory; |
|
43 |
import org.apache.http.conn.scheme.Scheme; |
|
44 |
import org.apache.http.conn.scheme.SchemeRegistry; |
|
45 |
import org.apache.http.conn.ssl.SSLSocketFactory; |
|
46 |
import org.apache.http.entity.HttpEntityWrapper; |
|
47 |
import org.apache.http.entity.EntityTemplate; |
|
48 |
import org.apache.http.impl.client.DefaultHttpClient; |
|
49 |
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; |
|
50 |
import org.apache.http.params.BasicHttpParams; |
|
51 |
import org.apache.http.params.HttpParams; |
|
52 |
import org.apache.http.params.HttpProtocolParams; |
|
53 |
import org.apache.http.protocol.HttpContext; |
|
54 |
import org.jibx.runtime.BindingDirectory; |
|
55 |
import org.jibx.runtime.IBindingFactory; |
|
56 |
import org.jibx.runtime.IUnmarshallingContext; |
|
57 |
import org.jibx.runtime.JiBXException; |
|
58 |
import java.io.IOException; |
|
59 |
import java.io.InputStream; |
|
60 |
import java.io.ByteArrayOutputStream; |
|
61 |
import java.util.Properties; |
|
62 |
import java.util.logging.Level; |
|
63 |
import java.util.logging.Logger; |
|
64 |
import java.util.zip.GZIPInputStream; |
|
65 |
|
|
66 |
/** |
|
67 |
* This class provides common code to the REST connection classes. |
|
68 |
* Logging: |
|
69 |
* <table> |
|
70 |
* <thead> |
|
71 |
* <th>Level</th> |
|
72 |
* <th align="left">Information</th> |
|
73 |
* </thead> |
|
74 |
* <tbody> |
|
75 |
* <tr> |
|
76 |
* <td>WARNING</td> |
|
77 |
* <td>Retries, Expired Authentication before the request is automatically retried</td> |
|
78 |
* </tr> |
|
79 |
* <tr> |
|
80 |
* <td>INFO</td> |
|
81 |
* <td>Request URI & Method</td> |
|
82 |
* </tr> |
|
83 |
* <tr> |
|
84 |
* <td>FINEST</td> |
|
85 |
* <td>Request Body, Response Body</td> |
|
86 |
* </tr> |
|
87 |
* </tbody> |
|
88 |
* </table> |
|
89 |
* |
|
90 |
* @author Jerome Bernard |
|
91 |
*/ |
|
92 |
public class RackspaceConnection { |
|
93 |
// this is the number of automatic retries |
|
94 |
private int maxRetries = 5; |
|
95 |
private String userAgent = "Elastic-Grid/"; |
|
96 |
private HttpClient hc = null; |
|
97 |
private int maxConnections = 100; |
|
98 |
private String proxyHost = null; |
|
99 |
private int proxyPort; |
|
100 |
private int connectionManagerTimeout = 0; |
|
101 |
private int soTimeout = 0; |
|
102 |
private int connectionTimeout = 0; |
|
103 |
|
|
104 |
private final String username; |
|
105 |
private final String apiKey; |
|
106 |
private String serverManagementURL; |
|
107 |
private String storageURL; |
|
108 |
private String cdnManagementURL; |
|
109 |
private String authToken; |
|
110 |
|
|
111 |
private boolean authenticated = false; |
|
112 |
|
|
113 |
private static final String API_AUTH_URL = "https://auth.api.rackspacecloud.com/v1.0"; |
|
114 |
|
|
115 |
private static final Logger logger = Logger.getLogger(RackspaceConnection.class.getName()); |
|
116 |
|
|
117 |
/** |
|
118 |
* Initializes the Rackspace connection with the Rackspace login information. |
|
119 |
* |
|
120 |
* @param username the Rackspace username |
|
121 |
* @param apiKey the Rackspace API key |
|
122 |
* @throws RackspaceException if the credentials are invalid |
|
123 |
* @throws IOException if there is a network issue |
|
124 |
* @see #authenticate() |
|
125 |
*/ |
|
126 |
public RackspaceConnection(String username, String apiKey) throws RackspaceException, IOException { |
|
127 |
this.username = username; |
|
128 |
this.apiKey = apiKey; |
|
129 |
String version; |
|
130 |
try { |
|
131 |
Properties props = new Properties(); |
|
132 |
props.load(this.getClass().getClassLoader().getResourceAsStream("version.properties")); |
|
133 |
version = props.getProperty("version"); |
|
134 |
} catch (Exception ex) { |
|
135 |
version = "?"; |
|
136 |
} |
|
137 |
userAgent = userAgent + version + " (" + System.getProperty("os.arch") + "; " + System.getProperty("os.name") + ")"; |
|
138 |
authenticate(); |
|
139 |
} |
|
140 |
|
|
141 |
/** |
|
142 |
* Authenticate on Rackspace API. Tokens are only valid for 24 hours, so client code should expect token to expire |
|
143 |
* and renew them if needed. |
|
144 |
* |
|
145 |
* @return the auth token, valid for 24 hours |
|
146 |
* @throws RackspaceException if the credentials are invalid |
|
147 |
* @throws IOException if there is a network issue |
|
148 |
*/ |
|
149 |
public String authenticate() throws RackspaceException, IOException { |
|
150 |
logger.info("Authenticating to Rackspace API..."); |
|
151 |
HttpGet request = new HttpGet(API_AUTH_URL); |
|
152 |
request.addHeader("X-Auth-User", username); |
|
153 |
request.addHeader("X-Auth-Key", apiKey); |
|
154 |
HttpResponse response = getHttpClient().execute(request); |
|
155 |
int statusCode = response.getStatusLine().getStatusCode(); |
|
156 |
switch (statusCode) { |
|
157 |
case 204: |
|
158 |
if (response.getFirstHeader("X-Server-Management-Url") != null) |
|
159 |
serverManagementURL = response.getFirstHeader("X-Server-Management-Url").getValue(); |
|
160 |
if (response.getFirstHeader("X-Storage-Url") != null) |
|
161 |
storageURL = response.getFirstHeader("X-Storage-Url").getValue(); |
|
162 |
if (response.getFirstHeader("X-CDN-Management-Url") != null) |
|
163 |
cdnManagementURL = response.getFirstHeader("X-CDN-Management-Url").getValue(); |
|
164 |
authToken = response.getFirstHeader("X-Auth-Token").getValue(); |
|
165 |
authenticated = true; |
|
166 |
return authToken; |
|
167 |
case 401: |
|
168 |
throw new RackspaceException("Invalid credentials: " + response.getStatusLine().getReasonPhrase()); |
|
169 |
default: |
|
170 |
throw new RackspaceException("Unexpected HTTP response"); |
|
171 |
} |
|
172 |
} |
|
173 |
|
|
174 |
/** |
|
175 |
* Make a http request and process the response. This method also performs automatic retries. |
|
176 |
* |
|
177 |
* @param request the HTTP method to use (GET, POST, DELETE, etc) |
|
178 |
* @param respType the class that represents the desired/expected return type |
|
179 |
* @return the unmarshalled entity |
|
180 |
* @throws RackspaceException |
|
181 |
* @throws IOException if there is an I/O exception |
|
182 |
* @throws HttpException if there is an HTTP exception |
|
183 |
* @throws JiBXException if the result can't be unmarshalled |
|
184 |
*/ |
|
185 |
@SuppressWarnings("unchecked") |
|
186 |
protected <T> T makeRequest(HttpRequestBase request, Class<T> respType) |
|
187 |
throws HttpException, IOException, JiBXException, RackspaceException { |
|
188 |
|
|
189 |
if (!authenticated) |
|
190 |
authenticate(); |
|
191 |
|
|
192 |
// add auth params, and protocol specific headers |
|
193 |
request.addHeader("X-Auth-Token", getAuthToken()); |
|
194 |
|
|
195 |
// set accept and content-type headers |
|
196 |
request.setHeader("Accept", "application/xml; charset=UTF-8"); |
|
197 |
request.setHeader("Accept-Encoding", "gzip"); |
|
198 |
request.setHeader("Content-Type", "application/xml; charset=UTF-8"); |
|
199 |
|
|
200 |
// send the request |
|
201 |
T result = null; |
|
202 |
boolean done = false; |
|
203 |
int retries = 0; |
|
204 |
boolean doRetry = false; |
|
205 |
RackspaceException error = null; |
|
206 |
do { |
|
207 |
HttpResponse response = null; |
|
208 |
if (retries > 0) |
|
209 |
logger.log(Level.INFO, "Retry #{0}: querying via {1} {2}", |
|
210 |
new Object[]{retries, request.getMethod(), request.getURI()}); |
|
211 |
else |
|
212 |
logger.log(Level.INFO, "Querying via {0} {1}", new Object[]{request.getMethod(), request.getURI()}); |
|
213 |
|
|
214 |
if (logger.isLoggable(Level.FINEST) && request instanceof HttpEntityEnclosingRequestBase) { |
|
215 |
HttpEntity entity = ((HttpEntityEnclosingRequestBase) request).getEntity(); |
|
216 |
if (entity instanceof EntityTemplate) { |
|
217 |
EntityTemplate template = (EntityTemplate) entity; |
|
218 |
ByteArrayOutputStream baos = null; |
|
219 |
try { |
|
220 |
baos = new ByteArrayOutputStream(); |
|
221 |
template.writeTo(baos); |
|
222 |
logger.log(Level.FINEST, "Request body:\n{0}", baos.toString()); |
|
223 |
} finally { |
|
224 |
IOUtils.closeQuietly(baos); |
|
225 |
} |
|
226 |
} |
|
227 |
} |
|
228 |
|
|
229 |
InputStream entityStream = null; |
|
230 |
HttpEntity entity = null; |
|
231 |
|
|
232 |
if (logger.isLoggable(Level.FINEST)) { |
|
233 |
response = getHttpClient().execute(request); |
|
234 |
entity = response.getEntity(); |
|
235 |
try { |
|
236 |
entityStream = entity.getContent(); |
|
237 |
logger.log(Level.FINEST, "Response body on " + request.getURI() |
|
238 |
+ " via " + request.getMethod() + ":\n" + IOUtils.toString(entityStream)); |
|
239 |
} finally { |
|
240 |
IOUtils.closeQuietly(entityStream); |
|
241 |
} |
|
242 |
} |
|
243 |
|
|
244 |
response = getHttpClient().execute(request); |
|
245 |
int statusCode = response.getStatusLine().getStatusCode(); |
|
246 |
entity = response.getEntity(); |
|
247 |
|
|
248 |
switch (statusCode) { |
|
249 |
case 200: |
|
250 |
case 202: |
|
251 |
case 203: |
|
252 |
try { |
|
253 |
entityStream = entity.getContent(); |
|
254 |
IBindingFactory bindingFactory = BindingDirectory.getFactory(respType); |
|
255 |
IUnmarshallingContext unmarshallingCxt = bindingFactory.createUnmarshallingContext(); |
|
256 |
result = (T) unmarshallingCxt.unmarshalDocument(entityStream, "UTF-8"); |
|
257 |
} finally { |
|
258 |
entity.consumeContent(); |
|
259 |
IOUtils.closeQuietly(entityStream); |
|
260 |
} |
|
261 |
done = true; |
|
262 |
break; |
|
263 |
case 503: // service unavailable |
|
264 |
logger.log(Level.WARNING, "Service unavailable on {0} via {1}. Will retry in {2} seconds.", |
|
265 |
new Object[]{request.getURI(), request.getMethod(), Math.pow(2.0, retries + 1)}); |
|
266 |
doRetry = true; |
|
267 |
break; |
|
268 |
case 401: // unauthorized |
|
269 |
logger.warning("Not authenticated or authentication token expired. Authenticating..."); |
|
270 |
authenticate(); |
|
271 |
doRetry = true; |
|
272 |
break; |
|
273 |
case 417: |
|
274 |
throw new RackspaceException(new IllegalArgumentException("Some parameters are invalid!")); // TODO: temp hack 'til Rackspace API is fixed! |
|
275 |
case 400: |
|
276 |
case 500: |
|
277 |
default: |
|
278 |
try { |
|
279 |
entityStream = entity.getContent(); |
|
280 |
IBindingFactory bindingFactory = BindingDirectory.getFactory(CloudServersAPIFault.class); |
|
281 |
IUnmarshallingContext unmarshallingCxt = bindingFactory.createUnmarshallingContext(); |
|
282 |
CloudServersAPIFault fault = (CloudServersAPIFault) unmarshallingCxt.unmarshalDocument(entityStream, "UTF-8"); |
|
283 |
done = true; |
|
284 |
throw new RackspaceException(fault.getCode(), fault.getMessage(), fault.getDetails()); |
|
285 |
} catch (JiBXException e) { |
|
286 |
response = getHttpClient().execute(request); |
|
287 |
entity = response.getEntity(); |
|
288 |
entityStream = entity.getContent(); |
|
289 |
logger.log(Level.SEVERE, "Can't unmarshal response from " + request.getURI() |
|
290 |
+ " via " + request.getMethod() + ":" + IOUtils.toString(entityStream)); |
|
291 |
e.printStackTrace(); |
|
292 |
throw e; |
|
293 |
} finally { |
|
294 |
entity.consumeContent(); |
|
295 |
IOUtils.closeQuietly(entityStream); |
|
296 |
} |
|
297 |
} |
|
298 |
|
|
299 |
if (doRetry) { |
|
300 |
retries++; |
|
301 |
if (retries > maxRetries) { |
|
302 |
throw new HttpException("Number of retries exceeded for " + request.getURI(), error); |
|
303 |
} |
|
304 |
doRetry = false; |
|
305 |
try { |
|
306 |
Thread.sleep((int) Math.pow(2.0, retries) * 1000); |
|
307 |
} catch (InterruptedException ex) { |
|
308 |
// do nothing |
|
309 |
} |
|
310 |
} |
|
311 |
} while (!done); |
|
312 |
|
|
313 |
return result; |
|
314 |
} |
|
315 |
|
|
316 |
private void configureHttpClient() { |
|
317 |
HttpParams params = new BasicHttpParams(); |
|
318 |
|
|
319 |
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); |
|
320 |
HttpProtocolParams.setContentCharset(params, "UTF-8"); |
|
321 |
HttpProtocolParams.setUserAgent(params, userAgent); |
|
322 |
HttpProtocolParams.setUseExpectContinue(params, true); |
|
323 |
|
|
324 |
// params.setBooleanParameter("http.tcp.nodelay", true); |
|
325 |
// params.setBooleanParameter("http.coonection.stalecheck", false); |
|
326 |
ConnManagerParams.setTimeout(params, getConnectionManagerTimeout()); |
|
327 |
ConnManagerParams.setMaxTotalConnections(params, getMaxConnections()); |
|
328 |
ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRouteBean(getMaxConnections())); |
|
329 |
params.setIntParameter("http.socket.timeout", getSoTimeout()); |
|
330 |
params.setIntParameter("http.connection.timeout", getConnectionTimeout()); |
|
331 |
|
|
332 |
SchemeRegistry schemeRegistry = new SchemeRegistry(); |
|
333 |
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); |
|
334 |
schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443)); |
|
335 |
ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schemeRegistry); |
|
336 |
hc = new DefaultHttpClient(connMgr, params); |
|
337 |
|
|
338 |
((DefaultHttpClient) hc).addRequestInterceptor(new HttpRequestInterceptor() { |
|
339 |
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException { |
|
340 |
if (!request.containsHeader("Accept-Encoding")) { |
|
341 |
request.addHeader("Accept-Encoding", "gzip"); |
|
342 |
} |
|
343 |
} |
|
344 |
}); |
|
345 |
((DefaultHttpClient) hc).addResponseInterceptor(new HttpResponseInterceptor() { |
|
346 |
public void process(HttpResponse response, HttpContext context) throws HttpException, IOException { |
|
347 |
HttpEntity entity = response.getEntity(); |
|
348 |
if (entity == null) |
|
349 |
return; |
|
350 |
Header ceHeader = entity.getContentEncoding(); |
|
351 |
if (ceHeader != null) { |
|
352 |
for (HeaderElement codec : ceHeader.getElements()) { |
|
353 |
if (codec.getName().equalsIgnoreCase("gzip")) { |
|
354 |
response.setEntity(new GzipDecompressingEntity(response.getEntity())); |
|
355 |
return; |
|
356 |
} |
|
357 |
} |
|
358 |
} |
|
359 |
} |
|
360 |
}); |
|
361 |
|
|
362 |
if (proxyHost != null) { |
|
363 |
HttpHost proxy = new HttpHost(proxyHost, proxyPort); |
|
364 |
hc.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); |
|
365 |
logger.info("Proxy Host set to " + proxyHost + ":" + proxyPort); |
|
366 |
} |
|
367 |
} |
|
368 |
|
|
369 |
public String getAuthToken() { |
|
370 |
return authToken; |
|
371 |
} |
|
372 |
|
|
373 |
public String getServerManagementURL() { |
|
374 |
return serverManagementURL; |
|
375 |
} |
|
376 |
|
|
377 |
public String getStorageURL() { |
|
378 |
return storageURL; |
|
379 |
} |
|
380 |
|
|
381 |
public String getCdnManagementURL() { |
|
382 |
return cdnManagementURL; |
|
383 |
} |
|
384 |
|
|
385 |
protected HttpClient getHttpClient() { |
|
386 |
if (hc == null) { |
|
387 |
configureHttpClient(); |
|
388 |
} |
|
389 |
return hc; |
|
390 |
} |
|
391 |
|
|
392 |
public void setHttpClient(HttpClient hc) { |
|
393 |
this.hc = hc; |
|
394 |
} |
|
395 |
|
|
396 |
public int getConnectionManagerTimeout() { |
|
397 |
return connectionManagerTimeout; |
|
398 |
} |
|
399 |
|
|
400 |
public void setConnectionManagerTimeout(int timeout) { |
|
401 |
connectionManagerTimeout = timeout; |
|
402 |
hc = null; |
|
403 |
} |
|
404 |
|
|
405 |
public int getSoTimeout() { |
|
406 |
return soTimeout; |
|
407 |
} |
|
408 |
|
|
409 |
public void setSoTimeout(int timeout) { |
|
410 |
soTimeout = timeout; |
|
411 |
hc = null; |
|
412 |
} |
|
413 |
|
|
414 |
public int getConnectionTimeout() { |
|
415 |
return connectionTimeout; |
|
416 |
} |
|
417 |
|
|
418 |
public void setConnectionTimeout(int timeout) { |
|
419 |
connectionTimeout = timeout; |
|
420 |
hc = null; |
|
421 |
} |
|
422 |
|
|
423 |
public int getMaxConnections() { |
|
424 |
return maxConnections; |
|
425 |
} |
|
426 |
|
|
427 |
public void setMaxConnections(int maxConnections) { |
|
428 |
this.maxConnections = maxConnections; |
|
429 |
hc = null; |
|
430 |
} |
|
431 |
|
|
432 |
public String getProxyHost() { |
|
433 |
return proxyHost; |
|
434 |
} |
|
435 |
|
|
436 |
public void setProxyHost(String proxyHost) { |
|
437 |
this.proxyHost = proxyHost; |
|
438 |
hc = null; |
|
439 |
} |
|
440 |
|
|
441 |
public int getProxyPort() { |
|
442 |
return proxyPort; |
|
443 |
} |
|
444 |
|
|
445 |
public void setProxyPort(int proxyPort) { |
|
446 |
this.proxyPort = proxyPort; |
|
447 |
hc = null; |
|
448 |
} |
|
449 |
|
|
450 |
static class GzipDecompressingEntity extends HttpEntityWrapper { |
|
451 |
|
|
452 |
public GzipDecompressingEntity(final HttpEntity entity) { |
|
453 |
super(entity); |
|
454 |
} |
|
455 |
|
|
456 |
@Override |
|
457 |
public InputStream getContent() throws IOException, IllegalStateException { |
|
458 |
// the wrapped entity's getContent() decides about repeatability |
|
459 |
InputStream wrappedin = wrappedEntity.getContent(); |
|
460 |
return new GZIPInputStream(wrappedin); |
|
461 |
} |
|
462 |
|
|
463 |
@Override |
|
464 |
public long getContentLength() { |
|
465 |
// length of ungzipped content is not known |
|
466 |
return -1; |
|
467 |
} |
|
468 |
|
|
469 |
} |
|
470 |
} |
Also available in: Unified diff