001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.rest.client;
019
020import java.io.BufferedInputStream;
021import java.io.ByteArrayInputStream;
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.net.URI;
026import java.net.URISyntaxException;
027import java.net.URL;
028import java.nio.file.Files;
029import java.security.KeyManagementException;
030import java.security.KeyStore;
031import java.security.KeyStoreException;
032import java.security.NoSuchAlgorithmException;
033import java.security.cert.CertificateException;
034import java.util.Collections;
035import java.util.Map;
036import java.util.Optional;
037import java.util.concurrent.ConcurrentHashMap;
038import javax.net.ssl.SSLContext;
039import org.apache.hadoop.conf.Configuration;
040import org.apache.hadoop.hbase.HBaseConfiguration;
041import org.apache.hadoop.hbase.rest.Constants;
042import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
043import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
044import org.apache.hadoop.security.authentication.client.AuthenticationException;
045import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
046import org.apache.http.Header;
047import org.apache.http.HttpResponse;
048import org.apache.http.HttpStatus;
049import org.apache.http.client.HttpClient;
050import org.apache.http.client.config.RequestConfig;
051import org.apache.http.client.methods.HttpDelete;
052import org.apache.http.client.methods.HttpGet;
053import org.apache.http.client.methods.HttpHead;
054import org.apache.http.client.methods.HttpPost;
055import org.apache.http.client.methods.HttpPut;
056import org.apache.http.client.methods.HttpUriRequest;
057import org.apache.http.entity.InputStreamEntity;
058import org.apache.http.impl.client.HttpClientBuilder;
059import org.apache.http.impl.client.HttpClients;
060import org.apache.http.message.BasicHeader;
061import org.apache.http.ssl.SSLContexts;
062import org.apache.http.util.EntityUtils;
063import org.apache.yetus.audience.InterfaceAudience;
064import org.slf4j.Logger;
065import org.slf4j.LoggerFactory;
066
067import org.apache.hbase.thirdparty.com.google.common.io.ByteStreams;
068import org.apache.hbase.thirdparty.com.google.common.io.Closeables;
069
070/**
071 * A wrapper around HttpClient which provides some useful function and semantics for interacting
072 * with the REST gateway.
073 */
074@InterfaceAudience.Public
075public class Client {
076  public static final Header[] EMPTY_HEADER_ARRAY = new Header[0];
077
078  private static final Logger LOG = LoggerFactory.getLogger(Client.class);
079
080  private HttpClient httpClient;
081  private Cluster cluster;
082  private Integer lastNodeId;
083  private boolean sticky = false;
084  private Configuration conf;
085  private boolean sslEnabled;
086  private HttpResponse resp;
087  private HttpGet httpGet = null;
088
089  private Map<String, String> extraHeaders;
090
091  private static final String AUTH_COOKIE = "hadoop.auth";
092  private static final String AUTH_COOKIE_EQ = AUTH_COOKIE + "=";
093  private static final String COOKIE = "Cookie";
094
095  /**
096   * Default Constructor
097   */
098  public Client() {
099    this(null);
100  }
101
102  private void initialize(Cluster cluster, Configuration conf, boolean sslEnabled,
103    Optional<KeyStore> trustStore) {
104    this.cluster = cluster;
105    this.conf = conf;
106    this.sslEnabled = sslEnabled;
107    extraHeaders = new ConcurrentHashMap<>();
108    String clspath = System.getProperty("java.class.path");
109    LOG.debug("classpath " + clspath);
110    HttpClientBuilder httpClientBuilder = HttpClients.custom();
111
112    int connTimeout = this.conf.getInt(Constants.REST_CLIENT_CONN_TIMEOUT,
113      Constants.DEFAULT_REST_CLIENT_CONN_TIMEOUT);
114    int socketTimeout = this.conf.getInt(Constants.REST_CLIENT_SOCKET_TIMEOUT,
115      Constants.DEFAULT_REST_CLIENT_SOCKET_TIMEOUT);
116    RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(connTimeout)
117      .setSocketTimeout(socketTimeout).setNormalizeUri(false) // URIs should not be normalized, see
118                                                              // HBASE-26903
119      .build();
120    httpClientBuilder.setDefaultRequestConfig(requestConfig);
121
122    // Since HBASE-25267 we don't use the deprecated DefaultHttpClient anymore.
123    // The new http client would decompress the gzip content automatically.
124    // In order to keep the original behaviour of this public class, we disable
125    // automatic content compression.
126    httpClientBuilder.disableContentCompression();
127
128    if (sslEnabled && trustStore.isPresent()) {
129      try {
130        SSLContext sslcontext =
131          SSLContexts.custom().loadTrustMaterial(trustStore.get(), null).build();
132        httpClientBuilder.setSSLContext(sslcontext);
133      } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
134        throw new ClientTrustStoreInitializationException("Error while processing truststore", e);
135      }
136    }
137
138    this.httpClient = httpClientBuilder.build();
139  }
140
141  /**
142   * Constructor
143   * @param cluster the cluster definition
144   */
145  public Client(Cluster cluster) {
146    this(cluster, false);
147  }
148
149  /**
150   * Constructor
151   * @param cluster    the cluster definition
152   * @param sslEnabled enable SSL or not
153   */
154  public Client(Cluster cluster, boolean sslEnabled) {
155    initialize(cluster, HBaseConfiguration.create(), sslEnabled, Optional.empty());
156  }
157
158  /**
159   * Constructor
160   * @param cluster    the cluster definition
161   * @param conf       Configuration
162   * @param sslEnabled enable SSL or not
163   */
164  public Client(Cluster cluster, Configuration conf, boolean sslEnabled) {
165    initialize(cluster, conf, sslEnabled, Optional.empty());
166  }
167
168  /**
169   * Constructor, allowing to define custom trust store (only for SSL connections)
170   * @param cluster            the cluster definition
171   * @param trustStorePath     custom trust store to use for SSL connections
172   * @param trustStorePassword password to use for custom trust store
173   * @param trustStoreType     type of custom trust store
174   * @throws ClientTrustStoreInitializationException if the trust store file can not be loaded
175   */
176  public Client(Cluster cluster, String trustStorePath, Optional<String> trustStorePassword,
177    Optional<String> trustStoreType) {
178    this(cluster, HBaseConfiguration.create(), trustStorePath, trustStorePassword, trustStoreType);
179  }
180
181  /**
182   * Constructor, allowing to define custom trust store (only for SSL connections)
183   * @param cluster            the cluster definition
184   * @param conf               Configuration
185   * @param trustStorePath     custom trust store to use for SSL connections
186   * @param trustStorePassword password to use for custom trust store
187   * @param trustStoreType     type of custom trust store
188   * @throws ClientTrustStoreInitializationException if the trust store file can not be loaded
189   */
190  public Client(Cluster cluster, Configuration conf, String trustStorePath,
191    Optional<String> trustStorePassword, Optional<String> trustStoreType) {
192
193    char[] password = trustStorePassword.map(String::toCharArray).orElse(null);
194    String type = trustStoreType.orElse(KeyStore.getDefaultType());
195
196    KeyStore trustStore;
197    try {
198      trustStore = KeyStore.getInstance(type);
199    } catch (KeyStoreException e) {
200      throw new ClientTrustStoreInitializationException("Invalid trust store type: " + type, e);
201    }
202    try (InputStream inputStream =
203      new BufferedInputStream(Files.newInputStream(new File(trustStorePath).toPath()))) {
204      trustStore.load(inputStream, password);
205    } catch (CertificateException | NoSuchAlgorithmException | IOException e) {
206      throw new ClientTrustStoreInitializationException("Trust store load error: " + trustStorePath,
207        e);
208    }
209
210    initialize(cluster, conf, true, Optional.of(trustStore));
211  }
212
213  /**
214   * Shut down the client. Close any open persistent connections.
215   */
216  public void shutdown() {
217  }
218
219  /** Returns the wrapped HttpClient */
220  public HttpClient getHttpClient() {
221    return httpClient;
222  }
223
224  /**
225   * Add extra headers. These extra headers will be applied to all http methods before they are
226   * removed. If any header is not used any more, client needs to remove it explicitly.
227   */
228  public void addExtraHeader(final String name, final String value) {
229    extraHeaders.put(name, value);
230  }
231
232  /**
233   * Get an extra header value.
234   */
235  public String getExtraHeader(final String name) {
236    return extraHeaders.get(name);
237  }
238
239  /**
240   * Get all extra headers (read-only).
241   */
242  public Map<String, String> getExtraHeaders() {
243    return Collections.unmodifiableMap(extraHeaders);
244  }
245
246  /**
247   * Remove an extra header.
248   */
249  public void removeExtraHeader(final String name) {
250    extraHeaders.remove(name);
251  }
252
253  /**
254   * Execute a transaction method given only the path. If sticky is false: Will select at random one
255   * of the members of the supplied cluster definition and iterate through the list until a
256   * transaction can be successfully completed. The definition of success here is a complete HTTP
257   * transaction, irrespective of result code. If sticky is true: For the first request it will
258   * select a random one of the members of the supplied cluster definition. For subsequent requests
259   * it will use the same member, and it will not automatically re-try if the call fails.
260   * @param cluster the cluster definition
261   * @param method  the transaction method
262   * @param headers HTTP header values to send
263   * @param path    the properly urlencoded path
264   * @return the HTTP response code
265   */
266  public HttpResponse executePathOnly(Cluster cluster, HttpUriRequest method, Header[] headers,
267    String path) throws IOException {
268    IOException lastException;
269    if (cluster.nodes.size() < 1) {
270      throw new IOException("Cluster is empty");
271    }
272    if (lastNodeId == null || !sticky) {
273      lastNodeId = (int) (cluster.nodes.size() * Math.random());
274    }
275    int start = lastNodeId;
276    do {
277      cluster.lastHost = cluster.nodes.get(lastNodeId);
278      try {
279        StringBuilder sb = new StringBuilder();
280        if (sslEnabled) {
281          sb.append("https://");
282        } else {
283          sb.append("http://");
284        }
285        sb.append(cluster.lastHost);
286        sb.append(path);
287        URI uri = new URI(sb.toString());
288        if (method instanceof HttpPut) {
289          HttpPut put = new HttpPut(uri);
290          put.setEntity(((HttpPut) method).getEntity());
291          put.setHeaders(method.getAllHeaders());
292          method = put;
293        } else if (method instanceof HttpGet) {
294          method = new HttpGet(uri);
295        } else if (method instanceof HttpHead) {
296          method = new HttpHead(uri);
297        } else if (method instanceof HttpDelete) {
298          method = new HttpDelete(uri);
299        } else if (method instanceof HttpPost) {
300          HttpPost post = new HttpPost(uri);
301          post.setEntity(((HttpPost) method).getEntity());
302          post.setHeaders(method.getAllHeaders());
303          method = post;
304        }
305        return executeURI(method, headers, uri.toString());
306      } catch (IOException e) {
307        lastException = e;
308      } catch (URISyntaxException use) {
309        lastException = new IOException(use);
310      }
311      if (!sticky) {
312        lastNodeId = (++lastNodeId) % cluster.nodes.size();
313      }
314      // Do not retry if sticky. Let the caller handle the error.
315    } while (!sticky && lastNodeId != start);
316    throw lastException;
317  }
318
319  /**
320   * Execute a transaction method given a complete URI.
321   * @param method  the transaction method
322   * @param headers HTTP header values to send
323   * @param uri     a properly urlencoded URI
324   * @return the HTTP response code
325   */
326  public HttpResponse executeURI(HttpUriRequest method, Header[] headers, String uri)
327    throws IOException {
328    // method.setURI(new URI(uri, true));
329    for (Map.Entry<String, String> e : extraHeaders.entrySet()) {
330      method.addHeader(e.getKey(), e.getValue());
331    }
332    if (headers != null) {
333      for (Header header : headers) {
334        method.addHeader(header);
335      }
336    }
337    long startTime = EnvironmentEdgeManager.currentTime();
338    if (resp != null) EntityUtils.consumeQuietly(resp.getEntity());
339    resp = httpClient.execute(method);
340    if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
341      // Authentication error
342      LOG.debug("Performing negotiation with the server.");
343      negotiate(method, uri);
344      resp = httpClient.execute(method);
345    }
346
347    long endTime = EnvironmentEdgeManager.currentTime();
348    if (LOG.isTraceEnabled()) {
349      LOG.trace(method.getMethod() + " " + uri + " " + resp.getStatusLine().getStatusCode() + " "
350        + resp.getStatusLine().getReasonPhrase() + " in " + (endTime - startTime) + " ms");
351    }
352    return resp;
353  }
354
355  /**
356   * Execute a transaction method. Will call either <tt>executePathOnly</tt> or <tt>executeURI</tt>
357   * depending on whether a path only is supplied in 'path', or if a complete URI is passed instead,
358   * respectively.
359   * @param cluster the cluster definition
360   * @param method  the HTTP method
361   * @param headers HTTP header values to send
362   * @param path    the properly urlencoded path or URI
363   * @return the HTTP response code
364   */
365  public HttpResponse execute(Cluster cluster, HttpUriRequest method, Header[] headers, String path)
366    throws IOException {
367    if (path.startsWith("/")) {
368      return executePathOnly(cluster, method, headers, path);
369    }
370    return executeURI(method, headers, path);
371  }
372
373  /**
374   * Initiate client side Kerberos negotiation with the server.
375   * @param method method to inject the authentication token into.
376   * @param uri    the String to parse as a URL.
377   * @throws IOException if unknown protocol is found.
378   */
379  private void negotiate(HttpUriRequest method, String uri) throws IOException {
380    try {
381      AuthenticatedURL.Token token = new AuthenticatedURL.Token();
382      KerberosAuthenticator authenticator = new KerberosAuthenticator();
383      authenticator.authenticate(new URL(uri), token);
384      // Inject the obtained negotiated token in the method cookie
385      injectToken(method, token);
386    } catch (AuthenticationException e) {
387      LOG.error("Failed to negotiate with the server.", e);
388      throw new IOException(e);
389    }
390  }
391
392  /**
393   * Helper method that injects an authentication token to send with the method.
394   * @param method method to inject the authentication token into.
395   * @param token  authentication token to inject.
396   */
397  private void injectToken(HttpUriRequest method, AuthenticatedURL.Token token) {
398    String t = token.toString();
399    if (t != null) {
400      if (!t.startsWith("\"")) {
401        t = "\"" + t + "\"";
402      }
403      method.addHeader(COOKIE, AUTH_COOKIE_EQ + t);
404    }
405  }
406
407  /** Returns the cluster definition */
408  public Cluster getCluster() {
409    return cluster;
410  }
411
412  /**
413   * @param cluster the cluster definition
414   */
415  public void setCluster(Cluster cluster) {
416    this.cluster = cluster;
417  }
418
419  /**
420   * The default behaviour is load balancing by sending each request to a random host. This DOES NOT
421   * work with scans, which have state on the REST servers. Make sure sticky is set to true before
422   * attempting Scan related operations if more than one host is defined in the cluster.
423   * @return whether subsequent requests will use the same host
424   */
425  public boolean isSticky() {
426    return sticky;
427  }
428
429  /**
430   * The default behaviour is load balancing by sending each request to a random host. This DOES NOT
431   * work with scans, which have state on the REST servers. Set sticky to true before attempting
432   * Scan related operations if more than one host is defined in the cluster. Nodes must not be
433   * added or removed from the Cluster object while sticky is true.
434   * @param sticky whether subsequent requests will use the same host
435   */
436  public void setSticky(boolean sticky) {
437    lastNodeId = null;
438    this.sticky = sticky;
439  }
440
441  /**
442   * Send a HEAD request
443   * @param path the path or URI
444   * @return a Response object with response detail
445   */
446  public Response head(String path) throws IOException {
447    return head(cluster, path, null);
448  }
449
450  /**
451   * Send a HEAD request
452   * @param cluster the cluster definition
453   * @param path    the path or URI
454   * @param headers the HTTP headers to include in the request
455   * @return a Response object with response detail
456   */
457  public Response head(Cluster cluster, String path, Header[] headers) throws IOException {
458    HttpHead method = new HttpHead(path);
459    try {
460      HttpResponse resp = execute(cluster, method, null, path);
461      return new Response(resp.getStatusLine().getStatusCode(), resp.getAllHeaders(), null);
462    } finally {
463      method.releaseConnection();
464    }
465  }
466
467  /**
468   * Send a GET request
469   * @param path the path or URI
470   * @return a Response object with response detail
471   */
472  public Response get(String path) throws IOException {
473    return get(cluster, path);
474  }
475
476  /**
477   * Send a GET request
478   * @param cluster the cluster definition
479   * @param path    the path or URI
480   * @return a Response object with response detail
481   */
482  public Response get(Cluster cluster, String path) throws IOException {
483    return get(cluster, path, EMPTY_HEADER_ARRAY);
484  }
485
486  /**
487   * Send a GET request
488   * @param path   the path or URI
489   * @param accept Accept header value
490   * @return a Response object with response detail
491   */
492  public Response get(String path, String accept) throws IOException {
493    return get(cluster, path, accept);
494  }
495
496  /**
497   * Send a GET request
498   * @param cluster the cluster definition
499   * @param path    the path or URI
500   * @param accept  Accept header value
501   * @return a Response object with response detail
502   */
503  public Response get(Cluster cluster, String path, String accept) throws IOException {
504    Header[] headers = new Header[1];
505    headers[0] = new BasicHeader("Accept", accept);
506    return get(cluster, path, headers);
507  }
508
509  /**
510   * Send a GET request
511   * @param path    the path or URI
512   * @param headers the HTTP headers to include in the request, <tt>Accept</tt> must be supplied
513   * @return a Response object with response detail
514   */
515  public Response get(String path, Header[] headers) throws IOException {
516    return get(cluster, path, headers);
517  }
518
519  /**
520   * Returns the response body of the HTTPResponse, if any, as an array of bytes. If response body
521   * is not available or cannot be read, returns <tt>null</tt> Note: This will cause the entire
522   * response body to be buffered in memory. A malicious server may easily exhaust all the VM
523   * memory. It is strongly recommended, to use getResponseAsStream if the content length of the
524   * response is unknown or reasonably large.
525   * @param resp HttpResponse
526   * @return The response body, null if body is empty
527   * @throws IOException If an I/O (transport) problem occurs while obtaining the response body.
528   */
529  public static byte[] getResponseBody(HttpResponse resp) throws IOException {
530    if (resp.getEntity() == null) {
531      return null;
532    }
533    InputStream instream = resp.getEntity().getContent();
534    if (instream == null) {
535      return null;
536    }
537    try {
538      long contentLength = resp.getEntity().getContentLength();
539      if (contentLength > Integer.MAX_VALUE) {
540        // guard integer cast from overflow
541        throw new IOException("Content too large to be buffered: " + contentLength + " bytes");
542      }
543      if (contentLength > 0) {
544        byte[] content = new byte[(int) contentLength];
545        ByteStreams.readFully(instream, content);
546        return content;
547      } else {
548        return ByteStreams.toByteArray(instream);
549      }
550    } finally {
551      Closeables.closeQuietly(instream);
552    }
553  }
554
555  /**
556   * Send a GET request
557   * @param c       the cluster definition
558   * @param path    the path or URI
559   * @param headers the HTTP headers to include in the request
560   * @return a Response object with response detail
561   */
562  public Response get(Cluster c, String path, Header[] headers) throws IOException {
563    if (httpGet != null) {
564      httpGet.releaseConnection();
565    }
566    httpGet = new HttpGet(path);
567    HttpResponse resp = execute(c, httpGet, headers, path);
568    return new Response(resp.getStatusLine().getStatusCode(), resp.getAllHeaders(), resp,
569      resp.getEntity() == null ? null : resp.getEntity().getContent());
570  }
571
572  /**
573   * Send a PUT request
574   * @param path        the path or URI
575   * @param contentType the content MIME type
576   * @param content     the content bytes
577   * @return a Response object with response detail
578   */
579  public Response put(String path, String contentType, byte[] content) throws IOException {
580    return put(cluster, path, contentType, content);
581  }
582
583  /**
584   * Send a PUT request
585   * @param path        the path or URI
586   * @param contentType the content MIME type
587   * @param content     the content bytes
588   * @param extraHdr    extra Header to send
589   * @return a Response object with response detail
590   */
591  public Response put(String path, String contentType, byte[] content, Header extraHdr)
592    throws IOException {
593    return put(cluster, path, contentType, content, extraHdr);
594  }
595
596  /**
597   * Send a PUT request
598   * @param cluster     the cluster definition
599   * @param path        the path or URI
600   * @param contentType the content MIME type
601   * @param content     the content bytes
602   * @return a Response object with response detail
603   * @throws IOException for error
604   */
605  public Response put(Cluster cluster, String path, String contentType, byte[] content)
606    throws IOException {
607    Header[] headers = new Header[1];
608    headers[0] = new BasicHeader("Content-Type", contentType);
609    return put(cluster, path, headers, content);
610  }
611
612  /**
613   * Send a PUT request
614   * @param cluster     the cluster definition
615   * @param path        the path or URI
616   * @param contentType the content MIME type
617   * @param content     the content bytes
618   * @param extraHdr    additional Header to send
619   * @return a Response object with response detail
620   * @throws IOException for error
621   */
622  public Response put(Cluster cluster, String path, String contentType, byte[] content,
623    Header extraHdr) throws IOException {
624    int cnt = extraHdr == null ? 1 : 2;
625    Header[] headers = new Header[cnt];
626    headers[0] = new BasicHeader("Content-Type", contentType);
627    if (extraHdr != null) {
628      headers[1] = extraHdr;
629    }
630    return put(cluster, path, headers, content);
631  }
632
633  /**
634   * Send a PUT request
635   * @param path    the path or URI
636   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
637   * @param content the content bytes
638   * @return a Response object with response detail
639   */
640  public Response put(String path, Header[] headers, byte[] content) throws IOException {
641    return put(cluster, path, headers, content);
642  }
643
644  /**
645   * Send a PUT request
646   * @param cluster the cluster definition
647   * @param path    the path or URI
648   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
649   * @param content the content bytes
650   * @return a Response object with response detail
651   */
652  public Response put(Cluster cluster, String path, Header[] headers, byte[] content)
653    throws IOException {
654    HttpPut method = new HttpPut(path);
655    try {
656      method.setEntity(new InputStreamEntity(new ByteArrayInputStream(content), content.length));
657      HttpResponse resp = execute(cluster, method, headers, path);
658      headers = resp.getAllHeaders();
659      content = getResponseBody(resp);
660      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
661    } finally {
662      method.releaseConnection();
663    }
664  }
665
666  /**
667   * Send a POST request
668   * @param path        the path or URI
669   * @param contentType the content MIME type
670   * @param content     the content bytes
671   * @return a Response object with response detail
672   */
673  public Response post(String path, String contentType, byte[] content) throws IOException {
674    return post(cluster, path, contentType, content);
675  }
676
677  /**
678   * Send a POST request
679   * @param path        the path or URI
680   * @param contentType the content MIME type
681   * @param content     the content bytes
682   * @param extraHdr    additional Header to send
683   * @return a Response object with response detail
684   */
685  public Response post(String path, String contentType, byte[] content, Header extraHdr)
686    throws IOException {
687    return post(cluster, path, contentType, content, extraHdr);
688  }
689
690  /**
691   * Send a POST request
692   * @param cluster     the cluster definition
693   * @param path        the path or URI
694   * @param contentType the content MIME type
695   * @param content     the content bytes
696   * @return a Response object with response detail
697   * @throws IOException for error
698   */
699  public Response post(Cluster cluster, String path, String contentType, byte[] content)
700    throws IOException {
701    Header[] headers = new Header[1];
702    headers[0] = new BasicHeader("Content-Type", contentType);
703    return post(cluster, path, headers, content);
704  }
705
706  /**
707   * Send a POST request
708   * @param cluster     the cluster definition
709   * @param path        the path or URI
710   * @param contentType the content MIME type
711   * @param content     the content bytes
712   * @param extraHdr    additional Header to send
713   * @return a Response object with response detail
714   * @throws IOException for error
715   */
716  public Response post(Cluster cluster, String path, String contentType, byte[] content,
717    Header extraHdr) throws IOException {
718    int cnt = extraHdr == null ? 1 : 2;
719    Header[] headers = new Header[cnt];
720    headers[0] = new BasicHeader("Content-Type", contentType);
721    if (extraHdr != null) {
722      headers[1] = extraHdr;
723    }
724    return post(cluster, path, headers, content);
725  }
726
727  /**
728   * Send a POST request
729   * @param path    the path or URI
730   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
731   * @param content the content bytes
732   * @return a Response object with response detail
733   */
734  public Response post(String path, Header[] headers, byte[] content) throws IOException {
735    return post(cluster, path, headers, content);
736  }
737
738  /**
739   * Send a POST request
740   * @param cluster the cluster definition
741   * @param path    the path or URI
742   * @param headers the HTTP headers to include, <tt>Content-Type</tt> must be supplied
743   * @param content the content bytes
744   * @return a Response object with response detail
745   */
746  public Response post(Cluster cluster, String path, Header[] headers, byte[] content)
747    throws IOException {
748    HttpPost method = new HttpPost(path);
749    try {
750      method.setEntity(new InputStreamEntity(new ByteArrayInputStream(content), content.length));
751      HttpResponse resp = execute(cluster, method, headers, path);
752      headers = resp.getAllHeaders();
753      content = getResponseBody(resp);
754      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
755    } finally {
756      method.releaseConnection();
757    }
758  }
759
760  /**
761   * Send a DELETE request
762   * @param path the path or URI
763   * @return a Response object with response detail
764   */
765  public Response delete(String path) throws IOException {
766    return delete(cluster, path);
767  }
768
769  /**
770   * Send a DELETE request
771   * @param path     the path or URI
772   * @param extraHdr additional Header to send
773   * @return a Response object with response detail
774   */
775  public Response delete(String path, Header extraHdr) throws IOException {
776    return delete(cluster, path, extraHdr);
777  }
778
779  /**
780   * Send a DELETE request
781   * @param cluster the cluster definition
782   * @param path    the path or URI
783   * @return a Response object with response detail
784   * @throws IOException for error
785   */
786  public Response delete(Cluster cluster, String path) throws IOException {
787    HttpDelete method = new HttpDelete(path);
788    try {
789      HttpResponse resp = execute(cluster, method, null, path);
790      Header[] headers = resp.getAllHeaders();
791      byte[] content = getResponseBody(resp);
792      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
793    } finally {
794      method.releaseConnection();
795    }
796  }
797
798  /**
799   * Send a DELETE request
800   * @param cluster the cluster definition
801   * @param path    the path or URI
802   * @return a Response object with response detail
803   * @throws IOException for error
804   */
805  public Response delete(Cluster cluster, String path, Header extraHdr) throws IOException {
806    HttpDelete method = new HttpDelete(path);
807    try {
808      Header[] headers = { extraHdr };
809      HttpResponse resp = execute(cluster, method, headers, path);
810      headers = resp.getAllHeaders();
811      byte[] content = getResponseBody(resp);
812      return new Response(resp.getStatusLine().getStatusCode(), headers, content);
813    } finally {
814      method.releaseConnection();
815    }
816  }
817
818  public static class ClientTrustStoreInitializationException extends RuntimeException {
819
820    public ClientTrustStoreInitializationException(String message, Throwable cause) {
821      super(message, cause);
822    }
823  }
824}