org.apache.hadoop.fs.swift.http.SwiftRestClient Maven / Gradle / Ivy
Show all versions of hadoop-openstack Show documentation
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.fs.swift.http;
import org.apache.hadoop.fs.swift.util.HttpResponseUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.SocketConfig;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.swift.auth.ApiKeyAuthenticationRequest;
import org.apache.hadoop.fs.swift.auth.ApiKeyCredentials;
import org.apache.hadoop.fs.swift.auth.AuthenticationRequest;
import org.apache.hadoop.fs.swift.auth.AuthenticationRequestWrapper;
import org.apache.hadoop.fs.swift.auth.AuthenticationResponse;
import org.apache.hadoop.fs.swift.auth.AuthenticationWrapper;
import org.apache.hadoop.fs.swift.auth.KeyStoneAuthRequest;
import org.apache.hadoop.fs.swift.auth.KeystoneApiKeyCredentials;
import org.apache.hadoop.fs.swift.auth.PasswordAuthenticationRequest;
import org.apache.hadoop.fs.swift.auth.PasswordCredentials;
import org.apache.hadoop.fs.swift.auth.entities.AccessToken;
import org.apache.hadoop.fs.swift.auth.entities.Catalog;
import org.apache.hadoop.fs.swift.auth.entities.Endpoint;
import org.apache.hadoop.fs.swift.exceptions.SwiftAuthenticationFailedException;
import org.apache.hadoop.fs.swift.exceptions.SwiftBadRequestException;
import org.apache.hadoop.fs.swift.exceptions.SwiftConfigurationException;
import org.apache.hadoop.fs.swift.exceptions.SwiftException;
import org.apache.hadoop.fs.swift.exceptions.SwiftInternalStateException;
import org.apache.hadoop.fs.swift.exceptions.SwiftInvalidResponseException;
import org.apache.hadoop.fs.swift.exceptions.SwiftThrottledRequestException;
import org.apache.hadoop.fs.swift.util.Duration;
import org.apache.hadoop.fs.swift.util.DurationStats;
import org.apache.hadoop.fs.swift.util.DurationStatsTable;
import org.apache.hadoop.fs.swift.util.JSONUtil;
import org.apache.hadoop.fs.swift.util.SwiftObjectPath;
import org.apache.hadoop.fs.swift.util.SwiftUtils;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Properties;
import static org.apache.http.HttpStatus.*;
import static org.apache.hadoop.fs.swift.http.SwiftProtocolConstants.*;
/**
* This implements the client-side of the Swift REST API.
*
* The core actions put, get and query data in the Swift object store,
* after authenticating the client.
*
* Logging:
*
* Logging at DEBUG level displays detail about the actions of this
* client, including HTTP requests and responses -excluding authentication
* details.
*/
public final class SwiftRestClient {
private static final Logger LOG =
LoggerFactory.getLogger(SwiftRestClient.class);
/**
* Header that says "use newest version" -ensures that
* the query doesn't pick up older versions served by
* an eventually consistent filesystem (except in the special case
* of a network partition, at which point no guarantees about
* consistency can be made.
*/
public static final Header NEWEST =
new BasicHeader(SwiftProtocolConstants.X_NEWEST, "true");
/**
* the authentication endpoint as supplied in the configuration.
*/
private final URI authUri;
/**
* Swift region. Some OpenStack installations has more than one region.
* In this case user can specify the region with which Hadoop will be working
*/
private final String region;
/**
* tenant name.
*/
private final String tenant;
/**
* username name.
*/
private final String username;
/**
* user password.
*/
private final String password;
/**
* user api key.
*/
private final String apiKey;
/**
* The authentication request used to authenticate with Swift.
*/
private final AuthenticationRequest authRequest;
/**
* This auth request is similar to @see authRequest,
* with one difference: it has another json representation when
* authRequest one is not applicable.
*/
private AuthenticationRequest keystoneAuthRequest;
private boolean useKeystoneAuthentication = false;
/**
* The container this client is working with.
*/
private final String container;
private final String serviceDescription;
/**
* Access token (Secret).
*/
private AccessToken token;
/**
* Endpoint for swift operations, obtained after authentication.
*/
private URI endpointURI;
/**
* URI under which objects can be found.
* This is set when the user is authenticated -the URI
* is returned in the body of the success response.
*/
private URI objectLocationURI;
/**
* The name of the service provider.
*/
private final String serviceProvider;
/**
* Should the public swift endpoint be used, rather than the in-cluster one?
*/
private final boolean usePublicURL;
/**
* Number of times to retry a connection.
*/
private final int retryCount;
/**
* How long (in milliseconds) should a connection be attempted.
*/
private final int connectTimeout;
/**
* How long (in milliseconds) should a connection be attempted.
*/
private final int socketTimeout;
/**
* How long (in milliseconds) between bulk operations.
*/
private final int throttleDelay;
/**
* the name of a proxy host (can be null, in which case there is no proxy).
*/
private String proxyHost;
/**
* The port of a proxy. This is ignored if {@link #proxyHost} is null.
*/
private int proxyPort;
/**
* Flag to indicate whether or not the client should
* query for file location data.
*/
private final boolean locationAware;
private final int partSizeKB;
/**
* The blocksize of this FS
*/
private final int blocksizeKB;
private final int bufferSizeKB;
private final DurationStatsTable durationStats = new DurationStatsTable();
/**
* objects query endpoint. This is synchronized
* to handle a simultaneous update of all auth data in one
* go.
*/
private synchronized URI getEndpointURI() {
return endpointURI;
}
/**
* token for Swift communication.
*/
private synchronized AccessToken getToken() {
return token;
}
/**
* Setter of authentication and endpoint details.
* Being synchronized guarantees that all three fields are set up together.
* It is up to the reader to read all three fields in their own
* synchronized block to be sure that they are all consistent.
*
* @param endpoint endpoint URI
* @param objectLocation object location URI
* @param authToken auth token
*/
private void setAuthDetails(URI endpoint,
URI objectLocation,
AccessToken authToken) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("setAuth: endpoint=%s; objectURI=%s; token=%s",
endpoint, objectLocation, authToken));
}
synchronized (this) {
endpointURI = endpoint;
objectLocationURI = objectLocation;
token = authToken;
}
}
/**
* Base class for all Swift REST operations.
*
* @param request
* @param result
*/
private static abstract class HttpRequestProcessor
{
public final M createRequest(String uri) throws IOException {
final M req = doCreateRequest(uri);
setup(req);
return req;
}
/**
* Override it to return some result after request is executed.
*/
public abstract R extractResult(M req, HttpResponse resp)
throws IOException;
/**
* Factory method to create a REST method against the given URI.
*
* @param uri target
* @return method to invoke
*/
protected abstract M doCreateRequest(String uri) throws IOException;
/**
* Override port to set up the request before it is executed.
*/
protected void setup(M req) throws IOException {
}
/**
* Override point: what are the status codes that this operation supports?
*
* @return an array with the permitted status code(s)
*/
protected int[] getAllowedStatusCodes() {
return new int[]{
SC_OK,
SC_CREATED,
SC_ACCEPTED,
SC_NO_CONTENT,
SC_PARTIAL_CONTENT,
};
}
}
private static abstract class GetRequestProcessor
extends HttpRequestProcessor {
@Override
protected final HttpGet doCreateRequest(String uri) {
return new HttpGet(uri);
}
}
private static abstract class PostRequestProcessor
extends HttpRequestProcessor {
@Override
protected final HttpPost doCreateRequest(String uri) {
return new HttpPost(uri);
}
}
/**
* There's a special type for auth messages, so that low-level
* message handlers can react to auth failures differently from everything
* else.
*/
private static final class AuthPostRequest extends HttpPost {
private AuthPostRequest(String uri) {
super(uri);
}
}
/**
* Generate an auth message.
* @param response
*/
private static abstract class AuthRequestProcessor
extends HttpRequestProcessor {
@Override
protected final AuthPostRequest doCreateRequest(String uri) {
return new AuthPostRequest(uri);
}
}
private static abstract class PutRequestProcessor
extends HttpRequestProcessor {
@Override
protected final HttpPut doCreateRequest(String uri) {
return new HttpPut(uri);
}
/**
* Override point: what are the status codes that this operation supports?
*
* @return the list of status codes to accept
*/
@Override
protected int[] getAllowedStatusCodes() {
return new int[]{
SC_OK,
SC_CREATED,
SC_NO_CONTENT,
SC_ACCEPTED,
};
}
}
/**
* Create operation.
*
* @param result type
*/
private static abstract class CopyRequestProcessor
extends HttpRequestProcessor {
@Override
protected final CopyRequest doCreateRequest(String uri)
throws SwiftException {
CopyRequest copy = new CopyRequest();
try {
copy.setURI(new URI(uri));
} catch (URISyntaxException e) {
throw new SwiftException("Failed to create URI from: " + uri);
}
return copy;
}
/**
* The only allowed status code is 201:created.
* @return an array with the permitted status code(s)
*/
@Override
protected int[] getAllowedStatusCodes() {
return new int[]{
SC_CREATED
};
}
}
/**
* Delete operation.
*
* @param
*/
private static abstract class DeleteRequestProcessor
extends HttpRequestProcessor {
@Override
protected final HttpDelete doCreateRequest(String uri) {
return new HttpDelete(uri);
}
@Override
protected int[] getAllowedStatusCodes() {
return new int[]{
SC_OK,
SC_ACCEPTED,
SC_NO_CONTENT,
SC_NOT_FOUND
};
}
}
private static abstract class HeadRequestProcessor
extends HttpRequestProcessor {
@Override
protected final HttpHead doCreateRequest(String uri) {
return new HttpHead(uri);
}
}
/**
* Create a Swift Rest Client instance.
*
* @param filesystemURI filesystem URI
* @param conf The configuration to use to extract the binding
* @throws SwiftConfigurationException the configuration is not valid for
* defining a rest client against the service
*/
private SwiftRestClient(URI filesystemURI,
Configuration conf)
throws SwiftConfigurationException {
Properties props = RestClientBindings.bind(filesystemURI, conf);
String stringAuthUri = getOption(props, SWIFT_AUTH_PROPERTY);
username = getOption(props, SWIFT_USERNAME_PROPERTY);
password = props.getProperty(SWIFT_PASSWORD_PROPERTY);
apiKey = props.getProperty(SWIFT_APIKEY_PROPERTY);
//optional
region = props.getProperty(SWIFT_REGION_PROPERTY);
//tenant is optional
tenant = props.getProperty(SWIFT_TENANT_PROPERTY);
//service is used for diagnostics
serviceProvider = props.getProperty(SWIFT_SERVICE_PROPERTY);
container = props.getProperty(SWIFT_CONTAINER_PROPERTY);
String isPubProp = props.getProperty(SWIFT_PUBLIC_PROPERTY, "false");
usePublicURL = "true".equals(isPubProp);
if (apiKey == null && password == null) {
throw new SwiftConfigurationException(
"Configuration for " + filesystemURI +" must contain either "
+ SWIFT_PASSWORD_PROPERTY + " or "
+ SWIFT_APIKEY_PROPERTY);
}
//create the (reusable) authentication request
if (password != null) {
authRequest = new PasswordAuthenticationRequest(tenant,
new PasswordCredentials(
username,
password));
} else {
authRequest = new ApiKeyAuthenticationRequest(tenant,
new ApiKeyCredentials(
username, apiKey));
keystoneAuthRequest = new KeyStoneAuthRequest(tenant,
new KeystoneApiKeyCredentials(username, apiKey));
}
locationAware = "true".equals(
props.getProperty(SWIFT_LOCATION_AWARE_PROPERTY, "false"));
//now read in properties that are shared across all connections
//connection and retries
try {
retryCount = conf.getInt(SWIFT_RETRY_COUNT, DEFAULT_RETRY_COUNT);
connectTimeout = conf.getInt(SWIFT_CONNECTION_TIMEOUT,
DEFAULT_CONNECT_TIMEOUT);
socketTimeout = conf.getInt(SWIFT_SOCKET_TIMEOUT,
DEFAULT_SOCKET_TIMEOUT);
throttleDelay = conf.getInt(SWIFT_THROTTLE_DELAY,
DEFAULT_THROTTLE_DELAY);
//proxy options
proxyHost = conf.get(SWIFT_PROXY_HOST_PROPERTY);
proxyPort = conf.getInt(SWIFT_PROXY_PORT_PROPERTY, 8080);
blocksizeKB = conf.getInt(SWIFT_BLOCKSIZE,
DEFAULT_SWIFT_BLOCKSIZE);
if (blocksizeKB <= 0) {
throw new SwiftConfigurationException("Invalid blocksize set in "
+ SWIFT_BLOCKSIZE
+ ": " + blocksizeKB);
}
partSizeKB = conf.getInt(SWIFT_PARTITION_SIZE,
DEFAULT_SWIFT_PARTITION_SIZE);
if (partSizeKB <=0) {
throw new SwiftConfigurationException("Invalid partition size set in "
+ SWIFT_PARTITION_SIZE
+ ": " + partSizeKB);
}
bufferSizeKB = conf.getInt(SWIFT_REQUEST_SIZE,
DEFAULT_SWIFT_REQUEST_SIZE);
if (bufferSizeKB <=0) {
throw new SwiftConfigurationException("Invalid buffer size set in "
+ SWIFT_REQUEST_SIZE
+ ": " + bufferSizeKB);
}
} catch (NumberFormatException e) {
//convert exceptions raised parsing ints and longs into
// SwiftConfigurationException instances
throw new SwiftConfigurationException(e.toString(), e);
}
//everything you need for diagnostics. The password is omitted.
serviceDescription = String.format(
"Service={%s} container={%s} uri={%s}"
+ " tenant={%s} user={%s} region={%s}"
+ " publicURL={%b}"
+ " location aware={%b}"
+ " partition size={%d KB}, buffer size={%d KB}"
+ " block size={%d KB}"
+ " connect timeout={%d}, retry count={%d}"
+ " socket timeout={%d}"
+ " throttle delay={%d}"
,
serviceProvider,
container,
stringAuthUri,
tenant,
username,
region != null ? region : "(none)",
usePublicURL,
locationAware,
partSizeKB,
bufferSizeKB,
blocksizeKB,
connectTimeout,
retryCount,
socketTimeout,
throttleDelay
);
if (LOG.isDebugEnabled()) {
LOG.debug(serviceDescription);
}
try {
this.authUri = new URI(stringAuthUri);
} catch (URISyntaxException e) {
throw new SwiftConfigurationException("The " + SWIFT_AUTH_PROPERTY
+ " property was incorrect: "
+ stringAuthUri, e);
}
}
/**
* Get a mandatory configuration option.
*
* @param props property set
* @param key key
* @return value of the configuration
* @throws SwiftConfigurationException if there was no match for the key
*/
private static String getOption(Properties props, String key) throws
SwiftConfigurationException {
String val = props.getProperty(key);
if (val == null) {
throw new SwiftConfigurationException("Undefined property: " + key);
}
return val;
}
/**
* Make an HTTP GET request to Swift to get a range of data in the object.
*
* @param path path to object
* @param offset offset from file beginning
* @param length file length
* @return The input stream -which must be closed afterwards.
* @throws IOException Problems
* @throws SwiftException swift specific error
* @throws FileNotFoundException path is not there
*/
public HttpBodyContent getData(SwiftObjectPath path,
long offset,
long length) throws IOException {
if (offset < 0) {
throw new SwiftException("Invalid offset: " + offset
+ " in getDataAsInputStream( path=" + path
+ ", offset=" + offset
+ ", length =" + length + ")");
}
if (length <= 0) {
throw new SwiftException("Invalid length: " + length
+ " in getDataAsInputStream( path="+ path
+ ", offset=" + offset
+ ", length ="+ length + ")");
}
final String range = String.format(SWIFT_RANGE_HEADER_FORMAT_PATTERN,
offset,
offset + length - 1);
if (LOG.isDebugEnabled()) {
LOG.debug("getData:" + range);
}
return getData(path,
new BasicHeader(HEADER_RANGE, range),
SwiftRestClient.NEWEST);
}
/**
* Returns object length.
*
* @param uri file URI
* @return object length
* @throws SwiftException on swift-related issues
* @throws IOException on network/IO problems
*/
public long getContentLength(URI uri) throws IOException {
preRemoteCommand("getContentLength");
return perform("getContentLength", uri, new HeadRequestProcessor() {
@Override
public Long extractResult(HttpHead req, HttpResponse resp)
throws IOException {
return HttpResponseUtils.getContentLength(resp);
}
@Override
protected void setup(HttpHead req) throws IOException {
super.setup(req);
req.addHeader(NEWEST);
}
});
}
/**
* Get the length of the remote object.
* @param path object to probe
* @return the content length
* @throws IOException on any failure
*/
public long getContentLength(SwiftObjectPath path) throws IOException {
return getContentLength(pathToURI(path));
}
/**
* Get the path contents as an input stream.
* Warning: this input stream must be closed to avoid
* keeping Http connections open.
*
* @param path path to file
* @param requestHeaders http headers
* @return byte[] file data or null if the object was not found
* @throws IOException on IO Faults
* @throws FileNotFoundException if there is nothing at the path
*/
public HttpBodyContent getData(SwiftObjectPath path,
final Header... requestHeaders)
throws IOException {
preRemoteCommand("getData");
return doGet(pathToURI(path),
requestHeaders);
}
/**
* Returns object location as byte[].
*
* @param path path to file
* @param requestHeaders http headers
* @return byte[] file data or null if the object was not found
* @throws IOException on IO Faults
*/
public byte[] getObjectLocation(SwiftObjectPath path,
final Header... requestHeaders) throws IOException {
if (!isLocationAware()) {
//if the filesystem is not location aware, do not ask for this information
return null;
}
preRemoteCommand("getObjectLocation");
try {
return perform("getObjectLocation", pathToObjectLocation(path),
new GetRequestProcessor() {
@Override
protected int[] getAllowedStatusCodes() {
return new int[]{
SC_OK,
SC_FORBIDDEN,
SC_NO_CONTENT
};
}
@Override
public byte[] extractResult(HttpGet req, HttpResponse resp) throws
IOException {
//TODO: remove SC_NO_CONTENT if it depends on Swift versions
int statusCode = resp.getStatusLine().getStatusCode();
if (statusCode == SC_NOT_FOUND
|| statusCode == SC_FORBIDDEN
|| statusCode == SC_NO_CONTENT
|| resp.getEntity().getContent() == null) {
return null;
}
final InputStream responseBodyAsStream =
resp.getEntity().getContent();
final byte[] locationData = new byte[1024];
return responseBodyAsStream.read(locationData) > 0 ?
locationData : null;
}
@Override
protected void setup(HttpGet req)
throws SwiftInternalStateException {
setHeaders(req, requestHeaders);
}
});
} catch (IOException e) {
LOG.warn("Failed to get the location of " + path + ": " + e, e);
return null;
}
}
/**
* Create the URI needed to query the location of an object.
* @param path object path to retrieve information about
* @return the URI for the location operation
* @throws SwiftException if the URI could not be constructed
*/
private URI pathToObjectLocation(SwiftObjectPath path) throws SwiftException {
URI uri;
String dataLocationURI = objectLocationURI.toString();
try {
if (path.toString().startsWith("/")) {
dataLocationURI = dataLocationURI.concat(path.toUriPath());
} else {
dataLocationURI = dataLocationURI.concat("/").concat(path.toUriPath());
}
uri = new URI(dataLocationURI);
} catch (URISyntaxException e) {
throw new SwiftException(e);
}
return uri;
}
/**
* Find objects under a prefix.
*
* @param path path prefix
* @param requestHeaders optional request headers
* @return byte[] file data or null if the object was not found
* @throws IOException on IO Faults
* @throws FileNotFoundException if nothing is at the end of the URI -that is,
* the directory is empty
*/
public byte[] findObjectsByPrefix(SwiftObjectPath path,
final Header... requestHeaders) throws IOException {
preRemoteCommand("findObjectsByPrefix");
URI uri;
String dataLocationURI = getEndpointURI().toString();
try {
String object = path.getObject();
if (object.startsWith("/")) {
object = object.substring(1);
}
object = encodeUrl(object);
dataLocationURI = dataLocationURI.concat("/")
.concat(path.getContainer())
.concat("/?prefix=")
.concat(object)
;
uri = new URI(dataLocationURI);
} catch (URISyntaxException e) {
throw new SwiftException("Bad URI: " + dataLocationURI, e);
}
return perform("findObjectsByPrefix", uri,
new GetRequestProcessor() {
@Override
public byte[] extractResult(HttpGet req, HttpResponse resp)
throws IOException {
if (resp.getStatusLine().getStatusCode() == SC_NOT_FOUND) {
//no result
throw new FileNotFoundException("Not found " + req.getURI());
}
return HttpResponseUtils.getResponseBody(resp);
}
@Override
protected int[] getAllowedStatusCodes() {
return new int[]{
SC_OK,
SC_NOT_FOUND
};
}
@Override
protected void setup(HttpGet req) throws SwiftInternalStateException {
setHeaders(req, requestHeaders);
}
});
}
/**
* Find objects in a directory.
*
* @param path path prefix
* @param requestHeaders optional request headers
* @return byte[] file data or null if the object was not found
* @throws IOException on IO Faults
* @throws FileNotFoundException if nothing is at the end of the URI -that is,
* the directory is empty
*/
public byte[] listDeepObjectsInDirectory(SwiftObjectPath path,
boolean listDeep,
final Header... requestHeaders)
throws IOException {
preRemoteCommand("listDeepObjectsInDirectory");
String endpoint = getEndpointURI().toString();
StringBuilder dataLocationURI = new StringBuilder();
dataLocationURI.append(endpoint);
String object = path.getObject();
if (object.startsWith("/")) {
object = object.substring(1);
}
if (!object.endsWith("/")) {
object = object.concat("/");
}
if (object.equals("/")) {
object = "";
}
dataLocationURI = dataLocationURI.append("/")
.append(path.getContainer())
.append("/?prefix=")
.append(object)
.append("&format=json");
//in listing deep set param to false
if (listDeep == false) {
dataLocationURI.append("&delimiter=/");
}
return findObjects(dataLocationURI.toString(), requestHeaders);
}
/**
* Find objects in a location.
* @param location URI
* @param requestHeaders optional request headers
* @return the body of te response
* @throws IOException IO problems
*/
private byte[] findObjects(String location, final Header[] requestHeaders)
throws IOException {
URI uri;
preRemoteCommand("findObjects");
try {
uri = new URI(location);
} catch (URISyntaxException e) {
throw new SwiftException("Bad URI: " + location, e);
}
return perform("findObjects", uri,
new GetRequestProcessor() {
@Override
public byte[] extractResult(HttpGet req, HttpResponse resp)
throws IOException {
if (resp.getStatusLine().getStatusCode() == SC_NOT_FOUND) {
//no result
throw new FileNotFoundException("Not found " + req.getURI());
}
return HttpResponseUtils.getResponseBody(resp);
}
@Override
protected int[] getAllowedStatusCodes() {
return new int[]{
SC_OK,
SC_NOT_FOUND
};
}
@Override
protected void setup(HttpGet req) throws SwiftInternalStateException {
setHeaders(req, requestHeaders);
}
});
}
/**
* Copy an object. This is done by sending a COPY method to the filesystem
* which is required to handle this WebDAV-level extension to the
* base HTTP operations.
*
* @param src source path
* @param dst destination path
* @param headers any headers
* @return true if the status code was considered successful
* @throws IOException on IO Faults
*/
public boolean copyObject(SwiftObjectPath src, final SwiftObjectPath dst,
final Header... headers) throws IOException {
preRemoteCommand("copyObject");
return perform("copy", pathToURI(src),
new CopyRequestProcessor() {
@Override
public Boolean extractResult(CopyRequest req, HttpResponse resp)
throws IOException {
return resp.getStatusLine().getStatusCode() != SC_NOT_FOUND;
}
@Override
protected void setup(CopyRequest req) throws
SwiftInternalStateException {
setHeaders(req, headers);
req.addHeader(HEADER_DESTINATION, dst.toUriPath());
}
});
}
/**
* Uploads file as Input Stream to Swift.
* The data stream will be closed after the request.
*
* @param path path to Swift
* @param data object data
* @param length length of data
* @param requestHeaders http headers
* @throws IOException on IO Faults
*/
public void upload(SwiftObjectPath path,
final InputStream data,
final long length,
final Header... requestHeaders)
throws IOException {
preRemoteCommand("upload");
try {
perform("upload", pathToURI(path), new PutRequestProcessor() {
@Override
public byte[] extractResult(HttpPut req, HttpResponse resp)
throws IOException {
return HttpResponseUtils.getResponseBody(resp);
}
@Override
protected void setup(HttpPut req) throws
SwiftInternalStateException {
req.setEntity(new InputStreamEntity(data, length));
setHeaders(req, requestHeaders);
}
});
} finally {
data.close();
}
}
/**
* Deletes object from swift.
* The result is true if this operation did the deletion.
*
* @param path path to file
* @param requestHeaders http headers
* @throws IOException on IO Faults
*/
public boolean delete(SwiftObjectPath path, final Header... requestHeaders) throws IOException {
preRemoteCommand("delete");
return perform("", pathToURI(path), new DeleteRequestProcessor() {
@Override
public Boolean extractResult(HttpDelete req, HttpResponse resp)
throws IOException {
return resp.getStatusLine().getStatusCode() == SC_NO_CONTENT;
}
@Override
protected void setup(HttpDelete req) throws
SwiftInternalStateException {
setHeaders(req, requestHeaders);
}
});
}
/**
* Issue a head request.
* @param reason reason -used in logs
* @param path path to query
* @param requestHeaders request header
* @return the response headers. This may be an empty list
* @throws IOException IO problems
* @throws FileNotFoundException if there is nothing at the end
*/
public Header[] headRequest(String reason,
SwiftObjectPath path,
final Header... requestHeaders)
throws IOException {
preRemoteCommand("headRequest: "+ reason);
return perform(reason, pathToURI(path),
new HeadRequestProcessor() {
@Override
public Header[] extractResult(HttpHead req, HttpResponse resp)
throws IOException {
if (resp.getStatusLine().getStatusCode() == SC_NOT_FOUND) {
throw new FileNotFoundException("Not Found " + req.getURI());
}
return resp.getAllHeaders();
}
@Override
protected void setup(HttpHead req) throws
SwiftInternalStateException {
setHeaders(req, requestHeaders);
}
});
}
/**
* Issue a put request.
* @param path path
* @param requestHeaders optional headers
* @return the HTTP response
* @throws IOException any problem
*/
public int putRequest(SwiftObjectPath path, final Header... requestHeaders)
throws IOException {
preRemoteCommand("putRequest");
return perform(pathToURI(path), new PutRequestProcessor() {
@Override
public Integer extractResult(HttpPut req, HttpResponse resp)
throws IOException {
return resp.getStatusLine().getStatusCode();
}
@Override
protected void setup(HttpPut req) throws
SwiftInternalStateException {
setHeaders(req, requestHeaders);
}
});
}
/**
* Authenticate to Openstack Keystone.
* As well as returning the access token, the member fields {@link #token},
* {@link #endpointURI} and {@link #objectLocationURI} are set up for re-use.
*
* This method is re-entrant -if more than one thread attempts to authenticate
* neither will block -but the field values with have those of the last caller.
*
* @return authenticated access token
*/
public AccessToken authenticate() throws IOException {
final AuthenticationRequest authenticationRequest;
if (useKeystoneAuthentication) {
authenticationRequest = keystoneAuthRequest;
} else {
authenticationRequest = authRequest;
}
LOG.debug("started authentication");
return perform("authentication",
authUri,
new AuthenticationPost(authenticationRequest));
}
private final class AuthenticationPost extends
AuthRequestProcessor {
final AuthenticationRequest authenticationRequest;
private AuthenticationPost(AuthenticationRequest authenticationRequest) {
this.authenticationRequest = authenticationRequest;
}
@Override
protected void setup(AuthPostRequest req) throws IOException {
req.setEntity(getAuthenticationRequst(authenticationRequest));
}
/**
* specification says any of the 2xxs are OK, so list all
* the standard ones
* @return a set of 2XX status codes.
*/
@Override
protected int[] getAllowedStatusCodes() {
return new int[]{
SC_OK,
SC_BAD_REQUEST,
SC_CREATED,
SC_ACCEPTED,
SC_NON_AUTHORITATIVE_INFORMATION,
SC_NO_CONTENT,
SC_RESET_CONTENT,
SC_PARTIAL_CONTENT,
SC_MULTI_STATUS,
SC_UNAUTHORIZED //if request unauthorized, try another method
};
}
@Override
public AccessToken extractResult(AuthPostRequest req, HttpResponse resp)
throws IOException {
//initial check for failure codes leading to authentication failures
if (resp.getStatusLine().getStatusCode() == SC_BAD_REQUEST) {
throw new SwiftAuthenticationFailedException(
authenticationRequest.toString(), "POST", authUri, resp);
}
final AuthenticationResponse access =
JSONUtil.toObject(HttpResponseUtils.getResponseBodyAsString(resp),
AuthenticationWrapper.class).getAccess();
final List serviceCatalog = access.getServiceCatalog();
//locate the specific service catalog that defines Swift; variations
//in the name of this add complexity to the search
StringBuilder catList = new StringBuilder();
StringBuilder regionList = new StringBuilder();
//these fields are all set together at the end of the operation
URI endpointURI = null;
URI objectLocation;
Endpoint swiftEndpoint = null;
AccessToken accessToken;
for (Catalog catalog : serviceCatalog) {
String name = catalog.getName();
String type = catalog.getType();
String descr = String.format("[%s: %s]; ", name, type);
catList.append(descr);
if (LOG.isDebugEnabled()) {
LOG.debug("Catalog entry " + descr);
}
if (name.equals(SERVICE_CATALOG_SWIFT)
|| name.equals(SERVICE_CATALOG_CLOUD_FILES)
|| type.equals(SERVICE_CATALOG_OBJECT_STORE)) {
//swift is found
if (LOG.isDebugEnabled()) {
LOG.debug("Found swift catalog as " + name + " => " + type);
}
//now go through the endpoints
for (Endpoint endpoint : catalog.getEndpoints()) {
String endpointRegion = endpoint.getRegion();
URI publicURL = endpoint.getPublicURL();
URI internalURL = endpoint.getInternalURL();
descr = String.format("[%s => %s / %s]; ",
endpointRegion,
publicURL,
internalURL);
regionList.append(descr);
if (LOG.isDebugEnabled()) {
LOG.debug("Endpoint " + descr);
}
if (region == null || endpointRegion.equals(region)) {
endpointURI = usePublicURL ? publicURL : internalURL;
swiftEndpoint = endpoint;
break;
}
}
}
}
if (endpointURI == null) {
String message = "Could not find swift service from auth URL "
+ authUri
+ " and region '" + region + "'. "
+ "Categories: " + catList
+ ((regionList.length() > 0) ?
("regions: " + regionList)
: "No regions");
throw new SwiftInvalidResponseException(message,
SC_OK,
"authenticating",
authUri);
}
accessToken = access.getToken();
String path = SWIFT_OBJECT_AUTH_ENDPOINT
+ swiftEndpoint.getTenantId();
String host = endpointURI.getHost();
try {
objectLocation = new URI(endpointURI.getScheme(),
null,
host,
endpointURI.getPort(),
path,
null,
null);
} catch (URISyntaxException e) {
throw new SwiftException("object endpoint URI is incorrect: "
+ endpointURI
+ " + " + path,
e);
}
setAuthDetails(endpointURI, objectLocation, accessToken);
if (LOG.isDebugEnabled()) {
LOG.debug("authenticated against " + endpointURI);
}
createDefaultContainer();
return accessToken;
}
}
private StringEntity getAuthenticationRequst(
AuthenticationRequest authenticationRequest) throws IOException {
final String data = JSONUtil.toJSON(new AuthenticationRequestWrapper(
authenticationRequest));
if (LOG.isDebugEnabled()) {
LOG.debug("Authenticating with " + authenticationRequest);
}
return new StringEntity(data, ContentType.create("application/json",
"UTF-8"));
}
/**
* create default container if it doesn't exist for Hadoop Swift integration.
* non-reentrant, as this should only be needed once.
*
* @throws IOException IO problems.
*/
private synchronized void createDefaultContainer() throws IOException {
createContainer(container);
}
/**
* Create a container -if it already exists, do nothing.
*
* @param containerName the container name
* @throws IOException IO problems
* @throws SwiftBadRequestException invalid container name
* @throws SwiftInvalidResponseException error from the server
*/
public void createContainer(String containerName) throws IOException {
SwiftObjectPath objectPath = new SwiftObjectPath(containerName, "");
try {
//see if the data is there
headRequest("createContainer", objectPath, NEWEST);
} catch (FileNotFoundException ex) {
int status = 0;
try {
status = putRequest(objectPath);
} catch (FileNotFoundException e) {
//triggered by a very bad container name.
//re-insert the 404 result into the status
status = SC_NOT_FOUND;
}
if (status == SC_BAD_REQUEST) {
throw new SwiftBadRequestException(
"Bad request -authentication failure or bad container name?",
status,
"PUT",
null);
}
if (!isStatusCodeExpected(status,
SC_OK,
SC_CREATED,
SC_ACCEPTED,
SC_NO_CONTENT)) {
throw new SwiftInvalidResponseException("Couldn't create container "
+ containerName +
" for storing data in Swift." +
" Try to create container " +
containerName + " manually ",
status,
"PUT",
null);
} else {
throw ex;
}
}
}
/**
* Trigger an initial auth operation if some of the needed
* fields are missing.
*
* @throws IOException on problems
*/
private void authIfNeeded() throws IOException {
if (getEndpointURI() == null) {
authenticate();
}
}
/**
* Pre-execution actions to be performed by methods. Currently this
*
* - Logs the operation at TRACE
* - Authenticates the client -if needed
*
* @throws IOException
*/
private void preRemoteCommand(String operation) throws IOException {
if (LOG.isTraceEnabled()) {
LOG.trace("Executing " + operation);
}
authIfNeeded();
}
/**
* Performs the HTTP request, validates the response code and returns
* the received data. HTTP Status codes are converted into exceptions.
*
* @param uri URI to source
* @param processor HttpMethodProcessor
* @param method
* @param result type
* @return result of HTTP request
* @throws IOException IO problems
* @throws SwiftBadRequestException the status code indicated "Bad request"
* @throws SwiftInvalidResponseException the status code is out of range
* for the action (excluding 404 responses)
* @throws SwiftInternalStateException the internal state of this client
* is invalid
* @throws FileNotFoundException a 404 response was returned
*/
private R perform(URI uri,
HttpRequestProcessor processor)
throws IOException,
SwiftBadRequestException,
SwiftInternalStateException,
SwiftInvalidResponseException,
FileNotFoundException {
return perform("",uri, processor);
}
/**
* Performs the HTTP request, validates the response code and returns
* the received data. HTTP Status codes are converted into exceptions.
* @param reason why is this operation taking place. Used for statistics
* @param uri URI to source
* @param processor HttpMethodProcessor
* @param method
* @param result type
* @return result of HTTP request
* @throws IOException IO problems
* @throws SwiftBadRequestException the status code indicated "Bad request"
* @throws SwiftInvalidResponseException the status code is out of range
* for the action (excluding 404 responses)
* @throws SwiftInternalStateException the internal state of this client
* is invalid
* @throws FileNotFoundException a 404 response was returned
*/
private R perform(String reason, URI uri,
HttpRequestProcessor processor)
throws IOException, SwiftBadRequestException, SwiftInternalStateException,
SwiftInvalidResponseException, FileNotFoundException {
checkNotNull(uri);
checkNotNull(processor);
final M req = processor.createRequest(uri.toString());
req.addHeader(HEADER_USER_AGENT, SWIFT_USER_AGENT);
//retry policy
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
clientBuilder.setRetryHandler(
new DefaultHttpRequestRetryHandler(retryCount, false));
RequestConfig.Builder requestConfigBuilder =
RequestConfig.custom().setConnectTimeout(connectTimeout);
if (proxyHost != null) {
requestConfigBuilder.setProxy(new HttpHost(proxyHost, proxyPort));
}
clientBuilder.setDefaultRequestConfig(requestConfigBuilder.build());
clientBuilder.setDefaultSocketConfig(
SocketConfig.custom().setSoTimeout(socketTimeout).build());
Duration duration = new Duration();
boolean success = false;
HttpResponse resp;
try {
// client should not be closed in this method because
// the connection can be used later
CloseableHttpClient client = clientBuilder.build();
int statusCode = 0;
try {
resp = exec(client, req);
statusCode = checkNotNull(resp.getStatusLine().getStatusCode());
} catch (IOException e) {
//rethrow with extra diagnostics and wiki links
throw ExceptionDiags.wrapException(uri.toString(), req.getMethod(), e);
}
//look at the response and see if it was valid or not.
//Valid is more than a simple 200; even 404 "not found" is considered
//valid -which it is for many methods.
//validate the allowed status code for this operation
int[] allowedStatusCodes = processor.getAllowedStatusCodes();
boolean validResponse = isStatusCodeExpected(statusCode,
allowedStatusCodes);
if (!validResponse) {
IOException ioe = buildException(uri, req, resp, statusCode);
throw ioe;
}
R r = processor.extractResult(req, resp);
success = true;
return r;
} catch (IOException e) {
//release the connection -always
req.releaseConnection();
throw e;
} finally {
duration.finished();
durationStats.add(req.getMethod() + " " + reason, duration, success);
}
}
/**
* Build an exception from a failed operation. This can include generating
* specific exceptions (e.g. FileNotFound), as well as the default
* {@link SwiftInvalidResponseException}.
*
* @param uri URI for operation
* @param resp operation that failed
* @param statusCode status code
* @param method type
* @return an exception to throw
*/
private IOException buildException(
URI uri, M req, HttpResponse resp, int statusCode) {
IOException fault;
//log the failure @debug level
String errorMessage = String.format("Method %s on %s failed, status code: %d," +
" status line: %s",
req.getMethod(),
uri,
statusCode,
resp.getStatusLine()
);
if (LOG.isDebugEnabled()) {
LOG.debug(errorMessage);
}
//send the command
switch (statusCode) {
case SC_NOT_FOUND:
fault = new FileNotFoundException("Operation " + req.getMethod()
+ " on " + uri);
break;
case SC_BAD_REQUEST:
//bad HTTP request
fault = new SwiftBadRequestException("Bad request against " + uri,
req.getMethod(), uri, resp);
break;
case SC_REQUESTED_RANGE_NOT_SATISFIABLE:
//out of range
StringBuilder errorText = new StringBuilder(
resp.getStatusLine().getReasonPhrase());
//get the requested length
Header requestContentLen = req.getFirstHeader(HEADER_CONTENT_LENGTH);
if (requestContentLen != null) {
errorText.append(" requested ").append(requestContentLen.getValue());
}
//and the result
Header availableContentRange = resp.getFirstHeader(HEADER_CONTENT_RANGE);
if (availableContentRange != null) {
errorText.append(" available ")
.append(availableContentRange.getValue());
}
fault = new EOFException(errorText.toString());
break;
case SC_UNAUTHORIZED:
//auth failure; should only happen on the second attempt
fault = new SwiftAuthenticationFailedException(
"Operation not authorized- current access token =" + getToken(),
req.getMethod(),
uri,
resp);
break;
case SwiftProtocolConstants.SC_TOO_MANY_REQUESTS_429:
case SwiftProtocolConstants.SC_THROTTLED_498:
//response code that may mean the client is being throttled
fault = new SwiftThrottledRequestException(
"Client is being throttled: too many requests",
req.getMethod(),
uri,
resp);
break;
default:
//return a generic invalid HTTP response
fault = new SwiftInvalidResponseException(
errorMessage,
req.getMethod(),
uri,
resp);
}
return fault;
}
/**
* Exec a GET request and return the input stream of the response.
*
* @param uri URI to GET
* @param requestHeaders request headers
* @return the input stream. This must be closed to avoid log errors
* @throws IOException
*/
private HttpBodyContent doGet(final URI uri, final Header... requestHeaders) throws IOException {
return perform("", uri, new GetRequestProcessor() {
@Override
public HttpBodyContent extractResult(HttpGet req, HttpResponse resp)
throws IOException {
return new HttpBodyContent(
new HttpInputStreamWithRelease(uri, req, resp),
HttpResponseUtils.getContentLength(resp));
}
@Override
protected void setup(HttpGet req) throws
SwiftInternalStateException {
setHeaders(req, requestHeaders);
}
});
}
/**
* Create an instance against a specific FS URI.
*
* @param filesystemURI filesystem to bond to
* @param config source of configuration data
* @return REST client instance
* @throws IOException on instantiation problems
*/
public static SwiftRestClient getInstance(URI filesystemURI,
Configuration config) throws IOException {
return new SwiftRestClient(filesystemURI, config);
}
/**
* Converts Swift path to URI to make request.
* This is public for unit testing
*
* @param path path to object
* @param endpointURI domain url e.g. http://domain.com
* @return valid URI for object
* @throws SwiftException
*/
public static URI pathToURI(SwiftObjectPath path,
URI endpointURI) throws SwiftException {
checkNotNull(endpointURI, "Null Endpoint -client is not authenticated");
String dataLocationURI = endpointURI.toString();
try {
dataLocationURI = SwiftUtils.joinPaths(dataLocationURI, encodeUrl(path.toUriPath()));
return new URI(dataLocationURI);
} catch (URISyntaxException e) {
throw new SwiftException("Failed to create URI from " + dataLocationURI, e);
}
}
/**
* Encode the URL. This extends {@link URLEncoder#encode(String, String)}
* with a replacement of + with %20.
* @param url URL string
* @return an encoded string
* @throws SwiftException if the URL cannot be encoded
*/
private static String encodeUrl(String url) throws SwiftException {
if (url.matches(".*\\s+.*")) {
try {
url = URLEncoder.encode(url, "UTF-8");
url = url.replace("+", "%20");
} catch (UnsupportedEncodingException e) {
throw new SwiftException("failed to encode URI", e);
}
}
return url;
}
/**
* Convert a swift path to a URI relative to the current endpoint.
*
* @param path path
* @return an path off the current endpoint URI.
* @throws SwiftException
*/
private URI pathToURI(SwiftObjectPath path) throws SwiftException {
return pathToURI(path, getEndpointURI());
}
/**
* Add the headers to the method, and the auth token (which must be set).
* @param method method to update
* @param requestHeaders the list of headers
* @throws SwiftInternalStateException not yet authenticated
*/
private void setHeaders(HttpUriRequest method, Header[] requestHeaders)
throws SwiftInternalStateException {
for (Header header : requestHeaders) {
method.addHeader(header);
}
setAuthToken(method, getToken());
}
/**
* Set the auth key header of the method to the token ID supplied.
*
* @param method method
* @param accessToken access token
* @throws SwiftInternalStateException if the client is not yet authenticated
*/
private void setAuthToken(HttpUriRequest method, AccessToken accessToken)
throws SwiftInternalStateException {
checkNotNull(accessToken,"Not authenticated");
method.addHeader(HEADER_AUTH_KEY, accessToken.getId());
}
/**
* Execute a method in a new HttpClient instance. If the auth failed,
* authenticate then retry the method.
*
* @param req request to exec
* @param client client to use
* @param Request type
* @return the status code
* @throws IOException on any failure
*/
private HttpResponse exec(HttpClient client, M req)
throws IOException {
HttpResponse resp = execWithDebugOutput(req, client);
int statusCode = resp.getStatusLine().getStatusCode();
if ((statusCode == HttpStatus.SC_UNAUTHORIZED
|| statusCode == HttpStatus.SC_BAD_REQUEST)
&& req instanceof AuthPostRequest
&& !useKeystoneAuthentication) {
if (LOG.isDebugEnabled()) {
LOG.debug("Operation failed with status " + statusCode
+ " attempting keystone auth");
}
//if rackspace key authentication failed - try custom Keystone authentication
useKeystoneAuthentication = true;
final AuthPostRequest authentication = (AuthPostRequest) req;
//replace rackspace auth with keystone one
authentication.setEntity(getAuthenticationRequst(keystoneAuthRequest));
resp = execWithDebugOutput(req, client);
}
if (statusCode == HttpStatus.SC_UNAUTHORIZED ) {
//unauthed -or the auth uri rejected it.
if (req instanceof AuthPostRequest) {
//unauth response from the AUTH URI itself.
throw new SwiftAuthenticationFailedException(authRequest.toString(),
"auth",
authUri,
resp);
}
//any other URL: try again
if (LOG.isDebugEnabled()) {
LOG.debug("Reauthenticating");
}
//re-auth, this may recurse into the same dir
authenticate();
if (LOG.isDebugEnabled()) {
LOG.debug("Retrying original request");
}
resp = execWithDebugOutput(req, client);
}
return resp;
}
/**
* Execute the request with the request and response logged at debug level.
* @param req request to execute
* @param client client to use
* @param method type
* @return the status code
* @throws IOException any failure reported by the HTTP client.
*/
private HttpResponse execWithDebugOutput(M req,
HttpClient client) throws IOException {
if (LOG.isDebugEnabled()) {
StringBuilder builder = new StringBuilder(
req.getMethod() + " " + req.getURI() + "\n");
for (Header header : req.getAllHeaders()) {
builder.append(header.toString());
}
LOG.debug(builder.toString());
}
HttpResponse resp = client.execute(req);
if (LOG.isDebugEnabled()) {
LOG.debug("Status code = " + resp.getStatusLine().getStatusCode());
}
return resp;
}
/**
* Ensures that an object reference passed as a parameter to the calling
* method is not null.
*
* @param reference an object reference
* @return the non-null reference that was validated
* @throws NullPointerException if {@code reference} is null
*/
private static T checkNotNull(T reference) throws
SwiftInternalStateException {
return checkNotNull(reference, "Null Reference");
}
private static T checkNotNull(T reference, String message) throws
SwiftInternalStateException {
if (reference == null) {
throw new SwiftInternalStateException(message);
}
return reference;
}
/**
* Check for a status code being expected -takes a list of expected values
*
* @param status received status
* @param expected expected value
* @return true if status is an element of [expected]
*/
private boolean isStatusCodeExpected(int status, int... expected) {
for (int code : expected) {
if (status == code) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "Swift client: " + serviceDescription;
}
/**
* Get the region which this client is bound to
* @return the region
*/
public String getRegion() {
return region;
}
/**
* Get the tenant to which this client is bound
* @return the tenant
*/
public String getTenant() {
return tenant;
}
/**
* Get the username this client identifies itself as
* @return the username
*/
public String getUsername() {
return username;
}
/**
* Get the container to which this client is bound
* @return the container
*/
public String getContainer() {
return container;
}
/**
* Is this client bound to a location aware Swift blobstore
* -that is, can you query for the location of partitions?
* @return true iff the location of multipart file uploads
* can be determined.
*/
public boolean isLocationAware() {
return locationAware;
}
/**
* Get the blocksize of this filesystem
* @return a blocksize > 0
*/
public long getBlocksizeKB() {
return blocksizeKB;
}
/**
* Get the partition size in KB.
* @return the partition size
*/
public int getPartSizeKB() {
return partSizeKB;
}
/**
* Get the buffer size in KB.
* @return the buffer size wanted for reads
*/
public int getBufferSizeKB() {
return bufferSizeKB;
}
public int getProxyPort() {
return proxyPort;
}
public String getProxyHost() {
return proxyHost;
}
public int getRetryCount() {
return retryCount;
}
public int getConnectTimeout() {
return connectTimeout;
}
public boolean isUsePublicURL() {
return usePublicURL;
}
public int getThrottleDelay() {
return throttleDelay;
}
/**
* Get the current operation statistics.
* @return a snapshot of the statistics
*/
public List getOperationStatistics() {
return durationStats.getDurationStatistics();
}
}