View Javadoc

1   /*
2    *
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  package org.apache.hadoop.hbase.rest.client;
21  
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.URL;
25  import java.util.Collections;
26  import java.util.Map;
27  import java.util.concurrent.ConcurrentHashMap;
28  
29  import org.apache.commons.httpclient.Header;
30  import org.apache.commons.httpclient.HttpClient;
31  import org.apache.commons.httpclient.HttpMethod;
32  import org.apache.commons.httpclient.HttpStatus;
33  import org.apache.commons.httpclient.HttpVersion;
34  import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
35  import org.apache.commons.httpclient.URI;
36  import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
37  import org.apache.commons.httpclient.methods.DeleteMethod;
38  import org.apache.commons.httpclient.methods.GetMethod;
39  import org.apache.commons.httpclient.methods.HeadMethod;
40  import org.apache.commons.httpclient.methods.PostMethod;
41  import org.apache.commons.httpclient.methods.PutMethod;
42  import org.apache.commons.httpclient.params.HttpClientParams;
43  import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  import org.apache.hadoop.hbase.classification.InterfaceAudience;
47  import org.apache.hadoop.hbase.classification.InterfaceStability;
48  import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
49  import org.apache.hadoop.security.authentication.client.AuthenticationException;
50  import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
51  
52  /**
53   * A wrapper around HttpClient which provides some useful function and
54   * semantics for interacting with the REST gateway.
55   */
56  @InterfaceAudience.Public
57  @InterfaceStability.Stable
58  public class Client {
59    public static final Header[] EMPTY_HEADER_ARRAY = new Header[0];
60  
61    private static final Log LOG = LogFactory.getLog(Client.class);
62  
63    private HttpClient httpClient;
64    private Cluster cluster;
65    private boolean sslEnabled;
66  
67    private Map<String, String> extraHeaders;
68  
69    private static final String AUTH_COOKIE = "hadoop.auth";
70    private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";
71    private static final String COOKIE = "Cookie";
72  
73    /**
74     * Default Constructor
75     */
76    public Client() {
77      this(null);
78    }
79  
80    private void initialize(Cluster cluster, boolean sslEnabled) {
81      this.cluster = cluster;
82      this.sslEnabled = sslEnabled;
83      MultiThreadedHttpConnectionManager manager =
84        new MultiThreadedHttpConnectionManager();
85      HttpConnectionManagerParams managerParams = manager.getParams();
86      managerParams.setConnectionTimeout(2000); // 2 s
87      managerParams.setDefaultMaxConnectionsPerHost(10);
88      managerParams.setMaxTotalConnections(100);
89      extraHeaders = new ConcurrentHashMap<String, String>();
90      this.httpClient = new HttpClient(manager);
91      HttpClientParams clientParams = httpClient.getParams();
92      clientParams.setVersion(HttpVersion.HTTP_1_1);
93  
94    }
95    /**
96     * Constructor
97     * @param cluster the cluster definition
98     */
99    public Client(Cluster cluster) {
100     initialize(cluster, false);
101   }
102 
103   /**
104    * Constructor
105    * @param cluster the cluster definition
106    * @param sslEnabled enable SSL or not
107    */
108   public Client(Cluster cluster, boolean sslEnabled) {
109     initialize(cluster, sslEnabled);
110   }
111 
112   /**
113    * Shut down the client. Close any open persistent connections.
114    */
115   public void shutdown() {
116     MultiThreadedHttpConnectionManager manager =
117       (MultiThreadedHttpConnectionManager) httpClient.getHttpConnectionManager();
118     manager.shutdown();
119   }
120 
121   /**
122    * @return the wrapped HttpClient
123    */
124   public HttpClient getHttpClient() {
125     return httpClient;
126   }
127 
128   /**
129    * Add extra headers.  These extra headers will be applied to all http
130    * methods before they are removed. If any header is not used any more,
131    * client needs to remove it explicitly.
132    */
133   public void addExtraHeader(final String name, final String value) {
134     extraHeaders.put(name, value);
135   }
136 
137   /**
138    * Get an extra header value.
139    */
140   public String getExtraHeader(final String name) {
141     return extraHeaders.get(name);
142   }
143 
144   /**
145    * Get all extra headers (read-only).
146    */
147   public Map<String, String> getExtraHeaders() {
148     return Collections.unmodifiableMap(extraHeaders);
149   }
150 
151   /**
152    * Remove an extra header.
153    */
154   public void removeExtraHeader(final String name) {
155     extraHeaders.remove(name);
156   }
157 
158   /**
159    * Execute a transaction method given only the path. Will select at random
160    * one of the members of the supplied cluster definition and iterate through
161    * the list until a transaction can be successfully completed. The
162    * definition of success here is a complete HTTP transaction, irrespective
163    * of result code.
164    * @param cluster the cluster definition
165    * @param method the transaction method
166    * @param headers HTTP header values to send
167    * @param path the properly urlencoded path
168    * @return the HTTP response code
169    * @throws IOException
170    */
171   public int executePathOnly(Cluster cluster, HttpMethod method,
172       Header[] headers, String path) throws IOException {
173     IOException lastException;
174     if (cluster.nodes.size() < 1) {
175       throw new IOException("Cluster is empty");
176     }
177     int start = (int)Math.round((cluster.nodes.size() - 1) * Math.random());
178     int i = start;
179     do {
180       cluster.lastHost = cluster.nodes.get(i);
181       try {
182         StringBuilder sb = new StringBuilder();
183         if (sslEnabled) {
184           sb.append("https://");
185         } else {
186           sb.append("http://");
187         }
188         sb.append(cluster.lastHost);
189         sb.append(path);
190         URI uri = new URI(sb.toString(), true);
191         return executeURI(method, headers, uri.toString());
192       } catch (IOException e) {
193         lastException = e;
194       }
195     } while (++i != start && i < cluster.nodes.size());
196     throw lastException;
197   }
198 
199   /**
200    * Execute a transaction method given a complete URI.
201    * @param method the transaction method
202    * @param headers HTTP header values to send
203    * @param uri a properly urlencoded URI
204    * @return the HTTP response code
205    * @throws IOException
206    */
207   public int executeURI(HttpMethod method, Header[] headers, String uri)
208       throws IOException {
209     method.setURI(new URI(uri, true));
210     for (Map.Entry<String, String> e: extraHeaders.entrySet()) {
211       method.addRequestHeader(e.getKey(), e.getValue());
212     }
213     if (headers != null) {
214       for (Header header: headers) {
215         method.addRequestHeader(header);
216       }
217     }
218     long startTime = System.currentTimeMillis();
219     int code = httpClient.executeMethod(method);
220     if (code == HttpStatus.SC_UNAUTHORIZED) { // Authentication error
221       LOG.debug("Performing negotiation with the server.");
222       negotiate(method, uri);
223       code = httpClient.executeMethod(method);
224     }
225     long endTime = System.currentTimeMillis();
226     if (LOG.isTraceEnabled()) {
227       LOG.trace(method.getName() + " " + uri + " " + code + " " +
228         method.getStatusText() + " in " + (endTime - startTime) + " ms");
229     }
230     return code;
231   }
232 
233   /**
234    * Execute a transaction method. Will call either <tt>executePathOnly</tt>
235    * or <tt>executeURI</tt> depending on whether a path only is supplied in
236    * 'path', or if a complete URI is passed instead, respectively.
237    * @param cluster the cluster definition
238    * @param method the HTTP method
239    * @param headers HTTP header values to send
240    * @param path the properly urlencoded path or URI
241    * @return the HTTP response code
242    * @throws IOException
243    */
244   public int execute(Cluster cluster, HttpMethod method, Header[] headers,
245       String path) throws IOException {
246     if (path.startsWith("/")) {
247       return executePathOnly(cluster, method, headers, path);
248     }
249     return executeURI(method, headers, path);
250   }
251 
252   /**
253    * Initiate client side Kerberos negotiation with the server.
254    * @param method method to inject the authentication token into.
255    * @param uri the String to parse as a URL.
256    * @throws IOException if unknown protocol is found.
257    */
258   private void negotiate(HttpMethod method, String uri) throws IOException {
259     try {
260       AuthenticatedURL.Token token = new AuthenticatedURL.Token();
261       KerberosAuthenticator authenticator = new KerberosAuthenticator();
262       authenticator.authenticate(new URL(uri), token);
263       // Inject the obtained negotiated token in the method cookie
264       injectToken(method, token);
265     } catch (AuthenticationException e) {
266       LOG.error("Failed to negotiate with the server.", e);
267       throw new IOException(e);
268     }
269   }
270 
271   /**
272    * Helper method that injects an authentication token to send with the method.
273    * @param method method to inject the authentication token into.
274    * @param token authentication token to inject.
275    */
276   private void injectToken(HttpMethod method, AuthenticatedURL.Token token) {
277     String t = token.toString();
278     if (t != null) {
279       if (!t.startsWith("\"")) {
280         t = "\"" + t + "\"";
281       }
282       method.addRequestHeader(COOKIE, AUTH_COOKIE_EQ + t);
283     }
284   }
285 
286   /**
287    * @return the cluster definition
288    */
289   public Cluster getCluster() {
290     return cluster;
291   }
292 
293   /**
294    * @param cluster the cluster definition
295    */
296   public void setCluster(Cluster cluster) {
297     this.cluster = cluster;
298   }
299 
300   /**
301    * Send a HEAD request
302    * @param path the path or URI
303    * @return a Response object with response detail
304    * @throws IOException
305    */
306   public Response head(String path) throws IOException {
307     return head(cluster, path, null);
308   }
309 
310   /**
311    * Send a HEAD request
312    * @param cluster the cluster definition
313    * @param path the path or URI
314    * @param headers the HTTP headers to include in the request
315    * @return a Response object with response detail
316    * @throws IOException
317    */
318   public Response head(Cluster cluster, String path, Header[] headers)
319       throws IOException {
320     HeadMethod method = new HeadMethod();
321     try {
322       int code = execute(cluster, method, null, path);
323       headers = method.getResponseHeaders();
324       return new Response(code, headers, null);
325     } finally {
326       method.releaseConnection();
327     }
328   }
329 
330   /**
331    * Send a GET request
332    * @param path the path or URI
333    * @return a Response object with response detail
334    * @throws IOException
335    */
336   public Response get(String path) throws IOException {
337     return get(cluster, path);
338   }
339 
340   /**
341    * Send a GET request
342    * @param cluster the cluster definition
343    * @param path the path or URI
344    * @return a Response object with response detail
345    * @throws IOException
346    */
347   public Response get(Cluster cluster, String path) throws IOException {
348     return get(cluster, path, EMPTY_HEADER_ARRAY);
349   }
350 
351   /**
352    * Send a GET request
353    * @param path the path or URI
354    * @param accept Accept header value
355    * @return a Response object with response detail
356    * @throws IOException
357    */
358   public Response get(String path, String accept) throws IOException {
359     return get(cluster, path, accept);
360   }
361 
362   /**
363    * Send a GET request
364    * @param cluster the cluster definition
365    * @param path the path or URI
366    * @param accept Accept header value
367    * @return a Response object with response detail
368    * @throws IOException
369    */
370   public Response get(Cluster cluster, String path, String accept)
371       throws IOException {
372     Header[] headers = new Header[1];
373     headers[0] = new Header("Accept", accept);
374     return get(cluster, path, headers);
375   }
376 
377   /**
378    * Send a GET request
379    * @param path the path or URI
380    * @param headers the HTTP headers to include in the request,
381    * <tt>Accept</tt> must be supplied
382    * @return a Response object with response detail
383    * @throws IOException
384    */
385   public Response get(String path, Header[] headers) throws IOException {
386     return get(cluster, path, headers);
387   }
388 
389   /**
390    * Send a GET request
391    * @param c the cluster definition
392    * @param path the path or URI
393    * @param headers the HTTP headers to include in the request
394    * @return a Response object with response detail
395    * @throws IOException
396    */
397   public Response get(Cluster c, String path, Header[] headers)
398       throws IOException {
399     GetMethod method = new GetMethod();
400     try {
401       int code = execute(c, method, headers, path);
402       headers = method.getResponseHeaders();
403       byte[] body = method.getResponseBody();
404       InputStream in = method.getResponseBodyAsStream();
405       return new Response(code, headers, body, in);
406     } finally {
407       method.releaseConnection();
408     }
409   }
410 
411   /**
412    * Send a PUT request
413    * @param path the path or URI
414    * @param contentType the content MIME type
415    * @param content the content bytes
416    * @return a Response object with response detail
417    * @throws IOException
418    */
419   public Response put(String path, String contentType, byte[] content)
420       throws IOException {
421     return put(cluster, path, contentType, content);
422   }
423 
424   /**
425    * Send a PUT request
426    * @param path the path or URI
427    * @param contentType the content MIME type
428    * @param content the content bytes
429    * @param extraHdr extra Header to send
430    * @return a Response object with response detail
431    * @throws IOException
432    */
433   public Response put(String path, String contentType, byte[] content, Header extraHdr)
434       throws IOException {
435     return put(cluster, path, contentType, content, extraHdr);
436   }
437 
438   /**
439    * Send a PUT request
440    * @param cluster the cluster definition
441    * @param path the path or URI
442    * @param contentType the content MIME type
443    * @param content the content bytes
444    * @return a Response object with response detail
445    * @throws IOException for error
446    */
447   public Response put(Cluster cluster, String path, String contentType,
448       byte[] content) throws IOException {
449     Header[] headers = new Header[1];
450     headers[0] = new Header("Content-Type", contentType);
451     return put(cluster, path, headers, content);
452   }
453 
454   /**
455    * Send a PUT request
456    * @param cluster the cluster definition
457    * @param path the path or URI
458    * @param contentType the content MIME type
459    * @param content the content bytes
460    * @param extraHdr additional Header to send
461    * @return a Response object with response detail
462    * @throws IOException for error
463    */
464   public Response put(Cluster cluster, String path, String contentType,
465       byte[] content, Header extraHdr) throws IOException {
466     int cnt = extraHdr == null ? 1 : 2;
467     Header[] headers = new Header[cnt];
468     headers[0] = new Header("Content-Type", contentType);
469     if (extraHdr != null) {
470       headers[1] = extraHdr;
471     }
472     return put(cluster, path, headers, content);
473   }
474 
475   /**
476    * Send a PUT request
477    * @param path the path or URI
478    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
479    * supplied
480    * @param content the content bytes
481    * @return a Response object with response detail
482    * @throws IOException
483    */
484   public Response put(String path, Header[] headers, byte[] content)
485       throws IOException {
486     return put(cluster, path, headers, content);
487   }
488 
489   /**
490    * Send a PUT request
491    * @param cluster the cluster definition
492    * @param path the path or URI
493    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
494    * supplied
495    * @param content the content bytes
496    * @return a Response object with response detail
497    * @throws IOException
498    */
499   public Response put(Cluster cluster, String path, Header[] headers,
500       byte[] content) throws IOException {
501     PutMethod method = new PutMethod();
502     try {
503       method.setRequestEntity(new ByteArrayRequestEntity(content));
504       int code = execute(cluster, method, headers, path);
505       headers = method.getResponseHeaders();
506       content = method.getResponseBody();
507       return new Response(code, headers, content);
508     } finally {
509       method.releaseConnection();
510     }
511   }
512 
513   /**
514    * Send a POST request
515    * @param path the path or URI
516    * @param contentType the content MIME type
517    * @param content the content bytes
518    * @return a Response object with response detail
519    * @throws IOException
520    */
521   public Response post(String path, String contentType, byte[] content)
522       throws IOException {
523     return post(cluster, path, contentType, content);
524   }
525 
526   /**
527    * Send a POST request
528    * @param path the path or URI
529    * @param contentType the content MIME type
530    * @param content the content bytes
531    * @param extraHdr additional Header to send
532    * @return a Response object with response detail
533    * @throws IOException
534    */
535   public Response post(String path, String contentType, byte[] content, Header extraHdr)
536       throws IOException {
537     return post(cluster, path, contentType, content, extraHdr);
538   }
539 
540   /**
541    * Send a POST request
542    * @param cluster the cluster definition
543    * @param path the path or URI
544    * @param contentType the content MIME type
545    * @param content the content bytes
546    * @return a Response object with response detail
547    * @throws IOException for error
548    */
549   public Response post(Cluster cluster, String path, String contentType,
550       byte[] content) throws IOException {
551     Header[] headers = new Header[1];
552     headers[0] = new Header("Content-Type", contentType);
553     return post(cluster, path, headers, content);
554   }
555 
556   /**
557    * Send a POST request
558    * @param cluster the cluster definition
559    * @param path the path or URI
560    * @param contentType the content MIME type
561    * @param content the content bytes
562    * @param extraHdr additional Header to send
563    * @return a Response object with response detail
564    * @throws IOException for error
565    */
566   public Response post(Cluster cluster, String path, String contentType,
567       byte[] content, Header extraHdr) throws IOException {
568     int cnt = extraHdr == null ? 1 : 2;
569     Header[] headers = new Header[cnt];
570     headers[0] = new Header("Content-Type", contentType);
571     if (extraHdr != null) {
572       headers[1] = extraHdr;
573     }
574     return post(cluster, path, headers, content);
575   }
576 
577   /**
578    * Send a POST request
579    * @param path the path or URI
580    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
581    * supplied
582    * @param content the content bytes
583    * @return a Response object with response detail
584    * @throws IOException
585    */
586   public Response post(String path, Header[] headers, byte[] content)
587       throws IOException {
588     return post(cluster, path, headers, content);
589   }
590 
591   /**
592    * Send a POST request
593    * @param cluster the cluster definition
594    * @param path the path or URI
595    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
596    * supplied
597    * @param content the content bytes
598    * @return a Response object with response detail
599    * @throws IOException
600    */
601   public Response post(Cluster cluster, String path, Header[] headers,
602       byte[] content) throws IOException {
603     PostMethod method = new PostMethod();
604     try {
605       method.setRequestEntity(new ByteArrayRequestEntity(content));
606       int code = execute(cluster, method, headers, path);
607       headers = method.getResponseHeaders();
608       content = method.getResponseBody();
609       return new Response(code, headers, content);
610     } finally {
611       method.releaseConnection();
612     }
613   }
614 
615   /**
616    * Send a DELETE request
617    * @param path the path or URI
618    * @return a Response object with response detail
619    * @throws IOException
620    */
621   public Response delete(String path) throws IOException {
622     return delete(cluster, path);
623   }
624 
625   /**
626    * Send a DELETE request
627    * @param path the path or URI
628    * @param extraHdr additional Header to send
629    * @return a Response object with response detail
630    * @throws IOException
631    */
632   public Response delete(String path, Header extraHdr) throws IOException {
633     return delete(cluster, path, extraHdr);
634   }
635 
636   /**
637    * Send a DELETE request
638    * @param cluster the cluster definition
639    * @param path the path or URI
640    * @return a Response object with response detail
641    * @throws IOException for error
642    */
643   public Response delete(Cluster cluster, String path) throws IOException {
644     DeleteMethod method = new DeleteMethod();
645     try {
646       int code = execute(cluster, method, null, path);
647       Header[] headers = method.getResponseHeaders();
648       byte[] content = method.getResponseBody();
649       return new Response(code, headers, content);
650     } finally {
651       method.releaseConnection();
652     }
653   }
654 
655   /**
656    * Send a DELETE request
657    * @param cluster the cluster definition
658    * @param path the path or URI
659    * @return a Response object with response detail
660    * @throws IOException for error
661    */
662   public Response delete(Cluster cluster, String path, Header extraHdr) throws IOException {
663     DeleteMethod method = new DeleteMethod();
664     try {
665       Header[] headers = { extraHdr };
666       int code = execute(cluster, method, headers, path);
667       headers = method.getResponseHeaders();
668       byte[] content = method.getResponseBody();
669       return new Response(code, headers, content);
670     } finally {
671       method.releaseConnection();
672     }
673   }
674 }