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