org.dspace.app.xmlui.cocoon.BitstreamReader.java Source code

Java tutorial

Introduction

Here is the source code for org.dspace.app.xmlui.cocoon.BitstreamReader.java

Source

/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.xmlui.cocoon;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.sql.SQLException;
import java.util.Map;

import javax.mail.internet.MimeUtility;
import javax.servlet.http.HttpServletResponse;

import org.apache.avalon.excalibur.pool.Recyclable;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.ResourceNotFoundException;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.environment.Response;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.cocoon.environment.http.HttpEnvironment;
import org.apache.cocoon.environment.http.HttpResponse;
import org.apache.cocoon.reading.AbstractReader;
import org.apache.cocoon.util.ByteRange;
import org.apache.commons.lang.StringUtils;
import org.dspace.app.xmlui.utils.AuthenticationUtil;
import org.dspace.app.xmlui.utils.ContextUtil;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.AuthorizeManager;
import org.dspace.authorize.ResourcePolicy;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.handle.HandleManager;
import org.dspace.usage.UsageEvent;
import org.dspace.utils.DSpace;
import org.xml.sax.SAXException;

import org.apache.log4j.Logger;
import org.dspace.core.LogManager;

/**
 * The BitstreamReader will query DSpace for a particular bitstream and transmit
 * it to the user. There are several methods of specifing the bitstream to be
 * delivered. You may reference a bitstream by either it's id or attempt to
 * resolve the bitstream's name.
 *
 *  /bitstream/{handle}/{sequence}/{name}
 *
 *  <map:read type="BitstreamReader">
 *    <map:parameter name="handle" value="{1}/{2}"/>
 *    <map:parameter name="sequence" value="{3}"/>
 *    <map:parameter name="name" value="{4}"/>
 *  </map:read>
 *
 *  When no handle is assigned yet you can access a bitstream
 *  using it's internal ID.
 *
 *  /bitstream/id/{bitstreamID}/{sequence}/{name}
 *
 *  <map:read type="BitstreamReader">
 *    <map:parameter name="bitstreamID" value="{1}"/>
 *    <map:parameter name="sequence" value="{2}"/>
 *  </map:read>
 *
 *  Alternatively, you can access the bitstream via a name instead
 *  of directly through it's sequence.
 *
 *  /html/{handle}/{name}
 *
 *  <map:read type="BitstreamReader">
 *    <map:parameter name="handle" value="{1}/{2}"/>
 *    <map:parameter name="name" value="{3}"/>
 *  </map:read>
 *
 *  Again when no handle is available you can also access it
 *  via an internal itemID & name.
 *
 *  /html/id/{itemID}/{name}
 *
 *  <map:read type="BitstreamReader">
 *    <map:parameter name="itemID" value="{1}"/>
 *    <map:parameter name="name" value="{2}"/>
 *  </map:read>
 *
 * Added request-item support. 
 * Original Concept, JSPUI version:    Universidade do Minho   at www.uminho.pt
 * Sponsorship of XMLUI version:    Instituto Oceanogrfico de Espaa at www.ieo.es
 * 
 * @author Scott Phillips
 * @author Adn Romn Ruiz at arvo.es (added request item support)
 */

public class BitstreamReader extends AbstractReader implements Recyclable {
    private static Logger log = Logger.getLogger(BitstreamReader.class);

    /**
     * Messages to be sent when the user is not authorized to view
     * a particular bitstream. They will be redirected to the login
     * where this message will be displayed.
     */
    private static final String AUTH_REQUIRED_HEADER = "xmlui.BitstreamReader.auth_header";
    private static final String AUTH_REQUIRED_MESSAGE = "xmlui.BitstreamReader.auth_message";

    /**
     * How big a buffer should we use when reading from the bitstream before
     * writing to the HTTP response?
     */
    protected static final int BUFFER_SIZE = 8192;

    /**
     * When should a bitstream expire in milliseconds. This should be set to
     * some low value just to prevent someone hiting DSpace repeatedy from
     * killing the server. Note: there are 1000 milliseconds in a second.
     *
     * Format: minutes * seconds * milliseconds
     *  60 * 60 * 1000 == 1 hour
     */
    protected static final int expires = 60 * 60 * 1000;

