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