org.jahia.services.content.files.FileServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.services.content.files.FileServlet.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     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/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.jahia.services.content.files;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;

import javax.jcr.*;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.catalina.servlets.RangeUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException;
import org.apache.jackrabbit.util.Text;
import org.jahia.api.Constants;
import org.jahia.exceptions.JahiaRuntimeException;
import org.jahia.services.SpringContextSingleton;
import org.jahia.services.cache.Cache;
import org.jahia.services.content.*;
import org.jahia.services.logging.MetricsLoggingService;
import org.jahia.services.render.filter.ContextPlaceholdersReplacer;
import org.jahia.services.usermanager.JahiaUserManagerService;
import org.jahia.services.visibility.VisibilityService;
import org.jahia.settings.SettingsBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;

import static org.jahia.services.content.JCRTemplate.*;

/**
 * Serves resources from the JCR repository.
 *
 * @author Thomas Draier
 * Date: Oct 13, 2008
 * Time: 2:08:59 PM
 */
public class FileServlet extends HttpServlet {

    private static Logger logger = LoggerFactory.getLogger(FileServlet.class);

    private static final long serialVersionUID = -414690364676304370L;

    private transient FileCacheManager cacheManager;

    private int cacheThreshold = 64 * 1024;

    private boolean cacheForLoggedUsers = true;

    private boolean cacheFromExternalProviders;

    private String characterEncoding = null;

    private transient MetricsLoggingService loggingService;

    private transient JCRSessionFactory sessionFactory;

