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 &amp; 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 &amp; 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