/*
* 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);
}
}
}
}