All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.hadoop.fs.swift.http.SwiftRestClient Maven / Gradle / Ivy

Go to download

This module used to contain code to support integration with OpenStack. It has been deleted as unsupported; the JAR is still published so as to not break applications which declare an explicit maven/ivy/SBT dependency on the module.

There is a newer version: 3.4.1
Show newest version
/**
 * 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.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpHost;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpConnectionParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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 org.apache.http.conn.params.ConnRoutePNames;

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.commons.httpclient.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 authenticationg 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 Log LOG = LogFactory.getLog(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 Header(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  method
   * @param  result
   */
  private static abstract class HttpMethodProcessor {
    public final M createMethod(String uri) throws IOException {
      final M method = doCreateMethod(uri);
      setup(method);
      return method;
    }

    /**
     * Override it to return some result after method is executed.
     */
    public abstract R extractResult(M method) throws IOException;

    /**
     * Factory method to create a REST method against the given URI
     *
     * @param uri target
     * @return method to invoke
     */
    protected abstract M doCreateMethod(String uri);

    /**
     * Override port to set up the method before it is executed.
     */
    protected void setup(M method) 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 GetMethodProcessor extends HttpMethodProcessor {
    @Override
    protected final GetMethod doCreateMethod(String uri) {
      return new GetMethod(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 class AuthPostMethod extends PostMethod {


    private AuthPostMethod(String uri) {
      super(uri);
    }
  }

  /**
   * Generate an auth message
   * @param  response
   */
  private static abstract class AuthMethodProcessor extends
                                                       HttpMethodProcessor {
    @Override
    protected final AuthPostMethod doCreateMethod(String uri) {
      return new AuthPostMethod(uri);
    }
  }

  private static abstract class PutMethodProcessor extends HttpMethodProcessor {
    @Override
    protected final PutMethod doCreateMethod(String uri) {
      return new PutMethod(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 
   */
  private static abstract class CopyMethodProcessor extends HttpMethodProcessor {
    @Override
    protected final CopyMethod doCreateMethod(String uri) {
      return new CopyMethod(uri);
    }

    /**
     * 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 DeleteMethodProcessor extends HttpMethodProcessor {
    @Override
    protected final DeleteMethod doCreateMethod(String uri) {
      return new DeleteMethod(uri);
    }

    @Override
    protected int[] getAllowedStatusCodes() {
      return new int[]{
              SC_OK,
              SC_ACCEPTED,
              SC_NO_CONTENT,
              SC_NOT_FOUND
      };
    }
  }

  private static abstract class HeadMethodProcessor extends HttpMethodProcessor {
    @Override
    protected final HeadMethod doCreateMethod(String uri) {
      return new HeadMethod(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 Header(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 HeadMethodProcessor() {
      @Override
      public Long extractResult(HeadMethod method) throws IOException {
        return method.getResponseContentLength();
      }

      @Override
      protected void setup(HeadMethod method) throws IOException {
        super.setup(method);
        method.addRequestHeader(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 GetMethodProcessor() {
                @Override
                protected int[] getAllowedStatusCodes() {
                  return new int[]{
                    SC_OK,
                    SC_FORBIDDEN,
                    SC_NO_CONTENT
                  };
                }

                @Override
                public byte[] extractResult(GetMethod method) throws
                        IOException {

                  //TODO: remove SC_NO_CONTENT if it depends on Swift versions
                  if (method.getStatusCode() == SC_NOT_FOUND
                      || method.getStatusCode() == SC_FORBIDDEN ||
                          method.getStatusCode() == SC_NO_CONTENT ||
                          method.getResponseBodyAsStream() == null) {
                    return null;
                  }
                  final InputStream responseBodyAsStream = method.getResponseBodyAsStream();
                  final byte[] locationData = new byte[1024];

                  return responseBodyAsStream.read(locationData) > 0 ? locationData : null;
                }

                @Override
                     protected void setup(GetMethod method)
                       throws SwiftInternalStateException {
                  setHeaders(method, 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 GetMethodProcessor() {
      @Override
      public byte[] extractResult(GetMethod method) throws IOException {
        if (method.getStatusCode() == SC_NOT_FOUND) {
          //no result
          throw new FileNotFoundException("Not found " + method.getURI());
        }
        return method.getResponseBody();
      }

      @Override
      protected int[] getAllowedStatusCodes() {
        return new int[]{
                SC_OK,
                SC_NOT_FOUND
        };
      }

      @Override
      protected void setup(GetMethod method) throws
                     SwiftInternalStateException {
        setHeaders(method, 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 GetMethodProcessor() {
      @Override
      public byte[] extractResult(GetMethod method) throws IOException {
        if (method.getStatusCode() == SC_NOT_FOUND) {
          //no result
          throw new FileNotFoundException("Not found " + method.getURI());
        }
        return method.getResponseBody();
      }

      @Override
      protected int[] getAllowedStatusCodes() {
        return new int[]{
                SC_OK,
                SC_NOT_FOUND
        };
      }

      @Override
      protected void setup(GetMethod method)
        throws SwiftInternalStateException {
        setHeaders(method, 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 CopyMethodProcessor() {
      @Override
      public Boolean extractResult(CopyMethod method) throws IOException {
        return method.getStatusCode() != SC_NOT_FOUND;
      }

      @Override
      protected void setup(CopyMethod method) throws
                     SwiftInternalStateException {
        setHeaders(method, headers);
        method.addRequestHeader(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 PutMethodProcessor() {
        @Override
        public byte[] extractResult(PutMethod method) throws IOException {
          return method.getResponseBody();
        }

        @Override
        protected void setup(PutMethod method) throws
                        SwiftInternalStateException {
          method.setRequestEntity(new InputStreamRequestEntity(data, length));
          setHeaders(method, 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 DeleteMethodProcessor() {
      @Override
      public Boolean extractResult(DeleteMethod method) throws IOException {
        return method.getStatusCode() == SC_NO_CONTENT;
      }

      @Override
      protected void setup(DeleteMethod method) throws
                    SwiftInternalStateException {
        setHeaders(method, 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 HeadMethodProcessor() {
      @Override
      public Header[] extractResult(HeadMethod method) throws IOException {
        if (method.getStatusCode() == SC_NOT_FOUND) {
          throw new FileNotFoundException("Not Found " + method.getURI());
        }

        return method.getResponseHeaders();
      }

      @Override
      protected void setup(HeadMethod method) throws
                    SwiftInternalStateException {
        setHeaders(method, 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 PutMethodProcessor() {

      @Override
      public Integer extractResult(PutMethod method) throws IOException {
        return method.getStatusCode();
      }

      @Override
      protected void setup(PutMethod method) throws
                    SwiftInternalStateException {
        setHeaders(method, 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 class AuthenticationPost extends AuthMethodProcessor { final AuthenticationRequest authenticationRequest; private AuthenticationPost(AuthenticationRequest authenticationRequest) { this.authenticationRequest = authenticationRequest; } @Override protected void setup(AuthPostMethod method) throws IOException { method.setRequestEntity(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(AuthPostMethod method) throws IOException { //initial check for failure codes leading to authentication failures if (method.getStatusCode() == SC_BAD_REQUEST) { throw new SwiftAuthenticationFailedException( authenticationRequest.toString(), "POST", authUri, method); } final AuthenticationResponse access = JSONUtil.toObject(method.getResponseBodyAsString(), 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 StringRequestEntity getAuthenticationRequst(AuthenticationRequest authenticationRequest) throws IOException { final String data = JSONUtil.toJSON(new AuthenticationRequestWrapper( authenticationRequest)); if (LOG.isDebugEnabled()) { LOG.debug("Authenticating with " + authenticationRequest); } return toJsonEntity(data); } /** * 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, HttpMethodProcessor 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, HttpMethodProcessor processor) throws IOException, SwiftBadRequestException, SwiftInternalStateException, SwiftInvalidResponseException, FileNotFoundException { checkNotNull(uri); checkNotNull(processor); final M method = processor.createMethod(uri.toString()); //retry policy HttpMethodParams methodParams = method.getParams(); methodParams.setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler( retryCount, false)); methodParams.setIntParameter(HttpConnectionParams.CONNECTION_TIMEOUT, connectTimeout); methodParams.setSoTimeout(socketTimeout); method.addRequestHeader(HEADER_USER_AGENT, SWIFT_USER_AGENT); Duration duration = new Duration(); boolean success = false; try { int statusCode = 0; try { statusCode = exec(method); } catch (IOException e) { //rethrow with extra diagnostics and wiki links throw ExceptionDiags.wrapException(uri.toString(), method.getName(), 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, method, statusCode); throw ioe; } R r = processor.extractResult(method); success = true; return r; } catch (IOException e) { //release the connection -always method.releaseConnection(); throw e; } finally { duration.finished(); durationStats.add(method.getName()+" " + 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 method operation that failed * @param statusCode status code * @param method type * @return an exception to throw */ private IOException buildException(URI uri, M method, int statusCode) { IOException fault; //log the failure @debug level String errorMessage = String.format("Method %s on %s failed, status code: %d," + " status line: %s", method.getName(), uri, statusCode, method.getStatusLine() ); if (LOG.isDebugEnabled()) { LOG.debug(errorMessage); } //send the command switch (statusCode) { case SC_NOT_FOUND: fault = new FileNotFoundException("Operation " + method.getName() + " on " + uri); break; case SC_BAD_REQUEST: //bad HTTP request fault = new SwiftBadRequestException( "Bad request against " + uri, method.getName(), uri, method); break; case SC_REQUESTED_RANGE_NOT_SATISFIABLE: //out of range StringBuilder errorText = new StringBuilder(method.getStatusText()); //get the requested length Header requestContentLen = method.getRequestHeader(HEADER_CONTENT_LENGTH); if (requestContentLen!=null) { errorText.append(" requested ").append(requestContentLen.getValue()); } //and the result Header availableContentRange = method.getResponseHeader( 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(), method.getName(), uri, method); 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", method.getName(), uri, method); break; default: //return a generic invalid HTTP response fault = new SwiftInvalidResponseException( errorMessage, method.getName(), uri, method); } 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 GetMethodProcessor() { @Override public HttpBodyContent extractResult(GetMethod method) throws IOException { return new HttpBodyContent( new HttpInputStreamWithRelease(uri, method), method.getResponseContentLength() ); } @Override protected void setup(GetMethod method) throws SwiftInternalStateException { setHeaders(method, 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); } /** * Convert the (JSON) data to a string request as UTF-8 * * @param data data * @return the data * @throws SwiftException if for some very unexpected reason it's impossible * to convert the data to UTF-8. */ private static StringRequestEntity toJsonEntity(String data) throws SwiftException { StringRequestEntity entity; try { entity = new StringRequestEntity(data, "application/json", "UTF-8"); } catch (UnsupportedEncodingException e) { throw new SwiftException("Could not encode data as UTF-8", e); } return entity; } /** * Converts Swift path to URI to make request. * This is public for unit testing * * @param path path to object * @param endpointURI damain 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(HttpMethodBase method, Header[] requestHeaders) throws SwiftInternalStateException { for (Header header : requestHeaders) { method.addRequestHeader(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(HttpMethodBase method, AccessToken accessToken) throws SwiftInternalStateException { checkNotNull(accessToken,"Not authenticated"); method.addRequestHeader(HEADER_AUTH_KEY, accessToken.getId()); } /** * Execute a method in a new HttpClient instance. * If the auth failed, authenticate then retry the method. * * @param method methot to exec * @param Method type * @return the status code * @throws IOException on any failure */ private int exec(M method) throws IOException { final HttpClient client = new HttpClient(); if (proxyHost != null) { client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, new HttpHost(proxyHost, proxyPort)); } int statusCode = execWithDebugOutput(method, client); if ((statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_BAD_REQUEST) && method instanceof AuthPostMethod && !useKeystoneAuthentication) { if (LOG.isDebugEnabled()) { LOG.debug("Operation failed with status " + method.getStatusCode() + " attempting keystone auth"); } //if rackspace key authentication failed - try custom Keystone authentication useKeystoneAuthentication = true; final AuthPostMethod authentication = (AuthPostMethod) method; //replace rackspace auth with keystone one authentication.setRequestEntity(getAuthenticationRequst(keystoneAuthRequest)); statusCode = execWithDebugOutput(method, client); } if (statusCode == HttpStatus.SC_UNAUTHORIZED ) { //unauthed -or the auth uri rejected it. if (method instanceof AuthPostMethod) { //unauth response from the AUTH URI itself. throw new SwiftAuthenticationFailedException(authRequest.toString(), "auth", authUri, method); } //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"); } statusCode = execWithDebugOutput(method, client); } return statusCode; } /** * Execute the request with the request and response logged at debug level * @param method method to execute * @param client client to use * @param method type * @return the status code * @throws IOException any failure reported by the HTTP client. */ private int execWithDebugOutput(M method, HttpClient client) throws IOException { if (LOG.isDebugEnabled()) { StringBuilder builder = new StringBuilder( method.getName() + " " + method.getURI() + "\n"); for (Header header : method.getRequestHeaders()) { builder.append(header.toString()); } LOG.debug(builder); } int statusCode = client.executeMethod(method); if (LOG.isDebugEnabled()) { LOG.debug("Status code = " + statusCode); } return statusCode; } /** * 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 iff 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(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy