com.joyent.manta.client.MantaObjectOutputStream.java Source code

Java tutorial

Introduction

Here is the source code for com.joyent.manta.client.MantaObjectOutputStream.java

Source

/*
 * Copyright (c) 2016-2017, Joyent, Inc. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package com.joyent.manta.client;

import com.joyent.manta.exception.MantaIOException;
import com.joyent.manta.http.HttpHelper;
import com.joyent.manta.http.MantaHttpHeaders;
import com.joyent.manta.http.entity.EmbeddedHttpContent;
import org.apache.commons.io.output.ClosedOutputStream;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.http.entity.ContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * {@link OutputStream} that wraps the PUT operations using an {@link java.io.InputStream}
 * as a data source. This implementation uses another thread to keep the Apache HTTP
 * Client's {@link OutputStream} open in order to proxy all calls to it. This is far
 * from an ideal implementation. Please only use this class as a last resort when needing
 * to provide compatibility with inflexible APIs that require an {@link OutputStream}.
 *
 * @author <a href="https://github.com/dekobon">Elijah Zupancic</a>
 * @since 2.4.0
 */
public class MantaObjectOutputStream extends OutputStream {
    /**
     * Logger instance.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(MantaObjectOutputStream.class);

    /**
     * Thread group for all Manta output stream threads.
     */
    private static final ThreadGroup THREAD_GROUP = new ThreadGroup("manta-outputstream");

    /**
     * Number of milliseconds to wait between checks to see if the stream has been closed.
     */
    private static final long CLOSED_CHECK_INTERVAL = 50L;

