org.xwiki.resource.servlet.AbstractServletResourceReferenceHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.resource.servlet.AbstractServletResourceReferenceHandler.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.resource.servlet;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;

import javax.inject.Inject;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.tika.Tika;
import org.slf4j.Logger;
import org.xwiki.container.Container;
import org.xwiki.container.Request;
import org.xwiki.container.Response;
import org.xwiki.container.servlet.ServletRequest;
import org.xwiki.container.servlet.ServletResponse;
import org.xwiki.resource.AbstractResourceReferenceHandler;
import org.xwiki.resource.ResourceReference;
import org.xwiki.resource.ResourceReferenceHandler;
import org.xwiki.resource.ResourceReferenceHandlerChain;
import org.xwiki.resource.ResourceReferenceHandlerException;
import org.xwiki.resource.ResourceType;
import org.xwiki.stability.Unstable;

/**
 * Base class for {@link ResourceReferenceHandler}s that can handle servlet resource requests.
 * 
 * @param <R> the resource type
 * @version $Id: b838d4755648c3c4642774dbd5798b03c5fe7e43 $
 * @since 7.4.6
 * @since 8.2.2
 * @since 8.3
 */
@Unstable
public abstract class AbstractServletResourceReferenceHandler<R extends ResourceReference>
        extends AbstractResourceReferenceHandler<ResourceType> {
    /**
     * One year duration can be considered as permanent caching.
     */
    private static final long CACHE_DURATION = 365 * 24 * 3600 * 1000L;

    @Inject
    private Logger logger;

    @Inject
    private Container container;

    /**
     * Used to determine the Content Type of the requested resource files.
     */
    private Tika tika = new Tika();

    @Override
    public void handle(ResourceReference resourceReference, ResourceReferenceHandlerChain chain)
            throws ResourceReferenceHandlerException {
        @SuppressWarnings("unchecked")
        R typedResourceReference = (R) resourceReference;

        if (!isResourceAccessible(typedResourceReference)) {
            sendError(HttpStatus.SC_FORBIDDEN, "You are not allowed to view [%s].",
                    getResourceName(typedResourceReference));
        } else if (!shouldBrowserUseCachedContent(typedResourceReference)) {
            // If we get here then either the resource is not cached by the browser or the resource is dynamic.
            InputStream resourceStream = getResourceStream(typedResourceReference);
            if (resourceStream != null) {
                try {
                    serveResource(typedResourceReference, filterResource(typedResourceReference, resourceStream));
                } catch (ResourceReferenceHandlerException e) {
                    this.logger.error(e.getMessage(), e);
                    sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage());
                }
            } else {
                sendError(HttpStatus.SC_NOT_FOUND, "Resource not found [%s].",
                        getResourceName(typedResourceReference));
            }
        }

        // Be a good citizen, continue the chain, in case some lower-priority handler has something to do for this
        // resource reference.
        chain.handleNext(resourceReference);
    }

    /**
     * @param resourceReference the reference of the requested resource
     * @return {@code true} if the specified resource is accessible, {@code false} otherwise
     */
    protected boolean isResourceAccessible(R resourceReference) {
        return true;
    }

    /**
     * @param resourceReference the reference of the requested resource
     * @return {@code true} if the requested resource is static and is cached by the browser, {@code false} if the
     *         browser should discard the cached version and use the new version from this response
     */
    private boolean shouldBrowserUseCachedContent(R resourceReference) {
        // If the request contains an "If-Modified-Since" header and the requested resource has not been modified then
        // return a 304 Not Modified to tell the browser to use its cached version.
        Request request = this.container.getRequest();
        if (request instanceof ServletRequest
                && ((ServletRequest) request).getHttpServletRequest().getHeader("If-Modified-Since") != null
                && isResourceCacheable(resourceReference)) {
            // The user probably used F5 to reload the page and the browser checks if there are changes.
            Response response = this.container.getResponse();
            if (response instanceof ServletResponse) {
                // Return the 304 Not Modified.
                ((ServletResponse) response).getHttpServletResponse()
                        .setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                return true;
            }
        }
        return false;
    }

    /**
     * @param resourceReference a resource reference
     * @return {@code true} if the specified resource can be cached, {@code false} otherwise
     */
    protected boolean isResourceCacheable(R resourceReference) {
        return true;
    }

    /**
     * @param resourceReference the reference of the requested resource
     * @return the stream that can be used to read the resource
     */
    protected abstract InputStream getResourceStream(R resourceReference);

    /**
     * @param resourceReference the reference of the requested resource
     * @return the name of the specified resource
     */
    protected abstract String getResourceName(R resourceReference);

    /**
     * Sends back the specified resource.
     *
     * @param resourceReference the reference of the requested resource
     * @param rawResourceStream the resource stream used to read the resource
     * @throws ResourceReferenceHandlerException if it fails to read the resource
     */
    private void serveResource(R resourceReference, InputStream rawResourceStream)
            throws ResourceReferenceHandlerException {
        InputStream resourceStream = rawResourceStream;
        String resourceName = getResourceName(resourceReference);

        // Make sure the resource stream supports mark & reset which is needed in order be able to detect the
        // content type without affecting the stream (Tika may need to read a few bytes from the start of the
        // stream, in which case it will mark & reset the stream).
        if (!resourceStream.markSupported()) {
            resourceStream = new BufferedInputStream(resourceStream);
        }

        try {
            Response response = this.container.getResponse();
            setResponseHeaders(response, resourceReference);
            response.setContentType(this.tika.detect(resourceStream, resourceName));
            IOUtils.copy(resourceStream, response.getOutputStream());
        } catch (Exception e) {
            throw new ResourceReferenceHandlerException(String.format("Failed to read resource [%s]", resourceName),
                    e);
        } finally {
            IOUtils.closeQuietly(resourceStream);
        }
    }

    /**
     * Filter the resource before sending it to the client.
     * 
     * @param resourceReference the resource to filter
     * @param resourceStream the resource content
     * @return the filtered resource content
     */
    protected InputStream filterResource(R resourceReference, InputStream resourceStream)
            throws ResourceReferenceHandlerException {
        return resourceStream;
    }

    /**
     * Sets the response headers needed to cache the resource permanently, if the resource can be cached.
     * 
     * @param response the response
     * @param resourceReference the resource that is being served
     */
    private void setResponseHeaders(Response response, R resourceReference) {
        // Cache the resource if possible.
        if (response instanceof ServletResponse && isResourceCacheable(resourceReference)) {
            HttpServletResponse httpResponse = ((ServletResponse) response).getHttpServletResponse();
            httpResponse.setHeader(HttpHeaders.CACHE_CONTROL, "public");
            httpResponse.setDateHeader(HttpHeaders.EXPIRES, new Date().getTime() + CACHE_DURATION);
            // Even if the resource is cached permanently, most browsers are still sending a request if the user reloads
            // the page using F5. We send back the "Last-Modified" header in the response so that the browser will send
            // us an "If-Modified-Since" request for any subsequent call for this static resource. When this happens we
            // return a 304 to tell the browser to use its cached version.
            httpResponse.setDateHeader(HttpHeaders.LAST_MODIFIED, new Date().getTime());
        }
    }

    /**
     * Sends back the specified status code with the given message in order for the browser to know the resource
     * couldn't be served. This is especially important as we don't want to cache an empty response.
     * 
     * @param statusCode the response status code to send
     * @param message the error message
     * @param parameters the message parameters
     * @throws ResourceReferenceHandlerException if setting the response status code fails
     */
    private void sendError(int statusCode, String message, Object... parameters)
            throws ResourceReferenceHandlerException {
        Response response = this.container.getResponse();
        if (response instanceof ServletResponse) {
            HttpServletResponse httpResponse = ((ServletResponse) response).getHttpServletResponse();
            try {
                httpResponse.sendError(statusCode, String.format(message, parameters));
            } catch (IOException e) {
                throw new ResourceReferenceHandlerException(
                        String.format("Failed to return status code [%s].", statusCode), e);
            }
        }
    }
}