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