net.siegmar.japtproxy.misc.IOHandler.java Source code

Java tutorial

Introduction

Here is the source code for net.siegmar.japtproxy.misc.IOHandler.java

Source

/**
 * Japt-Proxy: The JAVA(TM) based APT-Proxy
 *
 * Copyright (C) 2006-2008  Oliver Siegmar <oliver@siegmar.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.siegmar.japtproxy.misc;

import net.siegmar.japtproxy.exception.InitializationException;
import net.siegmar.japtproxy.exception.ResourceUnavailableException;
import net.siegmar.japtproxy.fetcher.FetchedResource;
import net.siegmar.japtproxy.fetcher.Fetcher;
import net.siegmar.japtproxy.fetcher.FetcherPool;
import net.siegmar.japtproxy.poolobject.PoolObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.TeeOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;

/**
 * The IOHandler utility class is responsible for the IO
 * operation between fetchers and pools.
 *
 * @author Oliver Siegmar
 */
public class IOHandler {

    /**
     * The logger instance.
     */
    private static final Logger LOG = LoggerFactory.getLogger(IOHandler.class);

    /**
     * Map that contains the timestamp of the last check per resource.
     */
    private final Map<String, Date> resourcesLastCheckedMap = new HashMap<>();

    /**
     * Duration between new version check of mutable files.
     *
     * Default 1 minute.
     */
    private final int cacheDuration = 60_000;

    /**
     * The FetcherFactory instance.
     */
    private FetcherPool fetcherPool;

    @Required
    public void setFetcherPool(final FetcherPool fetcherPool) {
        this.fetcherPool = fetcherPool;
    }

    /**
     * Checks if a new version check is required for a specific resource.
     *
     * @param resourceName the resource name to check if a version check is
     *                     required for
     * @return if a new version check is required
     */
    protected boolean isNewVersionCheckRequired(final PoolObject poolObject, final String resourceName) {
        // If the resource is known to be immutable,
        // no checks are required at all
        if (poolObject.getRepoPackage() != null && poolObject.getRepoPackage().isImmutable()) {
            LOG.debug("Resource '{}' is known to be immutable. No version check required.", resourceName);
            return false;
        }

        synchronized (resourcesLastCheckedMap) {
            final Date now = new Date();

            final Date d = resourcesLastCheckedMap.get(resourceName);

            // If the map doesn't contain an entry, add one
            if (d == null) {
                resourcesLastCheckedMap.put(resourceName, now);
                return true;
            }

            // If the map contains an entry, check if it is exceeded
            if (cacheDuration > now.getTime() - d.getTime()) {
                return false;
            }

            resourcesLastCheckedMap.remove(resourceName);
            return true;
        }
    }

