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