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

info.freelibrary.vertx.s3.S3Client Maven / Gradle / Ivy

There is a newer version: 2.0.0-alpha-2
Show newest version

package info.freelibrary.vertx.s3;

import static info.freelibrary.util.Constants.EOL;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;

import javax.xml.transform.TransformerException;

import info.freelibrary.util.HTTP;
import info.freelibrary.util.Logger;
import info.freelibrary.util.LoggerFactory;
import info.freelibrary.util.StringUtils;
import info.freelibrary.util.XmlUtils;

import info.freelibrary.vertx.s3.util.MessageCodes;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.AsyncFile;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpConnection;
import io.vertx.core.http.HttpMethod;

/**
 * An S3 client implementation.
 */
@SuppressWarnings("PMD.TooManyMethods")
public class S3Client {

    /**
     * The S3 client logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(S3Client.class, MessageCodes.BUNDLE);

    /**
     * The S3 list command.
     */
    private static final String LIST_CMD = "?list-type=2";

    /**
     * The S3 list command with a prefix.
     */
    private static final String PREFIX_LIST_CMD = "?list-type=2&prefix=";

    /**
     * A URL request template.
     */
    private static final String REQUEST = "/{}/{}";

    /**
     * AWS credentials.
     */
    private final AwsCredentials myCredentials;

    /**
     * HTTP client used to interact with S3.
     */
    private final HttpClient myHttpClient;

    /**
     * The internal Vert.x instance.
     */
    private final Vertx myVertx;

    /**
     * Creates a new S3 client using system defined AWS credentials and the default S3 endpoint.
     *
     * @param aVertx A Vert.x instance from which to create the HttpClient
     */
    public S3Client(final Vertx aVertx) {
        myCredentials = new AwsCredentialsProviderChain().getCredentials();
        myHttpClient = getHttpClient(aVertx, (S3ClientOptions) null);
        myVertx = aVertx;
    }

    /**
     * Creates a new S3 client using system defined AWS credentials and the supplied HttpClient options.
     *
     * @param aVertx A Vert.x instance from which to create the S3 client
     * @param aConfig A configuration for the internal HttpClient
     */
    public S3Client(final Vertx aVertx, final S3ClientOptions aConfig) {
        final Optional credentials = aConfig.getCredentials();

        if (credentials.isPresent()) {
            myCredentials = credentials.get();
        } else {
            myCredentials = new AwsCredentialsProviderChain().getCredentials();
        }

        myHttpClient = getHttpClient(aVertx, aConfig);
        myVertx = aVertx;
    }

