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