    /** The Cocoon response */
    protected Response response;

    /** The Cocoon request */
    protected Request request;

    /** The bitstream file */
    protected InputStream bitstreamInputStream;

    /** The bitstream's reported size */
    protected long bitstreamSize;

    /** The bitstream's mime-type */
    protected String bitstreamMimeType;

    /** The bitstream's name */
    protected String bitstreamName;

    /** True if bitstream is readable by anonymous users */
    protected boolean isAnonymouslyReadable;

    /** Item containing the Bitstream */
    private Item item = null;

    /** True if user agent making this request was identified as spider. */
    private boolean isSpider = false;

    /**
     * Set up the bitstream reader.
     *
     * See the class description for information on configuration options.
     */
    public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par)
            throws ProcessingException, SAXException, IOException {
        super.setup(resolver, objectModel, src, par);

        try {
            this.request = ObjectModelHelper.getRequest(objectModel);
            this.response = ObjectModelHelper.getResponse(objectModel);

            // Check to see if a context already exists or not. We may
            // have been aggregated into an http request by the XSL document
            // pulling in an XML-based bitstream. In this case the context has
            // already been created and we should leave it open because the
            // normal processes will close it.
            boolean BitstreamReaderOpenedContext = !ContextUtil.isContextAvailable(objectModel);
            Context context = ContextUtil.obtainContext(objectModel);

            // Get our parameters that identify the bitstream
            int itemID = par.getParameterAsInteger("itemID", -1);
            int bitstreamID = par.getParameterAsInteger("bitstreamID", -1);
            String handle = par.getParameter("handle", null);

            int sequence = par.getParameterAsInteger("sequence", -1);
            String name = par.getParameter("name", null);

            this.isSpider = par.getParameter("userAgent", "").equals("spider");

            // Resolve the bitstream
            Bitstream bitstream = null;
            DSpaceObject dso = null;

            if (bitstreamID > -1) {
                // Direct reference to the individual bitstream ID.
                bitstream = Bitstream.find(context, bitstreamID);
            } else if (itemID > -1) {
                // Referenced by internal itemID
                item = Item.find(context, itemID);

                if (sequence > -1) {
                    bitstream = findBitstreamBySequence(item, sequence);
                } else if (name != null) {
                    bitstream = findBitstreamByName(item, name);
                }
            } else if (handle != null) {
                // Reference by an item's handle.
                dso = HandleManager.resolveToObject(context, handle);

                if (dso instanceof Item) {
                    item = (Item) dso;

                    if (sequence > -1) {
                        bitstream = findBitstreamBySequence(item, sequence);
                    } else if (name != null) {
                        bitstream = findBitstreamByName(item, name);
                    }
                }
            }

            // if initial search was by sequence number and found nothing,
            // then try to find bitstream by name (assuming we have a file name)
            if ((sequence > -1 && bitstream == null) && name != null) {
                bitstream = findBitstreamByName(item, name);

                // if we found bitstream by name, send a redirect to its new sequence number location
                if (bitstream != null) {
                    String redirectURL = "";

                    // build redirect URL based on whether item has a handle assigned yet
                    if (item.getHandle() != null && item.getHandle().length() > 0) {
                        redirectURL = request.getContextPath() + "/bitstream/handle/" + item.getHandle();
                    } else {
                        redirectURL = request.getContextPath() + "/bitstream/item/" + item.getID();
                    }

                    redirectURL += "/" + name + "?sequence=" + bitstream.getSequenceID();

                    HttpServletResponse httpResponse = (HttpServletResponse) objectModel
                            .get(HttpEnvironment.HTTP_RESPONSE_OBJECT);
                    httpResponse.sendRedirect(redirectURL);
                    return;
                }
            }

            // Was a bitstream found?
            if (bitstream == null) {
                throw new ResourceNotFoundException("Unable to locate bitstream");
            }

            // Is there a User logged in and does the user have access to read it?
            boolean isAuthorized = AuthorizeManager.authorizeActionBoolean(context, bitstream, Constants.READ);
            if (item != null && item.isWithdrawn() && !AuthorizeManager.isAdmin(context)) {
                isAuthorized = false;
                log.info(LogManager.getHeader(context, "view_bitstream",
                        "handle=" + item.getHandle() + ",withdrawn=true"));
            }
            // It item-request is enabled to all request we redirect to restricted-resource immediately without login request  
            String requestItemType = ConfigurationManager.getProperty("request.item.type");
            if (!isAuthorized) {
                if (context.getCurrentUser() != null || StringUtils.equalsIgnoreCase("all", requestItemType)) {
                    // A user is logged in, but they are not authorized to read this bitstream,
                    // instead of asking them to login again we'll point them to a friendly error
                    // message that tells them the bitstream is restricted.
                    String redictURL = request.getContextPath() + "/handle/";
                    if (item != null) {
                        redictURL += item.getHandle();
                    } else if (dso != null) {
                        redictURL += dso.getHandle();
                    }
                    redictURL += "/restricted-resource?bitstreamId=" + bitstream.getID();

                    HttpServletResponse httpResponse = (HttpServletResponse) objectModel
                            .get(HttpEnvironment.HTTP_RESPONSE_OBJECT);
                    httpResponse.sendRedirect(redictURL);
                    return;
                } else {
                    if (ConfigurationManager.getProperty("request.item.type") == null
                            || ConfigurationManager.getProperty("request.item.type").equalsIgnoreCase("logged")) {
                        // The user does not have read access to this bitstream. Interrupt this current request
                        // and then forward them to the login page so that they can be authenticated. Once that is
                        // successful, their request will be resumed.
                        AuthenticationUtil.interruptRequest(objectModel, AUTH_REQUIRED_HEADER,
                                AUTH_REQUIRED_MESSAGE, null);

                        // Redirect
                        String redictURL = request.getContextPath() + "/login";

                        HttpServletResponse httpResponse = (HttpServletResponse) objectModel
                                .get(HttpEnvironment.HTTP_RESPONSE_OBJECT);
                        httpResponse.sendRedirect(redictURL);
                        return;
                    }
                }
            }

            // Success, bitstream found and the user has access to read it.
            // Store these for later retrieval:
            this.bitstreamInputStream = bitstream.retrieve();
            this.bitstreamSize = bitstream.getSize();
            this.bitstreamMimeType = bitstream.getFormat().getMIMEType();
            this.bitstreamName = bitstream.getName();
            if (context.getCurrentUser() == null) {
                this.isAnonymouslyReadable = true;
            } else {
                this.isAnonymouslyReadable = false;
                for (ResourcePolicy rp : AuthorizeManager.getPoliciesActionFilter(context, bitstream,
                        Constants.READ)) {
                    if (rp.getGroupID() == 0) {
                        this.isAnonymouslyReadable = true;
                    }
                }
            }

            // Trim any path information from the bitstream
            if (bitstreamName != null && bitstreamName.length() > 0) {
                int finalSlashIndex = bitstreamName.lastIndexOf('/');
                if (finalSlashIndex > 0) {
                    bitstreamName = bitstreamName.substring(finalSlashIndex + 1);
                }
            } else {
                // In case there is no bitstream name...
                bitstreamName = "bitstream";
            }

            // Log that the bitstream has been viewed, this is non-cached and the complexity
            // of adding it to the sitemap for every possible bitstream uri is not very tractable
            new DSpace().getEventService()
                    .fireEvent(new UsageEvent(UsageEvent.Action.VIEW, ObjectModelHelper.getRequest(objectModel),
                            ContextUtil.obtainContext(ObjectModelHelper.getRequest(objectModel)), bitstream));

            // If we created the database connection close it, otherwise leave it open.
            if (BitstreamReaderOpenedContext)
                context.complete();
        } catch (SQLException sqle) {
            throw new ProcessingException("Unable to read bitstream.", sqle);
        } catch (AuthorizeException ae) {
            throw new ProcessingException("Unable to read bitstream.", ae);
        }
    }

    /**
     * Find the bitstream identified by a sequence number on this item.
     *
     * @param item A DSpace item
     * @param sequence The sequence of the bitstream
     * @return The bitstream or null if none found.
     */
    private Bitstream findBitstreamBySequence(Item item, int sequence) throws SQLException {
        if (item == null) {
            return null;
        }

        Bundle[] bundles = item.getBundles();
        for (Bundle bundle : bundles) {
            Bitstream[] bitstreams = bundle.getBitstreams();

            for (Bitstream bitstream : bitstreams) {
                if (bitstream.getSequenceID() == sequence) {
                    return bitstream;
                }
            }
        }
        return null;
    }

    /**
     * Return the bitstream from the given item that is identified by the
     * given name. If the name has prepended directories they will be removed
     * one at a time until a bitstream is found. Note that if two bitstreams
     * have the same name then the first bitstream will be returned.
     *
     * @param item A DSpace item
     * @param name The name of the bitstream
     * @return The bitstream or null if none found.
     */
    private Bitstream findBitstreamByName(Item item, String name) throws SQLException {
        if (name == null || item == null) {
            return null;
        }

        // Determine our the maximum number of directories that will be removed for a path.
        int maxDepthPathSearch = 3;
        if (ConfigurationManager.getProperty("xmlui.html.max-depth-guess") != null) {
            maxDepthPathSearch = ConfigurationManager.getIntProperty("xmlui.html.max-depth-guess");
        }

        // Search for the named bitstream on this item. Each time through the loop
        // a directory is removed from the name until either our maximum depth is
        // reached or the bitstream is found. Note: an extra pass is added on to the
        // loop for a last ditch effort where all directory paths will be removed.
        for (int i = 0; i < maxDepthPathSearch + 1; i++) {
            // Search through all the bitstreams and see
            // if the name can be found
            Bundle[] bundles = item.getBundles();
            for (Bundle bundle : bundles) {
                Bitstream[] bitstreams = bundle.getBitstreams();

                for (Bitstream bitstream : bitstreams) {
                    if (name.equals(bitstream.getName())) {
                        return bitstream;
                    }
                }
            }

            // The bitstream was not found, so try removing a directory
            // off of the name and see if we lost some path information.
            int indexOfSlash = name.indexOf('/');

            if (indexOfSlash < 0) {
                // No more directories to remove from the path, so return null for no
                // bitstream found.
                return null;
            }

            name = name.substring(indexOfSlash + 1);

            // If this is our next to last time through the loop then
            // trim everything and only use the trailing filename.
            if (i == maxDepthPathSearch - 1) {
                int indexOfLastSlash = name.lastIndexOf('/');
                if (indexOfLastSlash > -1) {
                    name = name.substring(indexOfLastSlash + 1);
                }
            }

        }

        // The named bitstream was not found and we exhausted the maximum path depth that
        // we search.
        return null;
    }

    /**
     * Write the actual data out to the response.
     *
     * Some implementation notes:
     *
     * 1) We set a short expiration time just in the hopes of preventing someone
     * from overloading the server by clicking reload a bunch of times. I
     * Realize that this is nowhere near 100% effective but it may help in some
     * cases and shouldn't hurt anything.
     *
     * 2) We accept partial downloads, thus if you lose a connection halfway
     * through most web browser will enable you to resume downloading the
     * bitstream.
     */
    public void generate() throws IOException, SAXException, ProcessingException {
        if (this.bitstreamInputStream == null) {
            return;
        }

        // Only allow If-Modified-Since protocol if request is from a spider
        // since response headers would encourage a browser to cache results
        // that might change with different authentication.
        if (isSpider) {
            // Check for if-modified-since header -- ONLY if not authenticated
            long modSince = request.getDateHeader("If-Modified-Since");
            if (modSince != -1 && item != null && item.getLastModified().getTime() < modSince) {
                // Item has not been modified since requested date,
                // hence bitstream has not been, either; return 304
                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                return;
            }
        }

        // Only set Last-Modified: header for spiders or anonymous
        // access, since it might encourage browse to cache the result
        // which might leave a result only available to authenticated
        // users in the cache for a response later to anonymous user.
        try {
            if (item != null && (isSpider || ContextUtil.obtainContext(request).getCurrentUser() == null)) {
                // TODO:  Currently just borrow the date of the item, since
                // we don't have last-mod dates for Bitstreams
                response.setDateHeader("Last-Modified", item.getLastModified().getTime());
            }
        } catch (SQLException e) {
            throw new ProcessingException(e);
        }

        byte[] buffer = new byte[BUFFER_SIZE];
        int length = -1;

        // Only encourage caching if this is not a restricted resource, i.e.
        // if it is accessed anonymously or is readable by Anonymous:
        if (isAnonymouslyReadable) {
            response.setDateHeader("Expires", System.currentTimeMillis() + expires);
        }

        // If this is a large bitstream then tell the browser it should treat it as a download.
        int threshold = ConfigurationManager.getIntProperty("xmlui.content_disposition_threshold");
        if (bitstreamSize > threshold && threshold != 0) {
            String name = bitstreamName;

            // Try and make the download file name formatted for each browser.
            try {
                String agent = request.getHeader("USER-AGENT");
                if (agent != null && agent.contains("MSIE")) {
                    name = URLEncoder.encode(name, "UTF8");
                } else if (agent != null && agent.contains("Mozilla")) {
                    name = MimeUtility.encodeText(name, "UTF8", "B");
                }
            } catch (UnsupportedEncodingException see) {
                // do nothing
            }
            response.setHeader("Content-Disposition", "attachment;filename=" + '"' + name + '"');
        }

        ByteRange byteRange = null;

        // Turn off partial downloads, they cause problems
        // and are only rarely used. Specifically some windows pdf
        // viewers are incapable of handling this request. You can
        // uncomment the following lines to turn this feature back on.

        //        response.setHeader("Accept-Ranges", "bytes");
        //        String ranges = request.getHeader("Range");
        //        if (ranges != null)
        //        {
        //            try
        //            {
        //                ranges = ranges.substring(ranges.indexOf('=') + 1);
        //                byteRange = new ByteRange(ranges);
        //            }
        //            catch (NumberFormatException e)
        //            {
        //                byteRange = null;
        //                if (response instanceof HttpResponse)
        //                {
        //                    // Respond with status 416 (Request range not
        //                    // satisfiable)
        //                    response.setStatus(416);
        //                }
        //            }
        //        }

        try {
            if (byteRange != null) {
                String entityLength;
                String entityRange;
                if (this.bitstreamSize != -1) {
                    entityLength = "" + this.bitstreamSize;
                    entityRange = byteRange.intersection(new ByteRange(0, this.bitstreamSize)).toString();
                } else {
                    entityLength = "*";
                    entityRange = byteRange.toString();
                }

                response.setHeader("Content-Range", entityRange + "/" + entityLength);
                if (response instanceof HttpResponse) {
                    // Response with status 206 (Partial content)
                    response.setStatus(206);
                }

                int pos = 0;
                int posEnd;
                while ((length = this.bitstreamInputStream.read(buffer)) > -1) {
                    posEnd = pos + length - 1;
                    ByteRange intersection = byteRange.intersection(new ByteRange(pos, posEnd));
                    if (intersection != null) {
                        out.write(buffer, (int) intersection.getStart() - pos, (int) intersection.length());
                    }
                    pos += length;
                }
            } else {
                response.setHeader("Content-Length", String.valueOf(this.bitstreamSize));

                while ((length = this.bitstreamInputStream.read(buffer)) > -1) {
                    out.write(buffer, 0, length);
                }
                out.flush();
            }
        } finally {
            try {
                // Close the bitstream input stream so that we don't leak a file descriptor
                this.bitstreamInputStream.close();

                // Close the output stream as per Cocoon docs: http://cocoon.apache.org/2.2/core-modules/core/2.2/681_1_1.html
                out.close();
            } catch (IOException ioe) {
                // Closing the stream threw an IOException but do we want this to propagate up to Cocoon?
                // No point since the user has already got the bitstream contents.
                log.warn("Caught IO exception when closing a stream: " + ioe.getMessage());
            }
        }

    }

    /**
     * Returns the mime-type of the bitstream.
     */
    public String getMimeType() {
        return this.bitstreamMimeType;
    }

    /**
     * Recycle
     */
    public void recycle() {
        this.response = null;
        this.request = null;
        this.bitstreamInputStream = null;
        this.bitstreamSize = 0;
        this.bitstreamMimeType = null;
    }

}