    /**
     * Deletes the S3 resource represented by the supplied key.
     *
     * @param aBucket A bucket from which to delete the object
     * @param aKey The S3 key of the object to delete
     * @return A future indicating the success of the deletion
     */
    public Future delete(final String aBucket, final String aKey) {
        return createDeleteRequest(aBucket, aKey).compose(request -> request.send().compose(response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.NO_CONTENT) {
                return Future.succeededFuture();
            }

            return Future.failedFuture(new UnexpectedStatusException(statusCode, response.statusMessage()));
        }));
    }

    /**
     * Deletes the S3 resource represented by the supplied key.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @param aHandler A response handler
     */
    public void delete(final String aBucket, final String aKey, final Handler> aHandler) {
        final Promise promise = Promise.promise();

        // Set the supplied handler as the handler for our response promise
        promise.future().onComplete(aHandler);

        createDeleteRequest(aBucket, aKey).onComplete(deleteRequest -> {
            if (deleteRequest.succeeded()) {
                deleteRequest.result().response(send -> {
                    if (send.succeeded()) {
                        final HttpClientResponse response = send.result();
                        final int statusCode = response.statusCode();

                        if (statusCode == HTTP.NO_CONTENT) {
                            promise.complete();
                        } else {
                            promise.fail(new UnexpectedStatusException(statusCode, response.statusMessage()));
                        }
                    } else {
                        promise.fail(send.cause());
                    }
                }).exceptionHandler(error -> {
                    promise.fail(error);
                }).send();
            } else {
                promise.fail(deleteRequest.cause());
            }
        });
    }

    /**
     * Gets an object, represented by the supplied key, from an S3 bucket.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @return A future indicating the success or failure of the GET
     */
    public Future get(final String aBucket, final String aKey) {
        return createGetRequest(aBucket, aKey).compose(request -> request.send().compose(response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                return Future.succeededFuture(new S3ClientResponseImpl(response));
            }

            return Future.failedFuture(new UnexpectedStatusException(statusCode, response.statusMessage()));
        }));
    }

    /**
     * Gets an object, represented by the supplied key, from an S3 bucket.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @param aHandler A response handler
     */
    public void get(final String aBucket, final String aKey, final Handler> aHandler) {
        final Promise promise = Promise.promise();

        // Set the supplied handler as the handler for our response promise
        promise.future().onComplete(aHandler);

        createGetRequest(aBucket, aKey).onComplete(getRequest -> {
            if (getRequest.succeeded()) {
                getRequest.result().response(send -> {
                    if (send.succeeded()) {
                        final HttpClientResponse response = send.result();
                        final int statusCode = response.statusCode();

                        if (statusCode == HTTP.OK) {
                            promise.complete(new S3ClientResponseImpl(response));
                        } else {
                            promise.fail(new UnexpectedStatusException(statusCode, response.statusMessage()));
                        }
                    } else {
                        promise.fail(send.cause());
                    }
                }).exceptionHandler(error -> {
                    promise.fail(error);
                }).send();
            } else {
                promise.fail(getRequest.cause());
            }
        });
    }

    /**
     * Performs a HEAD request on an object in S3.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @return A future with the response headers
     */
    public Future head(final String aBucket, final String aKey) {
        return createHeadRequest(aBucket, aKey).compose(request -> request.send().compose(response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                return Future.succeededFuture(new HttpHeaders(response.headers()));
            }

            return Future.failedFuture(new UnexpectedStatusException(statusCode, response.statusMessage()));
        }));
    }

    /**
     * Performs a HEAD request on an object in S3.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @param aHandler A response handler
     */
    public void head(final String aBucket, final String aKey, final Handler> aHandler) {
        final Promise promise = Promise.promise();

        // Set the supplied handler as the handler for our response promise
        promise.future().onComplete(aHandler);

        createHeadRequest(aBucket, aKey).onComplete(headRequest -> {
            if (headRequest.succeeded()) {
                headRequest.result().response(send -> {
                    if (send.succeeded()) {
                        final HttpClientResponse response = send.result();
                        final int statusCode = response.statusCode();

                        if (statusCode == HTTP.OK) {
                            promise.complete(new HttpHeaders(response.headers()));
                        } else {
                            promise.fail(new UnexpectedStatusException(statusCode, response.statusMessage()));
                        }
                    } else {
                        promise.fail(send.cause());
                    }
                }).exceptionHandler(error -> {
                    promise.fail(error);
                }).send();
            } else {
                promise.fail(headRequest.cause());
            }
        });
    }

    /**
     * Performs a list request on an S3 bucket.
     *
     * @param aBucket An S3 bucket
     * @return A future with the list response
     */
    public Future list(final String aBucket) {
        return createGetRequest(aBucket, LIST_CMD).compose(request -> request.send().compose(response -> {
            final Promise promise = Promise.promise();

            // Builds an S3BucketList from the response and completes the promise with it
            buildList(response, promise);

            return promise.future();
        }));
    }

    /**
     * Lists an S3 bucket.
     *
     * @param aBucket A bucket from which to get a listing
     * @param aHandler A response handler
     */
    public void list(final String aBucket, final Handler> aHandler) {
        final Promise promise = Promise.promise();

        // Set the supplied handler as the handler for our response promise
        promise.future().onComplete(aHandler);

        createGetRequest(aBucket, LIST_CMD).onComplete(getRequest -> {
            if (getRequest.succeeded()) {
                getRequest.result().response(send -> {
                    if (send.succeeded()) {
                        // Builds an S3BucketList from the response and completes the promise with it
                        buildList(send.result(), promise);
                    } else {
                        promise.fail(send.cause());
                    }
                }).exceptionHandler(error -> {
                    promise.fail(error);
                }).send();
            } else {
                promise.fail(getRequest.cause());
            }
        });
    }

    /**
     * Performs a prefixed LIST request on an S3 bucket.
     *
     * @param aBucket An S3 bucket
     * @param aPrefix A prefix to use to limit which objects are listed
     * @return A future with the LIST results buffer
     */
    public Future list(final String aBucket, final String aPrefix) {
        final String prefixedList = PREFIX_LIST_CMD + aPrefix;
        return createGetRequest(aBucket, prefixedList).compose(request -> request.send().compose(response -> {
            final Promise promise = Promise.promise();

            // Builds an S3BucketList from the response and completes the promise with it
            buildList(response, promise);

            return promise.future();
        }));
    }

    /**
     * Lists an S3 bucket using the supplied prefix as a filter.
     *
     * @param aBucket An S3 bucket
     * @param aPrefix A prefix to use to limit which objects are listed
     * @param aHandler A response handler
     */
    public void list(final String aBucket, final String aPrefix, final Handler> aHandler) {
        final Promise promise = Promise.promise();

        // Set the supplied handler as the handler for our response promise
        promise.future().onComplete(aHandler);

        createGetRequest(aBucket, PREFIX_LIST_CMD + aPrefix).onComplete(getRequest -> {
            if (getRequest.succeeded()) {
                getRequest.result().response(send -> {
                    if (send.succeeded()) {
                        // Builds an S3BucketList from the response and completes the promise with it
                        buildList(send.result(), promise);
                    } else {
                        promise.fail(send.cause());
                    }
                }).exceptionHandler(error -> {
                    promise.fail(error);
                }).send();
            } else {
                promise.fail(getRequest.cause());
            }
        });
    }

    /**
     * Puts a buffer into an S3 bucket.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 object key
     * @param aBuffer A buffer to PUT
     * @return A future indicating when the buffer has been uploaded
     */
    public Future put(final String aBucket, final String aKey, final Buffer aBuffer) {
        return createPutRequest(aBucket, aKey).compose(request -> request.send(aBuffer).compose(response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                return Future.succeededFuture(new HttpHeaders(response.headers()));
            }

            return Future.failedFuture(new UnexpectedStatusException(statusCode, response.statusMessage()));
        }));
    }

    /**
     * Uploads contents of the Buffer to S3.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @param aBuffer A data buffer
     * @param aHandler A response handler
     */
    public void put(final String aBucket, final String aKey, final Buffer aBuffer,
            final Handler> aHandler) {
        final Promise promise = Promise.promise();

        // Set the supplied handler as the handler for our response promise
        promise.future().onComplete(aHandler);

        createPutRequest(aBucket, aKey).onComplete(putRequest -> {
            if (putRequest.succeeded()) {
                putRequest.result().response(send -> {
                    if (send.succeeded()) {
                        final HttpClientResponse response = send.result();
                        final int statusCode = response.statusCode();

                        if (statusCode == HTTP.OK) {
                            promise.complete(new HttpHeaders(response.headers()));
                        } else {
                            promise.fail(new UnexpectedStatusException(statusCode, response.statusMessage()));
                        }
                    } else {
                        promise.fail(send.cause());
                    }
                }).exceptionHandler(error -> {
                    promise.fail(error);
                }).send(aBuffer);
            } else {
                promise.fail(putRequest.cause());
            }
        });
    }

    /**
     * Puts a buffer into an S3 bucket.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 object key
     * @param aBuffer A buffer to PUT
     * @param aMetadata A metadata object
     * @return A future indicating when the buffer has been uploaded
     */
    public Future put(final String aBucket, final String aKey, final Buffer aBuffer,
            final UserMetadata aMetadata) {
        final Future future = createPutRequest(aBucket, aKey);
        return future.compose(request -> request.setUserMetadata(aMetadata).send(aBuffer).compose(response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                return Future.succeededFuture(new HttpHeaders(response.headers()));
            }

            return Future.failedFuture(new UnexpectedStatusException(statusCode, response.statusMessage()));
        }));
    }

    /**
     * Puts a buffer into an S3 bucket and sets the supplied user metadata.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @param aBuffer A data buffer
     * @param aMetadata User metadata that should be set on the S3 object
     * @param aHandler An upload response handler
     */
    public void put(final String aBucket, final String aKey, final Buffer aBuffer, final UserMetadata aMetadata,
            final Handler> aHandler) {
        final Promise promise = Promise.promise();

        // Set the supplied handler as the handler for our response promise
        promise.future().onComplete(aHandler);

        createPutRequest(aBucket, aKey).onComplete(putRequest -> {
            if (putRequest.succeeded()) {
                putRequest.result().setUserMetadata(aMetadata).response(send -> {
                    if (send.succeeded()) {
                        final HttpClientResponse response = send.result();
                        final int statusCode = response.statusCode();

                        if (statusCode == HTTP.OK) {
                            promise.complete(new HttpHeaders(response.headers()));
                        } else {
                            promise.fail(new UnexpectedStatusException(statusCode, response.statusMessage()));
                        }
                    } else {
                        promise.fail(send.cause());
                    }
                }).exceptionHandler(error -> {
                    promise.fail(error);
                }).send(aBuffer);
            } else {
                promise.fail(putRequest.cause());
            }
        });
    }

    /**
     * Uploads the file contents to S3. You are responsible for closing the AsyncFile when you're done with it.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 object key
     * @param aFile A file from which to read the buffer
     * @return A future indicating when the buffer has been uploaded
     */
    @SuppressWarnings("PMD.CognitiveComplexity")
    public Future put(final String aBucket, final String aKey, final AsyncFile aFile) {
        return createPutRequest(aBucket, aKey).compose(request -> request.send(aFile).compose(response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                return Future.succeededFuture(new HttpHeaders(response.headers()));
            }

            // Log more details if we get an unexpected result
            response.body(body -> {
                if (body.succeeded()) {
                    try {
                        // Additional details are wrapped in an XML wrapper
                        final String xml = body.result().toString(StandardCharsets.UTF_8);

                        try {
                            LOGGER.error(MessageCodes.VSS_021, aKey, EOL + XmlUtils.format(xml));
                        } catch (final TransformerException details) {
                            LOGGER.error(MessageCodes.VSS_021, aKey, xml);
                        }
                    } catch (final Exception details) { // NOPMD
                        LOGGER.error(MessageCodes.VSS_022, details.getMessage());
                    }
                } else {
                    LOGGER.error(MessageCodes.VSS_022, body.cause().getMessage());
                }
            });

            return Future.failedFuture(new UnexpectedStatusException(statusCode, response.statusMessage()));
        }));
    }

    /**
     * Uploads the file contents to S3. You are responsible for closing the AsyncFile when you're done with it.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @param aFile A file from which to read the content to be sent
     * @param aHandler An upload response handler
     */
    public void put(final String aBucket, final String aKey, final AsyncFile aFile,
            final Handler> aHandler) {
        put(aBucket, aKey, aFile, null, aHandler);
    }

    /**
     * Put a streamed buffer into an S3 bucket. You are responsible for closing the AsyncFile when you're done with it.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 object key
     * @param aFile A file from which to read the buffer
     * @param aMetadata A metadata object
     * @return A future indicating when the buffer has been uploaded
     */
    public Future put(final String aBucket, final String aKey, final AsyncFile aFile,
            final UserMetadata aMetadata) {
        final Future futurePut = createPutRequest(aBucket, aKey);

        return futurePut.compose(request -> request.setUserMetadata(aMetadata).send(aFile).compose(response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                return Future.succeededFuture(new HttpHeaders(response.headers()));
            }

            final String statusMessage = response.statusMessage();
            return Future.failedFuture(new UnexpectedStatusException(statusCode, statusMessage));
        }));
    }

    /**
     * Uploads the file contents to S3. You are responsible for closing the AsyncFile when you're done with it.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @param aFile A file from which to read the content to be sent
     * @param aMetadata User metadata to set on the uploaded S3 object
     * @param aHandler An upload response handler
     */
    @SuppressWarnings("PMD.CognitiveComplexity")
    public void put(final String aBucket, final String aKey, final AsyncFile aFile, final UserMetadata aMetadata,
            final Handler> aHandler) {
        final Promise promise = Promise.promise();

        // Set the supplied handler as the handler for our response promise
        promise.future().onComplete(aHandler);

        createPutRequest(aBucket, aKey).onComplete(putRequest -> {
            if (putRequest.succeeded()) {
                final S3ClientRequest request = putRequest.result();

                if (aMetadata != null) {
                    request.setUserMetadata(aMetadata);
                }

                request.response(send -> {
                    if (send.succeeded()) {
                        final HttpClientResponse response = send.result();
                        final int statusCode = response.statusCode();

                        if (statusCode == HTTP.OK) {
                            promise.complete(new HttpHeaders(response.headers()));
                        } else {
                            promise.fail(new UnexpectedStatusException(statusCode, response.statusMessage()));
                        }
                    } else {
                        promise.fail(send.cause());
                    }
                }).exceptionHandler(error -> {
                    promise.fail(error);
                }).send(aFile);
            } else {
                promise.fail(putRequest.cause());
            }
        });
    }

    /**
     * Closes the S3 client.
     *
     * @return The result of closing the client
     */
    public Future close() {
        return myHttpClient.close();
    }

    /**
     * Gets the Vert.x instance used by the S3 client.
     *
     * @return The underlying Vert.x instance
     */
    public Vertx getVertx() {
        return myVertx;
    }

    /**
     * Sets a connection handler for the client. This handler is called when a new connection is established.
     *
     * @param aHandler A connection handler
     * @return This S3 client
     */
    S3Client connectionHandler(final Handler aHandler) {
        myHttpClient.connectionHandler(aHandler);
        return this;
    }

    /**
     * Builds a S3BucketList from an S3 response.
     *
     * @param aResponse A response from an HttpClientRequest
     * @param aPromise A promise of completion
     */
    private void buildList(final HttpClientResponse aResponse, final Promise aPromise) {
        final int statusCode = aResponse.statusCode();

        if (statusCode == HTTP.OK) {
            aResponse.body(body -> {
                if (body.succeeded()) {
                    try {
                        aPromise.complete(new S3BucketList(body.result()));
                    } catch (final IOException details) {
                        aPromise.fail(details);
                    }
                } else {
                    aPromise.fail(body.cause());
                }
            });
        } else {
            aPromise.fail(new UnexpectedStatusException(statusCode, aResponse.statusMessage()));
        }
    }

    /**
     * A convenience method for building the request URI.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 object key
     * @return A request URI
     */
    private String getURI(final String aBucket, final String aKey) {
        return StringUtils.format(REQUEST, aBucket, aKey);
    }

    /**
     * Creates an S3 DELETE request.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @return An S3 client request
     */
    private Future createDeleteRequest(final String aBucket, final String aKey) {
        final Future futureRequest = myHttpClient.request(HttpMethod.DELETE, getURI(aBucket, aKey));
        final Promise promise = Promise.promise();

        futureRequest.onComplete(request -> {
            if (request.succeeded()) {
                promise.complete(new S3ClientRequest(request.result(), myCredentials));
            } else {
                promise.fail(request.cause());
            }
        });

        return promise.future();
    }

    /**
     * Creates an S3 GET request.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @return A S3 client GET request
     */
    private Future createGetRequest(final String aBucket, final String aKey) {
        final Future futureRequest = myHttpClient.request(HttpMethod.GET, getURI(aBucket, aKey));
        final Promise promise = Promise.promise();

        futureRequest.onComplete(request -> {
            if (request.succeeded()) {
                promise.complete(new S3ClientRequest(request.result(), myCredentials));
            } else {
                promise.fail(request.cause());
            }
        });

        return promise.future();
    }

    /**
     * Creates an S3 HEAD request.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @return A S3 client HEAD request
     */
    private Future createHeadRequest(final String aBucket, final String aKey) {
        final Future futureRequest = myHttpClient.request(HttpMethod.HEAD, getURI(aBucket, aKey));
        final Promise promise = Promise.promise();

        futureRequest.onComplete(request -> {
            if (request.succeeded()) {
                promise.complete(new S3ClientRequest(request.result(), myCredentials));
            } else {
                promise.fail(request.cause());
            }
        });

        return promise.future();
    }

    /**
     * Creates an S3 PUT request.
     *
     * @param aBucket An S3 bucket
     * @param aKey An S3 key
     * @return An S3 PUT request
     */
    private Future createPutRequest(final String aBucket, final String aKey) {
        final Future futureRequest = myHttpClient.request(HttpMethod.PUT, getURI(aBucket, aKey));
        final Promise promise = Promise.promise();

        futureRequest.onComplete(request -> {
            if (request.succeeded()) {
                promise.complete(new S3ClientRequest(request.result(), myCredentials));
            } else {
                promise.fail(request.cause());
            }
        });

        return promise.future();
    }

    /**
     * A convenience method to create a new HttpClient for use by our S3 client.
     *
     * @param aVertx A Vert.x instance
     * @param aConfig A S3 client configuration
     * @return A newly created HttpClient
     */
    private static HttpClient getHttpClient(final Vertx aVertx, final S3ClientOptions aConfig) {
        Objects.requireNonNull(aVertx);
        return aConfig == null ? aVertx.createHttpClient(new S3ClientOptions()) : aVertx.createHttpClient(aConfig);
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy