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.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.net.URI;
27  import java.net.URISyntaxException;
28  import java.util.Collections;
29  import java.util.Map;
30  import java.util.concurrent.ConcurrentHashMap;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.hadoop.hbase.classification.InterfaceAudience;
35  import org.apache.hadoop.hbase.classification.InterfaceStability;
36  import org.apache.http.Header;
37  import org.apache.http.HttpResponse;
38  import org.apache.http.client.HttpClient;
39  import org.apache.http.client.methods.HttpDelete;
40  import org.apache.http.client.methods.HttpGet;
41  import org.apache.http.client.methods.HttpHead;
42  import org.apache.http.client.methods.HttpPost;
43  import org.apache.http.client.methods.HttpPut;
44  import org.apache.http.client.methods.HttpUriRequest;
45  import org.apache.http.entity.InputStreamEntity;
46  import org.apache.http.impl.client.DefaultHttpClient;
47  import org.apache.http.message.BasicHeader;
48  import org.apache.http.params.CoreConnectionPNames;
49  import org.apache.http.util.EntityUtils;
50  
51  /**
52   * A wrapper around HttpClient which provides some useful function and
53   * semantics for interacting with the REST gateway.
54   */
55  @InterfaceAudience.Public
56  @InterfaceStability.Stable
57  public class Client {
58    public static final Header[] EMPTY_HEADER_ARRAY = new Header[0];
59  
60    private static final Log LOG = LogFactory.getLog(Client.class);
61  
62    private HttpClient httpClient;
63    private Cluster cluster;
64    private boolean sslEnabled;
65    private HttpResponse resp;
66    private HttpGet httpGet = null;
67  
68    private Map<String, String> extraHeaders;
69  
70    /**
71     * Default Constructor
72     */
73    public Client() {
74      this(null);
75    }
76  
77    private void initialize(Cluster cluster, boolean sslEnabled) {
78      this.cluster = cluster;
79      this.sslEnabled = sslEnabled;
80      extraHeaders = new ConcurrentHashMap<String, String>();
81      String clspath = System.getProperty("java.class.path");
82      LOG.debug("classpath " + clspath);
83      this.httpClient = new DefaultHttpClient();
84      this.httpClient.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 2000);
85    }
86    /**
87     * Constructor
88     * @param cluster the cluster definition
89     */
90    public Client(Cluster cluster) {
91      initialize(cluster, false);
92    }
93  
94    /**
95     * Constructor
96     * @param cluster the cluster definition
97     * @param sslEnabled enable SSL or not
98     */
99    public Client(Cluster cluster, boolean sslEnabled) {
100     initialize(cluster, sslEnabled);
101   }
102 
103   /**
104    * Shut down the client. Close any open persistent connections. 
105    */
106   public void shutdown() {
107   }
108 
109   /**
110    * @return the wrapped HttpClient
111    */
112   public HttpClient getHttpClient() {
113     return httpClient;
114   }
115 
116   /**
117    * Add extra headers.  These extra headers will be applied to all http
118    * methods before they are removed. If any header is not used any more,
119    * client needs to remove it explicitly.
120    */
121   public void addExtraHeader(final String name, final String value) {
122     extraHeaders.put(name, value);
123   }
124 
125   /**
126    * Get an extra header value.
127    */
128   public String getExtraHeader(final String name) {
129     return extraHeaders.get(name);
130   }
131 
132   /**
133    * Get all extra headers (read-only).
134    */
135   public Map<String, String> getExtraHeaders() {
136     return Collections.unmodifiableMap(extraHeaders);
137   }
138 
139   /**
140    * Remove an extra header.
141    */
142   public void removeExtraHeader(final String name) {
143     extraHeaders.remove(name);
144   }
145 
146   /**
147    * Execute a transaction method given only the path. Will select at random
148    * one of the members of the supplied cluster definition and iterate through
149    * the list until a transaction can be successfully completed. The
150    * definition of success here is a complete HTTP transaction, irrespective
151    * of result code.  
152    * @param cluster the cluster definition
153    * @param method the transaction method
154    * @param headers HTTP header values to send
155    * @param path the properly urlencoded path
156    * @return the HTTP response code
157    * @throws IOException
158    */
159   public HttpResponse executePathOnly(Cluster cluster, HttpUriRequest method,
160       Header[] headers, String path) throws IOException {
161     IOException lastException;
162     if (cluster.nodes.size() < 1) {
163       throw new IOException("Cluster is empty");
164     }
165     int start = (int)Math.round((cluster.nodes.size() - 1) * Math.random());
166     int i = start;
167     do {
168       cluster.lastHost = cluster.nodes.get(i);
169       try {
170         StringBuilder sb = new StringBuilder();
171         if (sslEnabled) {
172           sb.append("https://");
173         } else {
174           sb.append("http://");
175         }
176         sb.append(cluster.lastHost);
177         sb.append(path);
178         URI uri = new URI(sb.toString());
179         if (method instanceof HttpPut) {
180           HttpPut put = new HttpPut(uri);
181           put.setEntity(((HttpPut) method).getEntity());
182           put.setHeaders(method.getAllHeaders());
183           method = put;
184         } else if (method instanceof HttpGet) {
185           method = new HttpGet(uri);
186         } else if (method instanceof HttpHead) {
187           method = new HttpHead(uri);
188         } else if (method instanceof HttpDelete) {
189           method = new HttpDelete(uri);
190         } else if (method instanceof HttpPost) {
191           HttpPost post = new HttpPost(uri);
192           post.setEntity(((HttpPost) method).getEntity());
193           post.setHeaders(method.getAllHeaders());
194           method = post;
195         }
196         return executeURI(method, headers, uri.toString());
197       } catch (IOException e) {
198         lastException = e;
199       } catch (URISyntaxException use) {
200         lastException = new IOException(use);
201       }
202     } while (++i != start && i < cluster.nodes.size());
203     throw lastException;
204   }
205 
206   /**
207    * Execute a transaction method given a complete URI.
208    * @param method the transaction method
209    * @param headers HTTP header values to send
210    * @param uri a properly urlencoded URI
211    * @return the HTTP response
212    * @throws IOException
213    */
214   public HttpResponse executeURI(HttpUriRequest method, Header[] headers, String uri)
215       throws IOException {
216     for (Map.Entry<String, String> e: extraHeaders.entrySet()) {
217       method.addHeader(e.getKey(), e.getValue());
218     }
219     if (headers != null) {
220       for (Header header: headers) {
221         method.addHeader(header);
222       }
223     }
224     long startTime = System.currentTimeMillis();
225     if (resp != null) EntityUtils.consumeQuietly(resp.getEntity());
226     resp = httpClient.execute(method);
227     long endTime = System.currentTimeMillis();
228     if (LOG.isDebugEnabled()) {
229       LOG.debug(method.getMethod() + " " + uri + " " + resp.getStatusLine().getStatusCode() + " " +
230           resp.getStatusLine().getReasonPhrase() + " in " + (endTime - startTime) + " ms");
231     }
232     return resp;
233   }
234 
235   /**
236    * Execute a transaction method. Will call either <tt>executePathOnly</tt>
237    * or <tt>executeURI</tt> depending on whether a path only is supplied in
238    * 'path', or if a complete URI is passed instead, respectively.
239    * @param cluster the cluster definition
240    * @param method the HTTP method
241    * @param headers HTTP header values to send
242    * @param path the properly urlencoded path or URI
243    * @return the HTTP response code
244    * @throws IOException
245    */
246   public HttpResponse execute(Cluster cluster, HttpUriRequest method, Header[] headers,
247       String path) throws IOException {
248     if (path.startsWith("/")) {
249       return executePathOnly(cluster, method, headers, path);
250     }
251     return executeURI(method, headers, path);
252   }
253 
254   /**
255    * @return the cluster definition
256    */
257   public Cluster getCluster() {
258     return cluster;
259   }
260 
261   /**
262    * @param cluster the cluster definition
263    */
264   public void setCluster(Cluster cluster) {
265     this.cluster = cluster;
266   }
267 
268   /**
269    * Send a HEAD request 
270    * @param path the path or URI
271    * @return a Response object with response detail
272    * @throws IOException
273    */
274   public Response head(String path) throws IOException {
275     return head(cluster, path, null);
276   }
277 
278   /**
279    * Send a HEAD request 
280    * @param cluster the cluster definition
281    * @param path the path or URI
282    * @param headers the HTTP headers to include in the request
283    * @return a Response object with response detail
284    * @throws IOException
285    */
286   public Response head(Cluster cluster, String path, Header[] headers) 
287       throws IOException {
288     HttpHead method = new HttpHead(path);
289     try {
290       HttpResponse resp = execute(cluster, method, null, path);
291       // headers = method.getResponseHeaders();
292       return new Response(resp.getStatusLine().getStatusCode(), resp.getAllHeaders(), null);
293     } finally {
294       method.releaseConnection();
295     }
296   }
297 
298   /**
299    * Send a GET request 
300    * @param path the path or URI
301    * @return a Response object with response detail
302    * @throws IOException
303    */
304   public Response get(String path) throws IOException {
305     return get(cluster, path);
306   }
307 
308   /**
309    * Send a GET request 
310    * @param cluster the cluster definition
311    * @param path the path or URI
312    * @return a Response object with response detail
313    * @throws IOException
314    */
315   public Response get(Cluster cluster, String path) throws IOException {
316     return get(cluster, path, EMPTY_HEADER_ARRAY);
317   }
318 
319   /**
320    * Send a GET request 
321    * @param path the path or URI
322    * @param accept Accept header value
323    * @return a Response object with response detail
324    * @throws IOException
325    */
326   public Response get(String path, String accept) throws IOException {
327     return get(cluster, path, accept);
328   }
329 
330   /**
331    * Send a GET request 
332    * @param cluster the cluster definition
333    * @param path the path or URI
334    * @param accept Accept header value
335    * @return a Response object with response detail
336    * @throws IOException
337    */
338   public Response get(Cluster cluster, String path, String accept)
339       throws IOException {
340     Header[] headers = new Header[1];
341     headers[0] = new BasicHeader("Accept", accept);
342     return get(cluster, path, headers);
343   }
344 
345   /**
346    * Send a GET request
347    * @param path the path or URI
348    * @param headers the HTTP headers to include in the request, 
349    * <tt>Accept</tt> must be supplied
350    * @return a Response object with response detail
351    * @throws IOException
352    */
353   public Response get(String path, Header[] headers) throws IOException {
354     return get(cluster, path, headers);
355   }
356 
357   /**
358    * Returns the response body of the HTTPResponse, if any, as an array of bytes.
359    * If response body is not available or cannot be read, returns <tt>null</tt>
360    *
361    * Note: This will cause the entire response body to be buffered in memory. A
362    * malicious server may easily exhaust all the VM memory. It is strongly
363    * recommended, to use getResponseAsStream if the content length of the response
364    * is unknown or reasonably large.
365    *
366    * @param resp HttpResponse
367    * @return The response body, null if body is empty
368    * @throws IOException If an I/O (transport) problem occurs while obtaining the
369    * response body.
370    */
371   @edu.umd.cs.findbugs.annotations.SuppressWarnings(value =
372       "NP_LOAD_OF_KNOWN_NULL_VALUE", justification = "null is possible return value")
373   public static byte[] getResponseBody(HttpResponse resp) throws IOException {
374     if (resp.getEntity() == null) return null;
375     try (InputStream instream = resp.getEntity().getContent()) {
376       if (instream != null) {
377         long contentLength = resp.getEntity().getContentLength();
378         if (contentLength > Integer.MAX_VALUE) {
379           //guard integer cast from overflow
380           throw new IOException("Content too large to be buffered: " + contentLength +" bytes");
381         }
382         ByteArrayOutputStream outstream = new ByteArrayOutputStream(
383             contentLength > 0 ? (int) contentLength : 4*1024);
384         byte[] buffer = new byte[4096];
385         int len;
386         while ((len = instream.read(buffer)) > 0) {
387           outstream.write(buffer, 0, len);
388         }
389         outstream.close();
390         return outstream.toByteArray();
391       }
392       return null;
393     }
394   }
395 
396   /**
397    * Send a GET request
398    * @param c the cluster definition
399    * @param path the path or URI
400    * @param headers the HTTP headers to include in the request
401    * @return a Response object with response detail
402    * @throws IOException
403    */
404   public Response get(Cluster c, String path, Header[] headers) 
405       throws IOException {
406     if (httpGet != null) {
407       httpGet.releaseConnection();
408     }
409     httpGet = new HttpGet(path);
410     HttpResponse resp = execute(c, httpGet, headers, path);
411     return new Response(resp.getStatusLine().getStatusCode(), resp.getAllHeaders(),
412         resp, resp.getEntity() == null ? null : resp.getEntity().getContent());
413   }
414 
415   /**
416    * Send a PUT request
417    * @param path the path or URI
418    * @param contentType the content MIME type
419    * @param content the content bytes
420    * @return a Response object with response detail
421    * @throws IOException
422    */
423   public Response put(String path, String contentType, byte[] content)
424       throws IOException {
425     return put(cluster, path, contentType, content);
426   }
427 
428   /**
429    * Send a PUT request
430    * @param path the path or URI
431    * @param contentType the content MIME type
432    * @param content the content bytes
433    * @param extraHdr extra Header to send
434    * @return a Response object with response detail
435    * @throws IOException
436    */
437   public Response put(String path, String contentType, byte[] content, Header extraHdr)
438       throws IOException {
439     return put(cluster, path, contentType, content, extraHdr);
440   }
441 
442   /**
443    * Send a PUT request
444    * @param cluster the cluster definition
445    * @param path the path or URI
446    * @param contentType the content MIME type
447    * @param content the content bytes
448    * @return a Response object with response detail
449    * @throws IOException for error
450    */
451   public Response put(Cluster cluster, String path, String contentType, 
452       byte[] content) throws IOException {
453     Header[] headers = new Header[1];
454     headers[0] = new BasicHeader("Content-Type", contentType);
455     return put(cluster, path, headers, content);
456   }
457 
458   /**
459    * Send a PUT request
460    * @param cluster the cluster definition
461    * @param path the path or URI
462    * @param contentType the content MIME type
463    * @param content the content bytes
464    * @param extraHdr additional Header to send
465    * @return a Response object with response detail
466    * @throws IOException for error
467    */
468   public Response put(Cluster cluster, String path, String contentType, 
469       byte[] content, Header extraHdr) throws IOException {
470     int cnt = extraHdr == null ? 1 : 2;
471     Header[] headers = new Header[cnt];
472     headers[0] = new BasicHeader("Content-Type", contentType);
473     if (extraHdr != null) {
474       headers[1] = extraHdr;
475     }
476     return put(cluster, path, headers, content);
477   }
478 
479   /**
480    * Send a PUT request
481    * @param path the path or URI
482    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
483    * supplied
484    * @param content the content bytes
485    * @return a Response object with response detail
486    * @throws IOException
487    */
488   public Response put(String path, Header[] headers, byte[] content) 
489       throws IOException {
490     return put(cluster, path, headers, content);
491   }
492 
493   /**
494    * Send a PUT request
495    * @param cluster the cluster definition
496    * @param path the path or URI
497    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
498    * supplied
499    * @param content the content bytes
500    * @return a Response object with response detail
501    * @throws IOException
502    */
503   public Response put(Cluster cluster, String path, Header[] headers, 
504       byte[] content) throws IOException {
505     HttpPut method = new HttpPut(path);
506     try {
507       method.setEntity(new InputStreamEntity(new ByteArrayInputStream(content), content.length));
508       HttpResponse resp = execute(cluster, method, headers, path);
509       headers = resp.getAllHeaders();
510       content = getResponseBody(resp);
511       return new Response(resp.getStatusLine().getStatusCode(), headers, content);
512     } finally {
513       method.releaseConnection();
514     }
515   }
516 
517   /**
518    * Send a POST request
519    * @param path the path or URI
520    * @param contentType the content MIME type
521    * @param content the content bytes
522    * @return a Response object with response detail
523    * @throws IOException
524    */
525   public Response post(String path, String contentType, byte[] content)
526       throws IOException {
527     return post(cluster, path, contentType, content);
528   }
529 
530   /**
531    * Send a POST request
532    * @param path the path or URI
533    * @param contentType the content MIME type
534    * @param content the content bytes
535    * @param extraHdr additional Header to send
536    * @return a Response object with response detail
537    * @throws IOException
538    */
539   public Response post(String path, String contentType, byte[] content, Header extraHdr)
540       throws IOException {
541     return post(cluster, path, contentType, content, extraHdr);
542   }
543 
544   /**
545    * Send a POST request
546    * @param cluster the cluster definition
547    * @param path the path or URI
548    * @param contentType the content MIME type
549    * @param content the content bytes
550    * @return a Response object with response detail
551    * @throws IOException for error
552    */
553   public Response post(Cluster cluster, String path, String contentType, 
554       byte[] content) throws IOException {
555     Header[] headers = new Header[1];
556     headers[0] = new BasicHeader("Content-Type", contentType);
557     return post(cluster, path, headers, content);
558   }
559 
560   /**
561    * Send a POST request
562    * @param cluster the cluster definition
563    * @param path the path or URI
564    * @param contentType the content MIME type
565    * @param content the content bytes
566    * @param extraHdr additional Header to send
567    * @return a Response object with response detail
568    * @throws IOException for error
569    */
570   public Response post(Cluster cluster, String path, String contentType, 
571       byte[] content, Header extraHdr) throws IOException {
572     int cnt = extraHdr == null ? 1 : 2;
573     Header[] headers = new Header[cnt];
574     headers[0] = new BasicHeader("Content-Type", contentType);
575     if (extraHdr != null) {
576       headers[1] = extraHdr;
577     }
578     return post(cluster, path, headers, content);
579   }
580 
581   /**
582    * Send a POST request
583    * @param path the path or URI
584    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
585    * supplied
586    * @param content the content bytes
587    * @return a Response object with response detail
588    * @throws IOException
589    */
590   public Response post(String path, Header[] headers, byte[] content) 
591       throws IOException {
592     return post(cluster, path, headers, content);
593   }
594 
595   /**
596    * Send a POST request
597    * @param cluster the cluster definition
598    * @param path the path or URI
599    * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be
600    * supplied
601    * @param content the content bytes
602    * @return a Response object with response detail
603    * @throws IOException
604    */
605   public Response post(Cluster cluster, String path, Header[] headers, 
606       byte[] content) throws IOException {
607     HttpPost method = new HttpPost(path);
608     try {
609       method.setEntity(new InputStreamEntity(new ByteArrayInputStream(content), content.length));
610       HttpResponse resp = execute(cluster, method, headers, path);
611       headers = resp.getAllHeaders();
612       content = getResponseBody(resp);
613       return new Response(resp.getStatusLine().getStatusCode(), headers, content);
614     } finally {
615       method.releaseConnection();
616     }
617   }
618 
619   /**
620    * Send a DELETE request
621    * @param path the path or URI
622    * @return a Response object with response detail
623    * @throws IOException
624    */
625   public Response delete(String path) throws IOException {
626     return delete(cluster, path);
627   }
628 
629   /**
630    * Send a DELETE request
631    * @param path the path or URI
632    * @param extraHdr additional Header to send
633    * @return a Response object with response detail
634    * @throws IOException
635    */
636   public Response delete(String path, Header extraHdr) throws IOException {
637     return delete(cluster, path, extraHdr);
638   }
639 
640   /**
641    * Send a DELETE request
642    * @param cluster the cluster definition
643    * @param path the path or URI
644    * @return a Response object with response detail
645    * @throws IOException for error
646    */
647   public Response delete(Cluster cluster, String path) throws IOException {
648     HttpDelete method = new HttpDelete(path);
649     try {
650       HttpResponse resp = execute(cluster, method, null, path);
651       Header[] headers = resp.getAllHeaders();
652       byte[] content = getResponseBody(resp);
653       return new Response(resp.getStatusLine().getStatusCode(), headers, content);
654     } finally {
655       method.releaseConnection();
656     }
657   }
658 
659   /**
660    * Send a DELETE request
661    * @param cluster the cluster definition
662    * @param path the path or URI
663    * @return a Response object with response detail
664    * @throws IOException for error
665    */
666   public Response delete(Cluster cluster, String path, Header extraHdr) throws IOException {
667     HttpDelete method = new HttpDelete(path);
668     try {
669       Header[] headers = { extraHdr };
670       HttpResponse resp = execute(cluster, method, headers, path);
671       headers = resp.getAllHeaders();
672       byte[] content = getResponseBody(resp);
673       return new Response(resp.getStatusLine().getStatusCode(), headers, content);
674     } finally {
675       method.releaseConnection();
676     }
677   }
678 }