    /* Note: Do not turn this into a lambda expression - as of now it causes
     * a compilation error. */
    /**
     * Unhandled exception handler that logs errors that happened when reading
     * from the {@link InputStream}.
     */
    private static final Thread.UncaughtExceptionHandler EXCEPTION_HANDLER = new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(final Thread t, final Throwable e) {
            String msg = String.format("An error occurred in the " + "reading thread [%s] when attempting to "
                    + "write to an object via an OutputStream.", t.getName());
            LOGGER.error(msg, e);
        }
    };

    /**
     * Custom thread factory that makes sensibly named daemon threads.
     */
    private static final ThreadFactory THREAD_FACTORY = new ThreadFactory() {
        private final AtomicInteger count = new AtomicInteger(1);

        @Override
        public Thread newThread(final Runnable runnable) {
            final String name = String.format("stream-%d", count.getAndIncrement());
            Thread thread = new Thread(THREAD_GROUP, runnable, name);
            thread.setDaemon(true);
            thread.setUncaughtExceptionHandler(EXCEPTION_HANDLER);

            return thread;
        }
    };

    /**
     * Global executor service used for scheduling Manta OutputStream threads.
     * You shouldn't need to call shutdown on this because all of the threads scheduled
     * are daemon threads, but it is exposed so that you can manage its lifecycle
     * if needed.
     */
    public static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(THREAD_FACTORY);

    /**
     * Http content object that is proxied by this stream.
     */
    private final EmbeddedHttpContent httpContent;

    /**
     * {@link Future} that represents upload thread running.
     */
    private final Future<MantaObjectResponse> completed;

    /**
     * The response object when the upload has finished.
     */
    private MantaObjectResponse objectResponse;

    /**
     * A running count of the total number of bytes written.
     */
    private AtomicLong bytesWritten = new AtomicLong(0L);

    /**
     * Flag indicating that this stream has been closed.
     */
    private AtomicBoolean closed = new AtomicBoolean(false);

    /**
     * Path of the object being written to.
     */
    private final String path;

    /**
     * Creates a new instance of an {@link OutputStream} that wraps PUT
     * requests to Manta.
     *
     * @param path The fully qualified path of the object. i.e. /user/stor/foo/bar/baz
     * @param httpHelper reference to HTTP operations helper class
     * @param mantaHttpHeaders optional HTTP headers to include when copying the object
     * @param metadata optional user-supplied metadata for object
     * @param contentType HTTP Content-Type header value
     */
    MantaObjectOutputStream(final String path, final HttpHelper httpHelper, final MantaHttpHeaders mantaHttpHeaders,
            final MantaMetadata metadata, final ContentType contentType) {
        this.httpContent = new EmbeddedHttpContent(contentType.toString(), closed);
        this.path = path;

        final MantaHttpHeaders headers;

        if (mantaHttpHeaders == null) {
            headers = new MantaHttpHeaders();
        } else {
            headers = mantaHttpHeaders;
        }

        if (contentType != null) {
            headers.setContentType(contentType.toString());
        }

        /*
         * Thread execution definition that runs the HTTP PUT operation.
         */
        this.completed = EXECUTOR.submit(() -> httpHelper.httpPut(path, headers, httpContent, metadata));

        /*
         * We have to wait here until the upload to Manta starts and a Writer
         * becomes available.
         */
        while (httpContent.getWriter() == null) {
            try {
                Thread.sleep(CLOSED_CHECK_INTERVAL);
            } catch (InterruptedException e) {
                return;
            }
        }
    }

    @Override
    public void write(final int b) throws IOException {
        if (this.closed.get()) {
            MantaIOException e = new MantaIOException("Can't write to a closed stream");
            e.setContextValue("path", path);
            throw e;
        }

        httpContent.getWriter().write(b);
        bytesWritten.incrementAndGet();
    }

    @Override
    public void write(final byte[] b) throws IOException {
        if (this.closed.get()) {
            MantaIOException e = new MantaIOException("Can't write to a closed stream");
            e.setContextValue("path", path);
            throw e;
        }

        httpContent.getWriter().write(b);
        bytesWritten.addAndGet(b.length);
    }

    @Override
    public void write(final byte[] b, final int off, final int len) throws IOException {
        if (this.closed.get()) {
            MantaIOException e = new MantaIOException("Can't write to a closed stream");
            e.setContextValue("path", path);
            throw e;
        }

        httpContent.getWriter().write(b, off, len);
        bytesWritten.addAndGet(b.length);
    }

    @Override
    public void flush() throws IOException {
        if (this.closed.get()) {
            return;
        }

        httpContent.getWriter().flush();
    }

    /**
     * Uses reflection to look into the specified {@link OutputStream} instance to
     * see if there is a boolean field set called "closed", if it is set and accessible
     * via reflection, we return its value. Otherwise, we return null.
     *
     * @param stream instance to reflect on for closed property
     * @return reference to closed property or null if unavailable
     */
    protected static Boolean isInnerStreamClosed(final OutputStream stream) {
        OutputStream inner = findMostInnerOutputStream(stream);

        // If the inner most stream is a closed instance, then we can assume
        // the stream is close.
        if (inner.getClass().equals(ClosedOutputStream.class)) {
            return true;
        }

        try {
            Field f = FieldUtils.getField(inner.getClass(), "closed", true);

            if (f == null) {
                throw new IllegalArgumentException("FieldUtils.getField(inner.getClass()) " + "returned null");
            }

            Object result = f.get(inner);
            return (boolean) result;
        } catch (IllegalArgumentException | IllegalAccessException | ClassCastException e) {
            String msg = String.format("Error finding [closed] field on class: %s", inner.getClass());
            LOGGER.warn(msg, e);
            /* If we don't have an inner field called closed, it is inaccessible or
             * the field isn't a boolean, return null because we are now dealing with
             * undefined behavior. */
            return null;
        }
    }

    /**
     * Finds the most inner stream if the embedded stream is stored on the passed
     * stream as a field named <code>out</code>. This hold true for all classes
     * that extend {@link java.io.FilterOutputStream}.
     *
     * @param stream stream to search for inner stream
     * @return reference to inner stream class
     */
    protected static OutputStream findMostInnerOutputStream(final OutputStream stream) {
        Field f = FieldUtils.getField(stream.getClass(), "out", true);

        if (f == null) {
            return stream;
        } else {
            try {
                Object result = f.get(stream);

                if (result instanceof OutputStream) {
                    return findMostInnerOutputStream((OutputStream) result);
                } else {
                    return stream;
                }
            } catch (IllegalAccessException e) {
                // If we can't access the field, then we just return back the original stream
                return stream;
            }
        }
    }

    @Override
    public synchronized void close() throws IOException {
        this.closed.compareAndSet(false, true);

        Boolean innerIsClosed = isInnerStreamClosed(this.httpContent.getWriter());
        if (innerIsClosed != null && !innerIsClosed) {
            this.httpContent.getWriter().flush();
        }

        synchronized (this.httpContent) {
            this.httpContent.notify();
        }

        try {
            this.objectResponse = this.completed.get();
            this.objectResponse.setContentLength(bytesWritten.get());
        } catch (InterruptedException e) {
            // continue execution if interrupted
        } catch (ExecutionException e) {
            /* We wrap the cause because the stack trace for the
             * ExecutionException offers nothing useful and is just a wrapper
             * for exceptions that are thrown within a Future. */
            MantaIOException mioe = new MantaIOException(e.getCause());

            if (this.objectResponse != null) {
                final String requestId = this.objectResponse.getHeaderAsString(MantaHttpHeaders.REQUEST_ID);

                if (requestId != null) {
                    mioe.addContextValue("requestId", requestId);
                }
            }

            mioe.addContextValue("path", path);

            throw mioe;
        }
    }

    /**
     * Flag indicating if the stream has been closed.
     *
     * @return true if closed, otherwise false
     */
    public boolean isClosed() {
        return closed.get();
    }

    /**
     * Returns the PUT response object. This value is only available when
     * close() has completed.
     * @return PUT object response or null
     */
    public MantaObjectResponse getObjectResponse() {
        return objectResponse;
    }
}