    /**
     * Sends a locally stored pool object to the client. This method will
     * send HTTP status code 304 (not modified) if the client sent a
     * 'If-Modified-Since' header and the pool object wasn't modified since
     * that date.
     *
     * @param poolObject           the pool object to sent
     * @param requestModifiedSince the "If-Modified-Since" header
     * @param res                  the HttpServletResponse object
     * @throws IOException is thrown if a problem occured while sending data
     */
    protected void sendLocalFile(final PoolObject poolObject, final long requestModifiedSince,
            final HttpServletResponse res) throws IOException {
        final long poolModification = poolObject.getLastModified();

        if (requestModifiedSince != -1 && poolModification <= requestModifiedSince) {
            LOG.debug("Requested resource wasn't modified since last request. "
                    + "Returning status code 304 - not modified");
            res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        res.setContentType(poolObject.getContentType());
        res.setContentLength((int) poolObject.getSize());
        res.setDateHeader(HttpHeaderConstants.LAST_MODIFIED, poolObject.getLastModified());

        InputStream is = null;
        final OutputStream sendOs = res.getOutputStream();

        try {
            LOG.info("Sending locally cached object '{}'", poolObject.getName());

            is = poolObject.getInputStream();
            IOUtils.copy(is, sendOs);
        } finally {
            IOUtils.closeQuietly(is);
        }
    }

    /**
     * This method is responsible for fetching remote data (if needed) and
     * sending the data (locally stored, or remotely fetched) to the client.
     *
     * @param requestedData  the requested data
     * @param poolObject     the pool object
     * @param targetResource the remote resource link
     * @param res            the HttpServletResponse object
     * @return true if the file was sent from cache, false otherwise
     * @throws IOException is thrown if a problem occured while sending data
     * @throws net.siegmar.japtproxy.exception.ResourceUnavailableException is thrown if the resource was not found
     */
    public boolean sendAndSave(final RequestedData requestedData, final PoolObject poolObject,
            final URL targetResource, final HttpServletResponse res)
            throws IOException, ResourceUnavailableException, InitializationException {
        final String lockIdentifier = requestedData.getRequestedResource();
        final ReadWriteLock lock = ResourceLock.obtainLocker(lockIdentifier);
        final Lock readLock = lock.readLock();

        LockStatus lockStatus;

        readLock.lock();
        lockStatus = LockStatus.READ;

        LOG.debug("Obtained readLock for '{}'", lockIdentifier);

        final long poolModification = poolObject.getLastModified();

        FetchedResource fetchedResource = null;
        OutputStream saveOs = null;
        InputStream is = null;

        try {
            if (poolModification != 0) {
                if (!isNewVersionCheckRequired(poolObject, requestedData.getRequestedResource())) {
                    LOG.debug("Local object exists and no need to do a version check - sending local object");
                    sendLocalFile(poolObject, requestedData.getRequestModifiedSince(), res);
                    return true;
                }

                LOG.debug("Local object exists but new version check is required");
            } else {
                LOG.debug("No local object exists - requesting remote host");
            }

            // Get a fetcher (http, ftp) for the current targetResource
            final Fetcher fetcher = fetcherPool.getInstance(targetResource);

            if (fetcher == null) {
                throw new InitializationException("No fetcher found for resource '" + targetResource + "'");
            }

            fetchedResource = fetcher.fetch(targetResource, poolModification, requestedData.getUserAgent());

            final String contentType = fetchedResource.getContentType();
            final long remoteModification = fetchedResource.getLastModified();
            final long contentLength = fetchedResource.getContentLength();

            if (remoteModification != 0 && poolModification > remoteModification) {
                LOG.warn(
                        "Remote object is older than local pool object "
                                + "(Remote timestamp: {} - Local timestamp: {}). "
                                + "Object won't get updated! Check this manually!",
                        Util.getSimpleDateFromTimestamp(remoteModification),
                        Util.getSimpleDateFromTimestamp(poolModification));
            }

            setHeader(res, fetchedResource);

            if (!fetchedResource.isModified()) {
                LOG.debug("Remote resource has no new version - sending local object");
                sendLocalFile(poolObject, requestedData.getRequestModifiedSince(), res);
                return true;
            }

            if (LOG.isDebugEnabled()) {
                if (poolModification != 0) {
                    // Pool file exists, but it is out of date
                    LOG.debug(
                            "Newer version found (old Last-Modified: {}) - Request '{}', Last-Modified: {}, "
                                    + "Content-Type: {}, Content-Length: {}",
                            Util.getSimpleDateFromTimestamp(poolModification), targetResource,
                            Util.getSimpleDateFromTimestamp(remoteModification), contentType, contentLength);
                } else {
                    // No pool file exists
                    LOG.debug("Request '{}', Last-Modified: {}, Content-Type: {}, Content-Length: {}",
                            targetResource, Util.getSimpleDateFromTimestamp(remoteModification), contentType,
                            contentLength);
                }
            }

            readLock.unlock();
            lock.writeLock().lock();
            lockStatus = LockStatus.WRITE;

            LOG.debug("Obtained writeLock for '{}'", lockIdentifier);

            is = fetchedResource.getInputStream();

            LOG.info("Sending remote object '{}'", poolObject.getName());

            saveOs = new TeeOutputStream(poolObject.getOutputStream(), res.getOutputStream());
            final long bytesCopied = IOUtils.copyLarge(is, saveOs);

            LOG.debug("Data sent to file and client");

            poolObject.setLastModified(remoteModification);

            if (contentLength != -1 && bytesCopied != contentLength) {
                throw new IOException(
                        String.format("Received file has invalid file size - " + "only %d of %d were downloaded",
                                bytesCopied, contentLength));
            }

            poolObject.store();

            return false;
        } catch (final IOException e) {
            // Remove pool file if it was created by this thread
            if (poolModification == 0) {
                poolObject.remove();
            }

            throw e;
        } finally {
            IOUtils.closeQuietly(is);
            IOUtils.closeQuietly(saveOs);

            if (fetchedResource != null) {
                fetchedResource.close();
            }

            if (lockStatus == LockStatus.WRITE) {
                LOG.debug("Released writeLock for '{}'", lockIdentifier);
                lock.writeLock().unlock();
            } else {
                LOG.debug("Released readLock for '{}'", lockIdentifier);
                readLock.unlock();
            }
            ResourceLock.releaseLocker(lockIdentifier);
        }
    }

    protected void setHeader(final HttpServletResponse res, final FetchedResource fetchedResource) {
        final String contentType = fetchedResource.getContentType();
        final long contentLength = fetchedResource.getContentLength();
        final long remoteModification = fetchedResource.getLastModified();

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

        if (contentLength != -1) {
            res.setContentLength((int) contentLength);
        }

        if (remoteModification != 0) {
            res.setDateHeader(HttpHeaderConstants.LAST_MODIFIED, remoteModification);
        }
    }

    private enum LockStatus {

        READ, WRITE

    }

}