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

info.freelibrary.pairtree.s3.S3PairtreeObject Maven / Gradle / Ivy


package info.freelibrary.pairtree.s3;

import static info.freelibrary.pairtree.Constants.BUNDLE_NAME;
import static info.freelibrary.pairtree.Constants.PATH_SEP;
import static info.freelibrary.pairtree.Pairtree.PAIRTREE_ROOT;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import info.freelibrary.pairtree.HTTP;
import info.freelibrary.pairtree.MessageCodes;
import info.freelibrary.pairtree.PairtreeObject;
import info.freelibrary.pairtree.PairtreeUtils;
import info.freelibrary.util.I18nObject;
import info.freelibrary.util.Logger;
import info.freelibrary.util.LoggerFactory;
import info.freelibrary.vertx.s3.S3Client;

import io.vertx.core.AsyncResult;
import io.vertx.core.CompositeFuture;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;

/**
 * An S3-backed Pairtree object implementation.
 */
public class S3PairtreeObject extends I18nObject implements PairtreeObject {

    /** The logger used when interacting with the S3 Pairtree object */
    private static final Logger LOGGER = LoggerFactory.getLogger(S3PairtreeObject.class, BUNDLE_NAME);

    /** Creates a README file for an S3 Pairtree */
    private static final String README_FILE = "/README.txt";

    /** A regular plus symbol */
    private static final String UNENCODED_PLUS = "+";

    /** A URL encoded plus symbol */
    private static final String ENCODED_PLUS = "%2B";

    /** The client used to interact with the S3 Pairtree */
    private final S3Client myS3Client;

    /** The bucket in which the Pairtree resides */
    private final String myPairtreeBucket;

    /** The path in the bucket to the Pairtree */
    private final String myBucketPath;

    /** The Pairtree's prefix (optional) */
    private final Optional myPrefix;

    /** The Pairtree's ID */
    private final String myID;

    /**
     * Creates a new Pairtree object.
     *
     * @param aS3Client An S3 client to communicate with S3
     * @param aPairtree An S3-backed Pairtree
     * @param aID An ID for the Pairtree resource
     */
    public S3PairtreeObject(final S3Client aS3Client, final S3Pairtree aPairtree, final String aID) {
        super(BUNDLE_NAME);

        myBucketPath = aPairtree.getBucketPath();
        myPairtreeBucket = aPairtree.getPath();
        myPrefix = aPairtree.getPrefix();
        myS3Client = aS3Client;
        myID = aID;
    }