    private boolean canCache(JCRNodeWrapper n) {
        return cacheFromExternalProviders || n.getProvider().canCacheNode(n);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {

        long timer = System.currentTimeMillis();
        int code = HttpServletResponse.SC_OK;
        try {
            FileKey fileKey = parseKey(req);

            if (fileKey != null && fileKey.getWorkspace() != null && StringUtils.isNotEmpty(fileKey.getPath())) {

                Cache<String, FileLastModifiedCacheEntry> lastModifiedCache = cacheManager.getLastModifiedCache();

                FileLastModifiedCacheEntry lastModifiedEntry = lastModifiedCache.get(fileKey.getCacheKey());
                if (isNotModified(fileKey, lastModifiedEntry, req, res)) {
                    // resource is not changed
                    code = HttpServletResponse.SC_NOT_MODIFIED;
                    res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                    logAccess(fileKey, req, "ok-not-modified");
                    return;
                }

                Cache<String, Map<String, FileCacheEntry>> contentCache = cacheManager.getContentCache();

                Map<String, FileCacheEntry> entries = contentCache.get(fileKey.getCacheKey());
                FileCacheEntry fileEntry = entries != null ? entries.get(fileKey.getThumbnail()) : null;
                if (fileEntry == null) {
                    JCRNodeWrapper n = getNode(fileKey);
                    if (n == null || !n.isFile()) {
                        // cannot find it or it is not a file
                        code = HttpServletResponse.SC_NOT_FOUND;
                        res.sendError(HttpServletResponse.SC_NOT_FOUND);
                        return;
                    }

                    Date lastModifiedDate = n.getLastModifiedAsDate();
                    long lastModified = lastModifiedDate != null ? lastModifiedDate.getTime() : 0;
                    String eTag = generateETag(n.getIdentifier(), lastModified);
                    if (lastModifiedEntry == null) {
                        lastModifiedEntry = new FileLastModifiedCacheEntry(eTag, lastModified);
                        if (canCache(n)) {
                            lastModifiedCache.put(fileKey.getCacheKey(), lastModifiedEntry);
                        }
                    }

                    if (isNotModified(fileKey, lastModifiedEntry, req, res)) {
                        // resource is not changed
                        code = HttpServletResponse.SC_NOT_MODIFIED;
                        res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                        logAccess(fileKey, req, "ok-not-modified");
                        return;
                    }

                    fileEntry = getFileEntry(fileKey, n, lastModifiedEntry);
                    if (fileEntry != null && fileEntry.getData() != null) {
                        entries = contentCache.get(fileKey.getCacheKey());
                        if (entries == null) {
                            entries = new HashMap<String, FileCacheEntry>(1);
                        }
                        entries.put(fileKey.getThumbnail(), fileEntry);
                        contentCache.put(fileKey.getCacheKey(), entries);
                        logAccess(fileKey, req, "ok");
                    }
                } else {
                    if (lastModifiedEntry == null) {
                        lastModifiedEntry = new FileLastModifiedCacheEntry(fileEntry.getETag(),
                                fileEntry.getLastModified());
                        lastModifiedCache.put(fileKey.getCacheKey(), lastModifiedEntry);
                    }
                    logAccess(fileKey, req, "ok-cached");
                    if (logger.isDebugEnabled()) {
                        logger.debug("Serving cached file entry {}", fileKey.toString());
                    }
                }

                if (fileEntry != null) {
                    List<RangeUtils.Range> ranges;
                    boolean useRanges = true;
                    if (fileEntry.getBinary() instanceof BinaryRangesSupport) {
                        useRanges = ((BinaryRangesSupport) fileEntry.getBinary()).supportRanges();
                    }

                    ranges = useRanges ? RangeUtils.parseRange(req, res, fileEntry.getETag(),
                            fileEntry.getLastModified(), fileEntry.getContentLength()) : null;

                    if (fileKey.getPath().indexOf('%', fileKey.getPath().lastIndexOf('/')) != -1) {
                        res.setHeader("Content-Disposition",
                                "inline; filename=\"" + JCRContentUtils.unescapeLocalNodeName(
                                        StringUtils.substringAfterLast(fileKey.getPath(), "/")) + "\"");
                    }
                    res.setDateHeader("Last-Modified", fileEntry.getLastModified());
                    res.setHeader("ETag", fileEntry.getETag());
                    InputStream is = null;

                    if (fileEntry.getData() != null) {
                        // writing in-memory data
                        is = new ByteArrayInputStream(fileEntry.getData());
                    } else if (fileEntry.getBinary() != null) {
                        // spool from an input stream
                        is = fileEntry.getBinary().getStream();
                    } else {
                        code = HttpServletResponse.SC_NOT_FOUND;
                        res.sendError(HttpServletResponse.SC_NOT_FOUND);
                        return;
                    }

                    if (ranges == null || (ranges == RangeUtils.FULL)) {
                        res.setContentType(fileEntry.getMimeType());
                        if (fileEntry.getContentLength() <= Integer.MAX_VALUE) {
                            res.setContentLength((int) fileEntry.getContentLength());
                        } else {
                            res.setHeader("Content-Length", Long.toString(fileEntry.getContentLength()));
                        }
                        ServletOutputStream os = res.getOutputStream();
                        IOUtils.copy(is, os);
                        os.flush();
                        os.close();
                    } else {
                        res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                        if (ranges.size() == 1) {
                            res.setContentType(fileEntry.getMimeType());
                            RangeUtils.Range range = (RangeUtils.Range) ranges.get(0);
                            res.addHeader("Content-Range",
                                    "bytes " + range.start + "-" + range.end + "/" + range.length);
                            long length = range.end - range.start + 1;
                            if (length < Integer.MAX_VALUE) {
                                res.setContentLength((int) length);
                            } else {
                                // Set the content-length as String to be able to use a long
                                res.setHeader("Content-Length", "" + length);
                            }
                            ServletOutputStream os = res.getOutputStream();
                            RangeUtils.copy(is, os, range);
                            IOUtils.closeQuietly(is);
                            IOUtils.closeQuietly(os);

                        } else {
                            res.setContentType("multipart/byteranges; boundary=" + RangeUtils.MIME_SEPARATION);

                            try {
                                res.setBufferSize(RangeUtils.getOutput());
                            } catch (IllegalStateException e) {
                                // Silent catch
                            }
                            ServletOutputStream os = res.getOutputStream();
                            RangeUtils.copy(is, os, ranges.iterator(), fileEntry.getMimeType());
                            IOUtils.closeQuietly(is);
                            IOUtils.closeQuietly(os);
                        }
                    }
                    if ((fileEntry.getData() == null) && (fileEntry.getBinary() != null)) {
                        fileEntry.getBinary().dispose();
                        fileEntry.setBinary(null);
                    }
                    SpringContextSingleton.getInstance()
                            .publishEvent(new FileDownloadEvent(this, req, fileEntry.getIdentifier(),
                                    fileKey.getPath(), fileEntry.getNodeTypes(), fileKey.getWorkspace()));
                } else {
                    code = HttpServletResponse.SC_NOT_FOUND;
                    res.sendError(HttpServletResponse.SC_NOT_FOUND);
                    return;
                }
            } else {
                code = HttpServletResponse.SC_NOT_FOUND;
                res.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
        } catch (RepositoryException e) {
            logger.error("Cannot get file", e);

            code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
            res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("Served [{}] with status code [{}] in [{}ms]",
                        new Object[] {
                                req.getRequestURI()
                                        + (req.getQueryString() != null ? "?" + req.getQueryString() : ""),
                                code, (System.currentTimeMillis() - timer) });
            }
        }
    }

    protected String generateETag(String uuid, long lastModified) {
        return "\"" + StringUtils.replace(StringUtils.defaultIfEmpty(uuid, "unknown"), "\"", "") + "-"
                + lastModified + "\"";
    }

    protected JCRNodeWrapper getContentNode(JCRNodeWrapper n, String thumbnail) throws RepositoryException {

        JCRNodeWrapper content;
        thumbnail = Text.escapeIllegalJcrChars(thumbnail);
        if (StringUtils.isNotEmpty(thumbnail) && n.hasNode(thumbnail)) {
            // thumbnail requested -> try to find it
            content = n.getNode(thumbnail);
            if (!content.isNodeType(Constants.NT_RESOURCE)) {
                content = null;
            }
        } else {
            try {
                content = n.getNode(Constants.JCR_CONTENT);
            } catch (PathNotFoundException e) {
                logger.warn("Cannot find " + Constants.JCR_CONTENT + " sub-node in the {} node.", n.getPath());
                content = null;
            }
        }

        return content;
    }

    protected FileCacheEntry getFileEntry(FileKey fileKey, JCRNodeWrapper node,
            FileLastModifiedCacheEntry lastModifiedEntry) throws RepositoryException, IOException {

        FileCacheEntry fileEntry = null;

        JCRNodeWrapper content = getContentNode(node, fileKey.getThumbnail());
        if (content == null) {
            return null;
        }

        Binary binary = null;
        try {
            binary = content.getProperty(Constants.JCR_DATA).getBinary();
        } catch (PathNotFoundException e) {
            logger.warn("Unable to get " + Constants.JCR_DATA + " property for node {}", content.getPath());
            return null;
        }

        int contentLength = (int) binary.getSize();

        fileEntry = new FileCacheEntry(lastModifiedEntry.getETag(),
                content.getProperty(Constants.JCR_MIMETYPE).getString(), contentLength,
                lastModifiedEntry.getLastModified(), node.getIdentifier(), node.getNodeTypes());
        if (contentLength <= cacheThreshold && canCache(node) && isVisibleForGuest(node)) {
            InputStream is = null;
            try {
                is = binary.getStream();
                fileEntry.setData(IOUtils.toByteArray(is));
            } finally {
                IOUtils.closeQuietly(is);
                binary.dispose();
            }
        } else {
            fileEntry.setBinary(binary);
        }

        return fileEntry;
    }

    protected JCRNodeWrapper getNode(FileKey fileKey) {
        JCRNodeWrapper n = null;
        JCRSessionWrapper session = null;
        try {
            session = JCRSessionFactory.getInstance().getCurrentUserSession(fileKey.getWorkspace());

            if (fileKey.getVersionDate() != null) {
                session.setVersionDate(new Date(Long.valueOf(fileKey.getVersionDate())));
            }
            if (fileKey.getVersionLabel() != null) {
                session.setVersionLabel(fileKey.getVersionLabel());
            }

            n = session.getNode(fileKey.getPath());

            if (!isValid(n)) {
                n = null;
            }
        } catch (RuntimeException e) {
            // throw by the session.setVersionLabel()
            logger.debug(e.getMessage(), e);
        } catch (PathNotFoundException e) {
            logger.debug(e.getMessage(), e);
        } catch (RepositoryException e) {
            if (e.getCause() != null && e.getCause() instanceof MalformedPathException) {
                logger.debug(e.getMessage(), e);
            } else {
                logger.error("Error accesing path: " + fileKey.getPath() + " for user "
                        + (session != null ? session.getUserID() : null), e);
            }
        }
        return n;
    }

    private boolean isValid(JCRNodeWrapper n)
            throws ValueFormatException, PathNotFoundException, RepositoryException {
        if (!Constants.LIVE_WORKSPACE.equals(n.getSession().getWorkspace().getName())) {
            // we check validity only in live workspace
            return true;
        }

        // the file node should be published and visible
        return (!n.hasProperty("j:published") || n.getProperty("j:published").getBoolean())
                && VisibilityService.getInstance().matchesConditions(n);
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        characterEncoding = SettingsBean.getInstance().getCharacterEncoding();
        String value = config.getInitParameter("cache-threshold");
        if (value != null) {
            cacheThreshold = new Integer(value);
        }

        value = config.getInitParameter("cache-for-logged-in-users");
        if (value != null) {
            cacheForLoggedUsers = Boolean.parseBoolean(value);
        }

        value = config.getInitParameter("cache-from-external-providers");
        if (value != null) {
            cacheFromExternalProviders = Boolean.parseBoolean(value);
        }

        try {
            cacheManager = FileCacheManager.getInstance();
            cacheManager.getContentCache();
            cacheManager.getLastModifiedCache();
        } catch (JahiaRuntimeException e) {
            throw new ServletException(e.getCause());
        }

        if (SettingsBean.getInstance().isFileServletStatisticsEnabled()) {
            try {
                loggingService = (MetricsLoggingService) SpringContextSingleton.getBean("loggingService");

                sessionFactory = JCRSessionFactory.getInstance();
            } catch (Exception e) {
                logger.error("Unable to get the logging service instance. Metrics logging will be disabled.");
            }
        }
    }

    protected boolean isNotModified(FileKey fileKey, FileLastModifiedCacheEntry lastModifiedEntry,
            HttpServletRequest request, HttpServletResponse response) {
        if (lastModifiedEntry != null) {
            // check presence of the 'If-None-Match' header
            String eTag = request.getHeader("If-None-Match");
            if (eTag != null) {
                return eTag.equals(lastModifiedEntry.getETag());
            }
            // check presence of the 'If-Modified-Since' header
            long modifiedSince = request.getDateHeader("If-Modified-Since");
            if (modifiedSince > -1 && lastModifiedEntry.getLastModified() > 0
                    && lastModifiedEntry.getLastModified() / 1000 * 1000 <= modifiedSince) {
                return true;
            }
        }

        return false;
    }

    protected void logAccess(FileKey fileKey, HttpServletRequest req, String status) {
        if (loggingService == null || !loggingService.isEnabled()) {
            return;
        }

        HttpSession httpSession = req.getSession(false);
        String sessionID = httpSession != null ? httpSession.getId() : req.getRequestedSessionId();
        loggingService.logContentEvent(sessionFactory.getCurrentUser().getName(), req.getRemoteAddr(), sessionID,
                "", fileKey.getPath(), "", "fileAccessed", status);
    }

    protected FileKey parseKey(HttpServletRequest req) throws UnsupportedEncodingException {
        String workspace = null;
        String path = null;
        String p = req.getPathInfo();
        if (p != null && p.length() > 2) {
            int pathStart = p.indexOf("/", 1);
            workspace = pathStart > 1 ? p.substring(1, pathStart) : null;
            if (workspace != null) {
                path = p.substring(pathStart);
                if (ContextPlaceholdersReplacer.WORKSPACE_PLACEHOLDER
                        .equals(URLDecoder.decode(workspace, characterEncoding))) {
                    // Hack for CK Editor links
                    workspace = Constants.EDIT_WORKSPACE;
                }
                if (!JCRContentUtils.isValidWorkspace(workspace)) {
                    // unknown workspace
                    workspace = null;
                }
            }
        }

        return path != null && workspace != null
                ? new FileKey(workspace, JCRContentUtils.escapeNodePath(path), req.getParameter("v"),
                        req.getParameter("l"), StringUtils.defaultIfEmpty(req.getParameter("t"), StringUtils.EMPTY))
                : null;
    }

    private boolean isVisibleForGuest(final JCRNodeWrapper n) throws RepositoryException {
        if (!n.getSession().getUserID().equals(JahiaUserManagerService.GUEST_USERNAME)) {
            if (cacheForLoggedUsers) {
                try {
                    getInstance().doExecute(JahiaUserManagerService.GUEST_USERNAME, null,
                            n.getSession().getWorkspace().getName(), n.getSession().getLocale(),
                            new JCRCallback<Node>() {
                                public Node doInJCR(JCRSessionWrapper session) throws RepositoryException {
                                    return session.getProviderSession(n.getProvider())
                                            .getNodeByIdentifier(n.getIdentifier());
                                }
                            });
                } catch (ItemNotFoundException e) {
                    // not accessible by guest
                    return false;
                }
            } else {
                return false;
            }
        }
        return true;
    }

    public class FileDownloadEvent extends ApplicationEvent {
        private static final long serialVersionUID = -7003604984285879294L;

        HttpServletRequest request;
        String nodeId;
        String nodePath;
        List<String> nodeTypes;
        String workspace;

        protected FileDownloadEvent(Object source, HttpServletRequest request, String nodeId, String nodePath,
                List<String> nodeTypes, String workspace) {
            super(source);
            this.request = request;
            this.nodeId = nodeId;
            this.nodePath = nodePath;
            this.nodeTypes = nodeTypes;
            this.workspace = workspace;
        }

        public HttpServletRequest getRequest() {
            return request;
        }

        public String getNodeId() {
            return nodeId;
        }

        public String getNodePath() {
            return nodePath;
        }

        public List<String> getNodeTypes() {
            return nodeTypes;
        }

        public String getWorkspace() {
            return workspace;
        }
    }

    /**
     * Interface to set on javax.jcr.Binary implementation to set range support
     * If ranges are not supported, the entire file will be download each time
     */
    public interface BinaryRangesSupport {

        boolean supportRanges();

    }
}