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