com.kolich.havalo.controllers.api.ObjectApi.java Source code

Java tutorial

Introduction

Here is the source code for com.kolich.havalo.controllers.api.ObjectApi.java

Source

/**
 * Copyright (c) 2015 Mark S. Kolich
 * http://mark.koli.ch
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package com.kolich.havalo.controllers.api;

import com.kolich.bolt.ReentrantReadWriteEntityLock;
import com.kolich.common.util.secure.KolichChecksum;
import com.kolich.curacao.annotations.Controller;
import com.kolich.curacao.annotations.Injectable;
import com.kolich.curacao.annotations.RequestMapping;
import com.kolich.curacao.annotations.parameters.convenience.ContentLength;
import com.kolich.curacao.annotations.parameters.convenience.ContentType;
import com.kolich.curacao.annotations.parameters.convenience.IfMatch;
import com.kolich.curacao.entities.CuracaoEntity;
import com.kolich.curacao.entities.empty.StatusCodeOnlyCuracaoEntity;
import com.kolich.curacao.mappers.request.matchers.AntPathMatcher;
import com.kolich.havalo.components.RepositoryManagerComponent;
import com.kolich.havalo.controllers.HavaloApiController;
import com.kolich.havalo.entities.types.DiskObject;
import com.kolich.havalo.entities.types.HashedFileObject;
import com.kolich.havalo.entities.types.KeyPair;
import com.kolich.havalo.entities.types.Repository;
import com.kolich.havalo.exceptions.objects.ObjectConflictException;
import com.kolich.havalo.exceptions.objects.ObjectLengthNotSpecifiedException;
import com.kolich.havalo.exceptions.objects.ObjectNotFoundException;
import com.kolich.havalo.exceptions.objects.ObjectTooLargeException;
import com.kolich.havalo.filters.HavaloAuthenticationFilter;
import com.kolich.havalo.mappers.ObjectKeyArgumentMapper.ObjectKey;
import org.slf4j.Logger;

import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.io.Files.move;
import static com.google.common.net.HttpHeaders.*;
import static com.google.common.net.MediaType.OCTET_STREAM;
import static com.kolich.common.util.secure.KolichChecksum.getSHA1HashAndCopy;
import static com.kolich.curacao.annotations.RequestMapping.Method.*;
import static com.kolich.havalo.HavaloConfigurationFactory.getMaxUploadSize;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static org.apache.commons.io.FileUtils.deleteQuietly;
import static org.apache.commons.io.IOUtils.copyLarge;
import static org.slf4j.LoggerFactory.getLogger;

@Controller
public class ObjectApi extends HavaloApiController {

    private static final Logger logger__ = getLogger(ObjectApi.class);

    private static final String OCTET_STREAM_TYPE = OCTET_STREAM.toString();

    private final long uploadMaxSize_;

    @Injectable
    public ObjectApi(final RepositoryManagerComponent component) {
        super(component.getRepositoryManager());
        uploadMaxSize_ = getMaxUploadSize();
    }

    @RequestMapping(methods = HEAD, value = "/api/object/{key}", matcher = AntPathMatcher.class, filters = HavaloAuthenticationFilter.class)
    public final void head(final ObjectKey key, final KeyPair userKp, final HttpServletResponse response,
            final AsyncContext context) throws Exception {
        final Repository repo = getRepository(userKp.getKey());
        new ReentrantReadWriteEntityLock<Void>(repo) {
            @Override
            public Void transaction() throws Exception {
                final HashedFileObject hfo = getHashedFileObject(repo,
                        // The URL-decoded key of the object to delete.
                        key,
                        // Fail if not found.
                        true);
                new ReentrantReadWriteEntityLock<HashedFileObject>(hfo) {
                    @Override
                    public HashedFileObject transaction() throws Exception {
                        final DiskObject object = getCanonicalObject(repo, hfo);
                        streamHeaders(object, hfo, response);
                        return hfo;
                    }
                }.read(); // Shared read lock on file object
                return null;
            }

            @Override
            public void success(final Void v) throws Exception {
                context.complete();
            }
        }.read(); // Shared read lock on repo
    }

    @RequestMapping(methods = GET, value = "/api/object/{key}", matcher = AntPathMatcher.class, filters = HavaloAuthenticationFilter.class)
    public final void get(final ObjectKey key, final KeyPair userKp, final HttpServletResponse response,
            final AsyncContext context) throws Exception {
        final Repository repo = getRepository(userKp.getKey());
        new ReentrantReadWriteEntityLock<Void>(repo) {
            @Override
            public Void transaction() throws Exception {
                final HashedFileObject hfo = getHashedFileObject(repo,
                        // The URL-decoded key of the object to delete.
                        key,
                        // Fail if not found.
                        true);
                new ReentrantReadWriteEntityLock<HashedFileObject>(hfo) {
                    @Override
                    public HashedFileObject transaction() throws Exception {
                        final DiskObject object = getCanonicalObject(repo, hfo);
                        // Validate that the object file exists on disk
                        // before we attempt to load it.
                        if (!object.getFile().exists()) {
                            throw new ObjectNotFoundException("Failed " + "to find canonical object on disk "
                                    + "(key=" + key + ", file=" + object.getFile().getAbsolutePath() + ")");
                        }
                        streamHeaders(object, hfo, response);
                        streamObject(object, response);
                        return hfo;
                    }
                }.read(); // Shared read lock on file object, wait
                return null;
            }

            @Override
            public void success(final Void v) throws Exception {
                context.complete();
            }
        }.read(false); // Shared read lock on repo, no wait
    }

    @RequestMapping(methods = PUT, value = "/api/object/{key}", matcher = AntPathMatcher.class, filters = HavaloAuthenticationFilter.class)
    public final HashedFileObject put(final ObjectKey key, final KeyPair userKp, @IfMatch final String ifMatch,
            @ContentType final String contentType, @ContentLength final Long contentLength,
            final HttpServletRequest request, final HttpServletResponse response) throws Exception {
        final Repository repo = getRepository(userKp.getKey());
        return new ReentrantReadWriteEntityLock<HashedFileObject>(repo) {
            @Override
            public HashedFileObject transaction() throws Exception {
                // Havalo requires the consumer to send a Content-Length
                // request header with the request when uploading an
                // object.
                if (contentLength < 0L) {
                    // A value of -1 indicates that no Content-Length
                    // header was set.  Bail gracefully.
                    throw new ObjectLengthNotSpecifiedException("Client " + "sent request where '" + CONTENT_LENGTH
                            + "' header was not present or less than zero.");
                }
                // Only accept the object if the Content-Length of the
                // incoming request is less than or equal to the max
                // upload size.
                if (contentLength > uploadMaxSize_) {
                    throw new ObjectTooLargeException("The '" + CONTENT_LENGTH + "' of the incoming request "
                            + "is too large. Max upload size allowed is " + uploadMaxSize_ + "-bytes.");
                }
                final HashedFileObject hfo = getHashedFileObject(repo, key);
                return new ReentrantReadWriteEntityLock<HashedFileObject>(hfo) {
                    @Override
                    public HashedFileObject transaction() throws Exception {
                        final String eTag = hfo.getFirstHeader(ETAG);
                        // If we have an incoming If-Match, we need to compare
                        // that against the current HFO before we attempt to
                        // update.  If the If-Match ETag does not match, fail.
                        if (ifMatch != null && eTag != null) {
                            // OK, we have an incoming If-Match ETag, use it.
                            // NOTE: HFO's will _always_ have an ETag attached
                            // to their meta-data.  ETag's are always computed
                            // for HFO's upload. But new HFO's (one's the repo
                            // have never seen before) may not yet have an ETag.
                            if (!ifMatch.equals(eTag)) {
                                throw new ObjectConflictException("Failed "
                                        + "to update HFO; incoming If-Match ETag " + "does not match (hfo="
                                        + hfo.getName() + ", etag=" + eTag + ", if-match=" + ifMatch + ")");
                            }
                        }
                        final DiskObject object = getCanonicalObject(repo, hfo,
                                // Create the File on disk if it does not
                                // already exist. Yay!
                                true);
                        // The file itself (should exist now).
                        final File objFile = object.getFile();
                        final File tempObjFile = object.getTempFile();
                        try (final InputStream is = request.getInputStream();
                                final OutputStream os = new FileOutputStream(tempObjFile);) {
                            // Compute the ETag (an MD5 hash of the file) while
                            // copying the file into place.  The bytes of the
                            // input stream and piped into an MD5 digest _and_
                            // to the output stream -- ideally computing the
                            // hash and copying the file at the same time.
                            // Set the resulting ETag header (meta data).
                            hfo.setETag(getSHA1HashAndCopy(is, os,
                                    // Only copy as much as the incoming
                                    // Content-Length header sez is going
                                    // to be sent.  Anything more than this
                                    // is caught gracefully and dropped.
                                    contentLength));
                            // Move the uploaded file into place (moves
                            // the file from the temp location to the
                            // real destination inside of the repository
                            // on disk).
                            move(tempObjFile, objFile);
                            // Set the Last-Modified header (meta data).
                            hfo.setLastModified(objFile.lastModified());
                            // Set the Content-Length header (meta data).
                            hfo.setContentLength(objFile.length());
                            // Set the Content-Type header (meta data).
                            if (contentType != null) {
                                hfo.setContentType(contentType);
                            }
                        } catch (KolichChecksum.KolichChecksumException e) {
                            // Quietly delete the object on disk when
                            // it has exceeded the max upload size allowed
                            // by this Havalo instance.
                            throw new ObjectTooLargeException("The " + "size of the incoming object is too "
                                    + "large. Max upload size is " + uploadMaxSize_ + "-bytes.", e);
                        } finally {
                            // Delete the file from the temp upload
                            // location.  Note, this file may not exist
                            // if the upload was successful and the object
                            // was moved into place.. which is OK here.
                            deleteQuietly(tempObjFile);
                        }
                        // Append an ETag header to the response for the
                        // PUT'ed object.
                        response.setHeader(ETAG, hfo.getFirstHeader(ETAG));
                        return hfo;
                    }

                    @Override
                    public void success(final HashedFileObject e) throws Exception {
                        // On success only, ask the repo manager to
                        // asynchronously flush this repository's meta
                        // data to disk.
                        flushRepository(repo);
                    }
                }.write(); // Exclusive lock on this HFO, no wait
            }
        }.read(false); // Shared read lock on repo, no wait
    }

    @RequestMapping(methods = DELETE, value = "/api/object/{key}", matcher = AntPathMatcher.class, filters = HavaloAuthenticationFilter.class)
    public final CuracaoEntity delete(final ObjectKey key, @IfMatch final String ifMatch, final KeyPair userKp)
            throws Exception {
        // The delete operation does return a pointer to the "deleted"
        // HFO, but we're not using it, we're just dropping it on the
        // floor (intentionally not returning it to the caller).
        deleteHashedFileObject(userKp.getKey(),
                // The URL-decoded key of the object to delete.
                key,
                // Only delete the object if the provided ETag via the
                // If-Match header matches the object on disk.
                ifMatch);
        return new StatusCodeOnlyCuracaoEntity(SC_NO_CONTENT);
    }

    private static final void streamHeaders(final DiskObject object, final HashedFileObject hfo,
            final HttpServletResponse response) {
        checkNotNull(hfo, "Hashed file object cannot be null.");
        // Validate that the File object still exists.
        if (!object.getFile().exists()) {
            throw new ObjectNotFoundException("Object not " + "found (file=" + object.getFile().getAbsolutePath()
                    + ", key=" + hfo.getName() + ")");
        }
        // Extract any response headers from this objects' meta data.
        final Map<String, List<String>> headers = hfo.getHeaders();
        // Always set the Content-Length header to the actual length of the
        // file on disk -- effectively overriding any "Content-Length" meta
        // data header set by the user in the PUT request.
        headers.put(CONTENT_LENGTH, Arrays.asList(Long.toString(object.getFile().length())));
        // Set the Content-Type header to a default if one was not set by
        // the consumer in the meta data.
        if (headers.get(CONTENT_TYPE) == null) {
            headers.put(CONTENT_TYPE, Arrays.asList(OCTET_STREAM_TYPE));
        }
        // Now, send all headers to the response stream.
        for (final Map.Entry<String, List<String>> entry : headers.entrySet()) {
            final String key = entry.getKey();
            for (final String value : entry.getValue()) {
                response.addHeader(key, value);
            }
        }
    }

    private static final void streamObject(final DiskObject object, final HttpServletResponse response) {
        try (final InputStream is = new FileInputStream(object.getFile());
                final OutputStream os = response.getOutputStream()) {
            copyLarge(is, os);
        } catch (Exception e) {
            // On any Exception case, just log the failure and move on.
            // We're closing the output stream in the finally{} block below
            // so it's not like we can fail here then somehow return an error
            // message to the API consumer.  We're handling this as gracefully
            // as best we can.
            logger__.error("Failed to stream object to client.", e);
        }
    }

}