    @Override
    public void exists(final Handler> aHandler) {
        Objects.requireNonNull(aHandler, getI18n(MessageCodes.PT_010));

        final Future future = Future.future().setHandler(aHandler);

        myS3Client.head(myPairtreeBucket, myBucketPath + getPath() + README_FILE, response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                final String contentLength = response.getHeader(HTTP.CONTENT_LENGTH);

                try {
                    if (Integer.parseInt(contentLength) > 0) {
                        future.complete(true);
                    } else {
                        future.complete(false);
                    }
                } catch (final NumberFormatException details) {
                    future.fail(getI18n(MessageCodes.PT_019, contentLength));
                }
            } else if (statusCode == HTTP.NOT_FOUND) {
                future.complete(false);
            } else {
                final String statusMessage = response.statusMessage();
                future.fail(getI18n(MessageCodes.PT_DEBUG_045, statusCode, getPath() + README_FILE, statusMessage));
            }
        });
    }

    @Override
    public void create(final Handler> aHandler) {
        Objects.requireNonNull(aHandler, getI18n(MessageCodes.PT_010));

        final Future future = Future.future().setHandler(aHandler);

        myS3Client.put(myPairtreeBucket, myBucketPath + getPath() + README_FILE, Buffer.buffer(myID), response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                future.complete();
            } else {
                final String statusMessage = response.statusMessage();

                future.fail(getI18n(MessageCodes.PT_DEBUG_045, statusCode, getPath() + README_FILE, statusMessage));
            }
        });
    }

    @Override
    public void delete(final Handler> aHandler) {
        Objects.requireNonNull(aHandler, getI18n(MessageCodes.PT_010));

        final Future future = Future.future().setHandler(aHandler);

        myS3Client.list(myPairtreeBucket, myBucketPath + getPath(), listResponse -> {
            final int listStatusCode = listResponse.statusCode();

            if (listStatusCode == HTTP.OK) {
                listResponse.bodyHandler(bodyHandler -> {
                    final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();

                    saxParserFactory.setNamespaceAware(true);

                    try {
                        final SAXParser saxParser = saxParserFactory.newSAXParser();
                        final XMLReader xmlReader = saxParser.getXMLReader();
                        final ObjectListHandler s3ListHandler = new ObjectListHandler();
                        final List futures = new ArrayList<>();
                        final List keyList;

                        xmlReader.setContentHandler(s3ListHandler);
                        xmlReader.parse(new InputSource(new StringReader(bodyHandler.toString())));
                        keyList = s3ListHandler.getKeys();

                        for (final String key : keyList) {
                            final Future keyFuture = Future.future();

                            futures.add(keyFuture);

                            myS3Client.delete(myPairtreeBucket, key, deleteResponse -> {
                                final int deleteStatusCode = deleteResponse.statusCode();

                                if (deleteStatusCode == HTTP.NO_CONTENT) {
                                    keyFuture.complete();
                                } else {
                                    final String status = deleteResponse.statusMessage();
                                    keyFuture.fail(getI18n(MessageCodes.PT_DEBUG_045, deleteStatusCode, key, status));
                                }
                            });
                        }

                        CompositeFuture.all(futures).setHandler(result -> {
                            if (result.succeeded()) {
                                future.complete();
                            } else {
                                future.fail(result.cause());
                            }
                        });
                    } catch (final ParserConfigurationException | SAXException | IOException details) {
                        future.fail(details);
                    }
                });
            } else {
                final String status = listResponse.statusMessage();
                future.fail(getI18n(MessageCodes.PT_DEBUG_045, listStatusCode, getPath(), status));
            }
        });
    }

    @Override
    public String getID() {
        return !myPrefix.isPresent() ? myID : myPrefix.get() + PATH_SEP + myID;
    }

    /**
     * Gets the object path. If the path contains a '+' it will be URL encoded for interaction with S3's HTTP API.
     *
     * @return the path of the Pairtree object as it's found in S3
     */
    @Override
    public String getPath() {
        // We need to URL encode '+'s to work around an S3 bug
        // (Cf. https://forums.aws.amazon.com/thread.jspa?threadID=55746)
        return PAIRTREE_ROOT + PATH_SEP + PairtreeUtils.mapToPtPath(myID).replace(UNENCODED_PLUS, ENCODED_PLUS) +
                PATH_SEP + PairtreeUtils.encodeID(myID).replace(UNENCODED_PLUS, ENCODED_PLUS);
    }

    /**
     * Gets the path of the requested object resource. If the path contains a '+' it will be URL encoded for
     * interaction with S3's HTTP API.
     *
     * @param aResourcePath The Pairtree resource which the returned path should represent
     * @return The path of the requested object resource as it's found in S3
     */
    @Override
    public String getPath(final String aResourcePath) {
        // We need to URL encode '+'s to work around an S3 bug
        // (Cf. https://forums.aws.amazon.com/thread.jspa?threadID=55746)
        return aResourcePath.charAt(0) == '/' ? getPath() + aResourcePath.replace(UNENCODED_PLUS, ENCODED_PLUS)
                : getPath() + PATH_SEP + aResourcePath.replace(UNENCODED_PLUS, ENCODED_PLUS);
    }

    @Override
    public void put(final String aPath, final Buffer aBuffer, final Handler> aHandler) {
        Objects.requireNonNull(aHandler, getI18n(MessageCodes.PT_010));

        final Future future = Future.future().setHandler(aHandler);

        LOGGER.debug(MessageCodes.PT_DEBUG_057, aPath);

        myS3Client.put(myPairtreeBucket, myBucketPath + getPath(aPath), aBuffer, response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                future.complete();
            } else {
                if (statusCode == HTTP.FORBIDDEN) {
                    response.bodyHandler(handler -> {
                        LOGGER.debug(new String(handler.getBytes()));
                    });
                }

                final String status = response.statusMessage();
                future.fail(getI18n(MessageCodes.PT_DEBUG_045, statusCode, getPath() + PATH_SEP + aPath, status));
            }
        });
    }

    @Override
    public void get(final String aPath, final Handler> aHandler) {
        Objects.requireNonNull(aHandler, getI18n(MessageCodes.PT_010));

        final Future future = Future.future().setHandler(aHandler);

        LOGGER.debug(MessageCodes.PT_DEBUG_058, aPath);

        myS3Client.get(myPairtreeBucket, myBucketPath + getPath(aPath), response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                response.bodyHandler(bodyHandlerResult -> {
                    future.complete(Buffer.buffer(bodyHandlerResult.getBytes()));
                });
            } else {
                final String status = response.statusMessage();
                future.fail(getI18n(MessageCodes.PT_DEBUG_045, statusCode, getPath() + PATH_SEP + aPath, status));
            }
        });
    }

    @Override
    public void find(final String aPath, final Handler> aHandler) {
        Objects.requireNonNull(aHandler, getI18n(MessageCodes.PT_010));

        final Future future = Future.future().setHandler(aHandler);

        LOGGER.debug(MessageCodes.PT_DEBUG_059, aPath, myPairtreeBucket, getPath(aPath));

        myS3Client.head(myPairtreeBucket, myBucketPath + getPath(aPath), response -> {
            final int statusCode = response.statusCode();

            if (statusCode == HTTP.OK) {
                final String contentLength = response.getHeader(HTTP.CONTENT_LENGTH);

                try {
                    if (Integer.parseInt(contentLength) > 0) {
                        future.complete(true);
                    } else {
                        future.complete(false);
                    }
                } catch (final NumberFormatException details) {
                    future.fail(getI18n(MessageCodes.PT_019, contentLength));
                }
            } else if (statusCode == HTTP.NOT_FOUND) {
                future.complete(false);
            } else {
                final String status = response.statusMessage();
                future.fail(getI18n(MessageCodes.PT_DEBUG_045, statusCode, getPath() + PATH_SEP + aPath, status));
            }
        });
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy