org.alfresco.repo.web.scripts.content.ContentStreamer.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.web.scripts.content.ContentStreamer.java

Source

/*
 * #%L
 * Alfresco Remote API
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.web.scripts.content;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.filestore.FileContentReader;
import org.alfresco.repo.events.EventPublisher;
import org.alfresco.repo.web.util.HttpRangeProcessor;
import org.alfresco.rest.framework.resource.content.CacheDirective;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.MimetypeService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.TempFileProvider;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import org.springframework.extensions.surf.util.URLEncoder;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
import org.springframework.util.FileCopyUtils;

/**
 * Can be used when the binary data of a content property needs to be streamed back to the client
 * as the result of executing a web script.
 * 
 * These methods are taken from the StreamContent class so they can be reused by other webscripts.
 *
 */

public class ContentStreamer implements ResourceLoaderAware {
    // Logger
    private static final Log logger = LogFactory.getLog(ContentStreamer.class);

    public static final String KEY_ALLOW_BROWSER_TO_CACHE = "allowBrowserToCache";
    public static final String KEY_CACHE_DIRECTIVE = "cacheDirective";

    /**
     * format definied by RFC 822, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3
     */
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z",
            Locale.US);

    private static final String HEADER_CONTENT_RANGE = "Content-Range";
    private static final String HEADER_CONTENT_LENGTH = "Content-Length";
    private static final String HEADER_ACCEPT_RANGES = "Accept-Ranges";
    private static final String HEADER_RANGE = "Range";
    private static final String HEADER_USER_AGENT = "User-Agent";

    /**
     * Services
     */
    // protected PermissionService permissionService;
    protected NodeService nodeService;
    protected ContentService contentService;
    protected MimetypeService mimetypeService;
    protected ResourceLoader resourceLoader;
    protected EventPublisher eventPublisher;
    protected SiteService siteService;

    /**
     * @param mimetypeService MimetypeService
     */
    public void setMimetypeService(MimetypeService mimetypeService) {
        this.mimetypeService = mimetypeService;
    }

    /**
     * @param nodeService NodeService
     */
    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    /**
     * @param eventPublisher EventPublisher
     */
    public void setEventPublisher(EventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    /**
     * @param siteService SiteService
     */
    public void setSiteService(SiteService siteService) {
        this.siteService = siteService;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    /**
     * @param contentService ContentService
     */
    public void setContentService(ContentService contentService)

    {
        this.contentService = contentService;
    }

    /**
     * Streams content back to client from a given File.
     * 
     * @param req               The request
     * @param res               The response
     * @param file              The file whose content is to be streamed.
     * @param modifiedTime      The modified datetime to use for the streamed content. If <tt>null</tt> the
     *                          file's timestamp will be used.
     * @param attach            Indicates whether the content should be streamed as an attachment or not
     * @param attachFileName    Optional file name to use when attach is <code>true</code>
     * @throws IOException
     */
    public void streamContent(WebScriptRequest req, WebScriptResponse res, File file, Long modifiedTime,
            boolean attach, String attachFileName, Map<String, Object> model) throws IOException {
        if (logger.isDebugEnabled())
            logger.debug("Retrieving content from file " + file.getAbsolutePath() + " (attach: " + attach + ")");

        // determine mimetype from file extension
        String filePath = file.getAbsolutePath();
        String mimetype = MimetypeMap.MIMETYPE_BINARY;
        int extIndex = filePath.lastIndexOf('.');
        if (extIndex != -1) {
            mimetype = mimetypeService.getMimetype(filePath.substring(extIndex + 1));
        }

        // setup file reader and stream
        FileContentReader reader = new FileContentReader(file);
        reader.setMimetype(mimetype);
        reader.setEncoding("UTF-8");

        long lastModified = modifiedTime == null ? file.lastModified() : modifiedTime;
        Date lastModifiedDate = new Date(lastModified);

        streamContentImpl(req, res, reader, null, null, attach, lastModifiedDate,
                String.valueOf(lastModifiedDate.getTime()), attachFileName, model);
    }

    /**
     * Streams the content on a given node's content property to the response of the web script.
     *
     * @param req            Request
     * @param res            Response
     * @param nodeRef        The node reference
     * @param propertyQName  The content property name
     * @param attach         Indicates whether the content should be streamed as an attachment or not
     * @param attachFileName Optional file name to use when attach is <code>true</code>
     * @throws IOException
     */
    public void streamContent(WebScriptRequest req, WebScriptResponse res, NodeRef nodeRef, QName propertyQName,
            boolean attach, String attachFileName, Map<String, Object> model) throws IOException {
        if (logger.isDebugEnabled())
            logger.debug("Retrieving content from node ref " + nodeRef.toString() + " (property: "
                    + propertyQName.toString() + ") (attach: " + attach + ")");

        // TODO
        // This was commented out to accomadate records management permissions.  We need to review how we cope with this
        // hard coded permission checked.

        // check that the user has at least READ_CONTENT access - else redirect to the login page
        //        if (permissionService.hasPermission(nodeRef, PermissionService.READ_CONTENT) == AccessStatus.DENIED)
        //        {
        //            throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Permission denied");
        //        }

        // check If-Modified-Since header and set Last-Modified header as appropriate
        Date modified = (Date) nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED);
        if (modified != null) {
            long modifiedSince = -1;
            String modifiedSinceStr = req.getHeader("If-Modified-Since");
            if (modifiedSinceStr != null) {
                try {
                    modifiedSince = dateFormat.parse(modifiedSinceStr).getTime();
                } catch (Throwable e) {
                    if (logger.isInfoEnabled())
                        logger.info("Browser sent badly-formatted If-Modified-Since header: " + modifiedSinceStr);
                }

                if (modifiedSince > 0L) {
                    // round the date to the ignore millisecond value which is not supplied by header
                    long modDate = (modified.getTime() / 1000L) * 1000L;
                    if (modDate <= modifiedSince) {
                        res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        return;
                    }
                }
            }
        }

        // get the content reader
        ContentReader reader = contentService.getReader(nodeRef, propertyQName);
        if (reader == null || !reader.exists()) {
            throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to locate content for node ref "
                    + nodeRef + " (property: " + propertyQName.toString() + ")");
        }

        // Stream the content
        streamContentImpl(req, res, reader, nodeRef, propertyQName, attach, modified,
                modified == null ? null : Long.toString(modified.getTime()), attachFileName, model);
    }

    /**
     * Streams content back to client from a given resource path.
     * 
     * @param req               The request
     * @param res               The response
     * @param resourcePath      The classpath resource path the content is required for
     * @param attach            Indicates whether the content should be streamed as an attachment or not
     * @throws IOException
     */
    public void streamContent(WebScriptRequest req, WebScriptResponse res, String resourcePath, boolean attach,
            Map<String, Object> model) throws IOException {
        streamContent(req, res, resourcePath, attach, null, model);
    }

    /**
     * Streams content back to client from a given resource path.
     * 
     * @param req               The request
     * @param res               The response
     * @param resourcePath      The classpath resource path the content is required for.
     * @param attach            Indicates whether the content should be streamed as an attachment or not
     * @param attachFileName    Optional file name to use when attach is <code>true</code>
     * @throws IOException
     */
    protected void streamContent(WebScriptRequest req, WebScriptResponse res, String resourcePath, boolean attach,
            String attachFileName, Map<String, Object> model) throws IOException {
        if (logger.isDebugEnabled())
            logger.debug("Retrieving content from resource path " + resourcePath + " (attach: " + attach + ")");

        // get extension of resource
        String ext = "";
        int extIndex = resourcePath.lastIndexOf('.');
        if (extIndex != -1) {
            ext = resourcePath.substring(extIndex);
        }

        // We need to retrieve the modification date/time from the resource itself.
        StringBuilder sb = new StringBuilder("classpath:").append(resourcePath);
        final String classpathResource = sb.toString();

        long resourceLastModified = resourceLoader.getResource(classpathResource).lastModified();

        // create temporary file 
        File file = TempFileProvider.createTempFile("streamContent-", ext);

        InputStream is = resourceLoader.getResource(classpathResource).getInputStream();
        OutputStream os = new FileOutputStream(file);
        FileCopyUtils.copy(is, os);

        // stream the contents of the file, but using the modifiedDate of the original resource.
        streamContent(req, res, file, resourceLastModified, attach, attachFileName, model);
    }

    /**
     * Stream content implementation
     * 
     * @param req               The request
     * @param res               The response
     * @param reader            The reader
     * @param nodeRef           The content nodeRef if applicable
     * @param propertyQName     The content property if applicable
     * @param attach            Indicates whether the content should be streamed as an attachment or not
     * @param modified          Modified date of content
     * @param eTag              ETag to use
     * @param attachFileName    Optional file name to use when attach is <code>true</code>
     * @throws IOException
     */
    public void streamContentImpl(WebScriptRequest req, WebScriptResponse res, ContentReader reader,
            final NodeRef nodeRef, final QName propertyQName, final boolean attach, final Date modified,
            String eTag, final String attachFileName, Map<String, Object> model) throws IOException {
        setAttachment(req, res, attach, attachFileName);

        // establish mimetype
        String mimetype = reader.getMimetype();
        String extensionPath = req.getExtensionPath();
        if (mimetype == null || mimetype.length() == 0) {
            mimetype = MimetypeMap.MIMETYPE_BINARY;
            int extIndex = extensionPath.lastIndexOf('.');
            if (extIndex != -1) {
                String ext = extensionPath.substring(extIndex + 1);
                mimetype = mimetypeService.getMimetype(ext);
            }
        }

        res.setHeader(HEADER_ACCEPT_RANGES, "bytes");
        try {
            boolean processedRange = false;
            String range = req.getHeader(HEADER_CONTENT_RANGE);
            final long size = reader.getSize();
            final String encoding = reader.getEncoding();

            //            if (attach)
            //            {
            //                final String finalMimetype = mimetype;
            //                
            //                eventPublisher.publishEvent(new EventPreparator(){
            //                    @Override
            //                    public Event prepareEvent(String user, String networkId, String transactionId)
            //                    {
            //                        String siteId = siteService.getSiteShortName(nodeRef);
            //                        
            //                        return new ContentEventImpl(ContentEvent.DOWNLOAD, user, networkId, transactionId,
            //                                    nodeRef.getId(), siteId, propertyQName.toString(), Client.asType(ClientType.webclient), attachFileName, finalMimetype, size, encoding);
            //                    }
            //                });
            //            }

            if (range == null) {
                range = req.getHeader(HEADER_RANGE);
            }
            if (range != null) {
                if (logger.isDebugEnabled())
                    logger.debug("Found content range header: " + range);

                // ensure the range header is starts with "bytes=" and process the range(s)
                if (range.length() > 6) {
                    if (range.indexOf(',') != -1 && (nodeRef == null || propertyQName == null)) {
                        if (logger.isInfoEnabled())
                            logger.info("Multi-range only supported for nodeRefs");
                    } else {
                        HttpRangeProcessor rangeProcessor = new HttpRangeProcessor(contentService);
                        processedRange = rangeProcessor.processRange(res, reader, range.substring(6), nodeRef,
                                propertyQName, mimetype, req.getHeader(HEADER_USER_AGENT));
                    }
                }
            }
            if (processedRange == false) {
                if (logger.isDebugEnabled())
                    logger.debug("Sending complete file content...");

                // set mimetype for the content and the character encoding for the stream
                res.setContentType(mimetype);
                res.setContentEncoding(encoding);

                // return the complete entity range
                res.setHeader(HEADER_CONTENT_RANGE,
                        "bytes 0-" + Long.toString(size - 1L) + "/" + Long.toString(size));
                res.setHeader(HEADER_CONTENT_LENGTH, Long.toString(size));

                // set caching
                setResponseCache(res, modified, eTag, model);

                // get the content and stream directly to the response output stream
                // assuming the repository is capable of streaming in chunks, this should allow large files
                // to be streamed directly to the browser response stream.
                reader.getContent(res.getOutputStream());
            }
        } catch (SocketException e1) {
            // the client cut the connection - our mission was accomplished apart from a little error message
            if (logger.isInfoEnabled())
                logger.info("Client aborted stream read:\n\tcontent: " + reader);
        } catch (ContentIOException e2) {
            if (logger.isInfoEnabled())
                logger.info("Client aborted stream read:\n\tcontent: " + reader);
        }
    }

    /**
     * Set attachment header
     * 
     * @param req WebScriptRequest
     * @param res WebScriptResponse
     * @param attach boolean
     * @param attachFileName String
     */
    public void setAttachment(WebScriptRequest req, WebScriptResponse res, boolean attach, String attachFileName) {
        if (attach == true) {
            String headerValue = "attachment";
            if (attachFileName != null && attachFileName.length() > 0) {
                if (logger.isDebugEnabled())
                    logger.debug("Attaching content using filename: " + attachFileName);

                if (req == null) {
                    headerValue += "; filename*=UTF-8''" + URLEncoder.encode(attachFileName) + "; filename=\""
                            + attachFileName + "\"";
                } else {
                    String userAgent = req.getHeader(HEADER_USER_AGENT);
                    boolean isLegacy = (null != userAgent)
                            && (userAgent.contains("MSIE 8") || userAgent.contains("MSIE 7"));
                    if (isLegacy) {
                        headerValue += "; filename=\"" + URLEncoder.encode(attachFileName);
                    } else {
                        headerValue += "; filename=\"" + attachFileName + "\"; filename*=UTF-8''"
                                + URLEncoder.encode(attachFileName);
                    }
                }
            }

            // set header based on filename - will force a Save As from the browse if it doesn't recognize it
            // this is better than the default response of the browser trying to display the contents
            res.setHeader("Content-Disposition", headerValue);
        }
    }

    /**
     * Set the cache settings on the response
     * 
     * @param res WebScriptResponse
     * @param modified Date
     * @param eTag String
     */
    protected void setResponseCache(WebScriptResponse res, Date modified, String eTag, Map<String, Object> model) {
        Cache cache = new Cache();

        Object obj;
        if (model != null && (obj = model.get(KEY_CACHE_DIRECTIVE)) instanceof CacheDirective) {
            CacheDirective cacheDirective = (CacheDirective) obj;
            cache.setNeverCache(cacheDirective.isNeverCache());
            cache.setMustRevalidate(cacheDirective.isMustRevalidate());
            cache.setMaxAge(cacheDirective.getMaxAge());
            cache.setLastModified(cacheDirective.getLastModified());
            cache.setETag(cacheDirective.getETag());
            cache.setIsPublic(cacheDirective.isPublic());
        } else if (model == null || !getBooleanValue(model.get(KEY_ALLOW_BROWSER_TO_CACHE))) {
            // if 'allowBrowserToCache' is null or false
            cache.setNeverCache(false);
            cache.setMustRevalidate(true);
            cache.setMaxAge(0L);
            cache.setLastModified(modified);
            cache.setETag(eTag);
        } else {
            cache.setNeverCache(false);
            cache.setMustRevalidate(false);
            cache.setMaxAge(Long.valueOf(31536000));// one year
            cache.setLastModified(modified);
            cache.setETag(eTag);
        }

        res.setCache(cache);
    }

    private boolean getBooleanValue(Object obj) {
        if (obj instanceof String) {
            return Boolean.valueOf((String) obj);
        }
        return Boolean.TRUE.equals(obj);
    }
}