/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.faces.application.resource;
import com.sun.faces.application.ApplicationAssociate;
import com.sun.faces.config.WebConfiguration;
import static com.sun.faces.config.WebConfiguration.WebContextInitParameter.DefaultResourceMaxAge;
import static com.sun.faces.config.WebConfiguration.WebContextInitParameter.ResourceBufferSize;
import static com.sun.faces.config.WebConfiguration.WebContextInitParameter.ResourceExcludes;
import com.sun.faces.util.FacesLogger;
import com.sun.faces.util.RequestStateManager;
import static com.sun.faces.util.RequestStateManager.RESOURCE_REQUEST;
import com.sun.faces.util.Util;
import static com.sun.faces.util.Util.getFacesMapping;
import static com.sun.faces.util.Util.isPrefixMapped;
import static com.sun.faces.util.Util.notNegative;
import static com.sun.faces.util.Util.notNull;
import java.io.IOException;
import java.io.InputStream;
import static java.lang.Boolean.FALSE;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static javax.faces.application.ProjectStage.Development;
import static javax.faces.application.ProjectStage.Production;
import javax.faces.application.Resource;
import javax.faces.application.ResourceHandler;
import javax.faces.application.ResourceVisitOption;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
/**
* This is the default implementation of {@link ResourceHandler}.
*/
public class ResourceHandlerImpl extends ResourceHandler {
// Log instance for this class
private static final Logger LOGGER = FacesLogger.APPLICATION.getLogger();
ResourceManager manager;
List<Pattern> excludePatterns;
private long creationTime;
private long maxAge;
private WebConfiguration webconfig;
// ------------------------------------------------------------ Constructors
/**
* Creates a new instance of ResourceHandlerImpl
*/
public ResourceHandlerImpl() {
creationTime = System.currentTimeMillis();
webconfig = WebConfiguration.getInstance();
ExternalContext extContext = FacesContext.getCurrentInstance().getExternalContext();
manager = ApplicationAssociate.getInstance(extContext).getResourceManager();
initExclusions(extContext.getApplicationMap());
initMaxAge();
}
// ------------------------------------------- Methods from Resource Handler
/**
* @see ResourceHandler#createResource(String)
*/
@Override
public Resource createResource(String resourceName) {
return createResource(resourceName, null, null);
}
/**
* @see ResourceHandler#createResource(String, String)
*/
@Override
public Resource createResource(String resourceName, String libraryName) {
return createResource(resourceName, libraryName, null);
}
/**
* @see ResourceHandler#createResource(String, String, String)
*/
@Override
public Resource createResource(String resourceName, String libraryName, String contentType) {
notNull("resourceName", resourceName);
FacesContext ctx = FacesContext.getCurrentInstance();
String ctype = contentType != null ? contentType : getContentType(ctx, resourceName);
ResourceInfo info = manager.findResource(libraryName, resourceName, ctype, ctx);
if (info == null) {
return null;
}
return new ResourceImpl(info, ctype, creationTime, maxAge);
}
@Override
public Resource createViewResource(FacesContext facesContext, String resourceName) {
notNull("resourceName", resourceName);
String contentType = getContentType(facesContext, resourceName);
ResourceInfo resourceInfo = manager.findViewResource(resourceName, contentType, facesContext);
if (resourceInfo == null) {
return null;
}
return new ResourceImpl(resourceInfo, contentType, creationTime, maxAge);
}
/**
* @see ResourceHandler#getViewResources(FacesContext, String, ResourceVisitOption...)
*/
@Override
public Stream<String> getViewResources(FacesContext facesContext, String path, ResourceVisitOption... options) {
notNull("path", path);
return manager.getViewResources(facesContext, path, Integer.MAX_VALUE, options);
}
/**
* @see ResourceHandler#getViewResources(FacesContext, String, int, ResourceVisitOption...)
*/
@Override
public Stream<String> getViewResources(FacesContext facesContext, String path, int maxDepth, ResourceVisitOption... options) {
notNull("path", path);
notNegative("maxDepth", maxDepth);
return manager.getViewResources(facesContext, path, maxDepth, options);
}
/**
* @see ResourceHandler#createResourceFromId(String)
*/
@Override
public Resource createResourceFromId(String resourceId) {
notNull("resourceId", resourceId);
FacesContext ctx = FacesContext.getCurrentInstance();
boolean development = ctx.isProjectStage(Development);
ResourceInfo info = manager.findResource(resourceId);
String ctype = getContentType(ctx, resourceId);
if (info == null) {
logMissingResource(ctx, resourceId, null);
return null;
} else {
return new ResourceImpl(info, ctype, creationTime, maxAge);
}
}
@Override
public boolean libraryExists(String libraryName) {
if (libraryName.contains("../")) {
return false;
}
FacesContext context = FacesContext.getCurrentInstance();
// PENDING(fcaputo) do we need to iterate over the contracts here? I don't think so.
LibraryInfo info = manager.findLibrary(libraryName, null, null, context);
if (info == null) {
info = manager.findLibraryOnClasspathWithZipDirectoryEntryScan(libraryName, null, null, context, true);
}
return info != null;
}
/**
* @see ResourceHandler#isResourceRequest(javax.faces.context.FacesContext)
*/
@Override
public boolean isResourceRequest(FacesContext context) {
Boolean isResourceRequest = (Boolean) RequestStateManager.get(context, RESOURCE_REQUEST);
if (isResourceRequest == null) {
String resourceId = normalizeResourceRequest(context);
isResourceRequest = (resourceId != null
? resourceId.startsWith(RESOURCE_IDENTIFIER)
: FALSE);
RequestStateManager.set(context, RESOURCE_REQUEST, isResourceRequest);
}
return isResourceRequest;
}
@Override
public String getRendererTypeForResourceName(String resourceName) {
String rendererType = null;
String contentType = getContentType(FacesContext.getCurrentInstance(),
resourceName);
if (null != contentType) {
contentType = contentType.toLowerCase();
if (-1 != contentType.indexOf("javascript")) {
rendererType = "javax.faces.resource.Script";
}
else if (-1 != contentType.indexOf("css")) {
rendererType = "javax.faces.resource.Stylesheet";
}
}
return rendererType;
}
/**
* @see javax.faces.application.ResourceHandler#handleResourceRequest(javax.faces.context.FacesContext)
*/
@Override
public void handleResourceRequest(FacesContext context) throws IOException {
String resourceId = normalizeResourceRequest(context);
// handleResourceRequest called for a non-resource request, bail out.
if (resourceId == null) {
return;
}
ExternalContext extContext = context.getExternalContext();
if (isExcluded(resourceId)) {
extContext.setResponseStatus(SC_NOT_FOUND);
return;
}
assert (null != resourceId);
assert (resourceId.startsWith(RESOURCE_IDENTIFIER));
Resource resource = null;
String resourceName = null;
String libraryName = null;
if (RESOURCE_IDENTIFIER.length() < resourceId.length()) {
resourceName = resourceId.substring(RESOURCE_IDENTIFIER.length() + 1);
assert(resourceName != null);
libraryName = context.getExternalContext().getRequestParameterMap().get("ln");
boolean createResource;
if (libraryName != null) {
createResource = libraryNameIsSafe(libraryName);
if (!createResource) {
send404(context, resourceName, libraryName, true);
return;
}
} else {
createResource = true;
}
if (createResource) {
resource = context.getApplication().getResourceHandler().createResource(resourceName, libraryName);
}
}
if (resource != null) {
if (resource.userAgentNeedsUpdate(context)) {
ReadableByteChannel resourceChannel = null;
WritableByteChannel out = null;
ByteBuffer buf = allocateByteBuffer();
try {
InputStream in = resource.getInputStream();
if (in == null) {
send404(context, resourceName, libraryName, true);
return;
}
resourceChannel = Channels.newChannel(in);
out = Channels.newChannel(extContext.getResponseOutputStream());
extContext.setResponseBufferSize(buf.capacity());
String contentType = resource.getContentType();
if (contentType != null) {
extContext.setResponseContentType(resource.getContentType());
}
handleHeaders(context, resource);
int size = 0;
for (int thisRead = resourceChannel.read(buf), totalWritten = 0;
thisRead != -1;
thisRead = resourceChannel.read(buf)) {
buf.rewind();
buf.limit(thisRead);
size += thisRead;
do {
totalWritten += out.write(buf);
} while (totalWritten < size);
buf.clear();
}
if (!extContext.isResponseCommitted()) {
extContext.setResponseContentLength(size);
}
} catch (IOException ioe) {
send404(context, resourceName, libraryName, ioe, true);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {
// Maybe log a warning here?
}
}
if (resourceChannel != null) {
resourceChannel.close();
}
}
} else {
send304(context);
}
} else {
// already logged elsewhere
send404(context, resourceName, libraryName, true);
}
}
private boolean libraryNameIsSafe(String libraryName) {
assert(null != libraryName);
boolean result;
result = !(libraryName.startsWith(".") ||
libraryName.startsWith("/") ||
libraryName.contains("/") ||
libraryName.startsWith("\\") ||
libraryName.contains("\\") ||
libraryName.startsWith("%2e") ||
libraryName.startsWith("%2f") ||
libraryName.contains("%2f") ||
libraryName.startsWith("%5c") ||
libraryName.contains("%5c") ||
libraryName.startsWith("\\u002e") ||
libraryName.startsWith("\\u002f") ||
libraryName.contains("\\u002f") ||
libraryName.startsWith("\\u005c") ||
libraryName.contains("\\u005c"));
return result;
}
private void send404(FacesContext ctx, String resourceName, String libraryName, boolean logMessage) {
send404(ctx, resourceName, libraryName, null, logMessage);
}
private void send404(FacesContext ctx, String resourceName, String libraryName, Throwable t, boolean logMessage) {
ctx.getExternalContext().setResponseStatus(SC_NOT_FOUND);
if (logMessage) {
logMissingResource(ctx, resourceName, libraryName, t);
}
}
private void send304(FacesContext ctx) {
ctx.getExternalContext().setResponseStatus(SC_NOT_MODIFIED);
}
// ------------------------------------------------- Package Private Methods
/**
* This method is leveraged by {@link ResourceImpl} to detemine if a resource
* has been upated. In short, a resource has been updated if the timestamp
* is newer than the timestamp of the ResourceHandler creation time.
* @return the time when the ResourceHandler was instantiated (in milliseconds)
*/
@SuppressWarnings({"UnusedDeclaration"})
long getCreationTime() {
return creationTime;
}
/**
* This method is here soley for the purpose of unit testing and will
* not be invoked during normal runtime.
* @param creationTime the time in milliseconds
*/
@SuppressWarnings({"UnusedDeclaration"})
void setCreationTime(long creationTime) {
this.creationTime = creationTime;
}
/**
* Utility method leveraged by ResourceImpl to reduce the cost of
* looking up the WebConfiguration per-instance.
* @return the {@link WebConfiguration} for this application
*/
@SuppressWarnings({"UnusedDeclaration"})
WebConfiguration getWebConfig() {
return webconfig;
}
// --------------------------------------------------------- Private Methods
/**
* Log a message indicating a particular resource (reference by name and/or
* library) could not be found. If this was due to an exception, the exception
* provided will be logged as well.
*
* @param ctx the {@link FacesContext} for the current request
* @param resourceName the resource name
* @param libraryName the resource library
* @param t the exception caught when attempting to find the resource
*/
private void logMissingResource(FacesContext ctx, String resourceName, String libraryName, Throwable t) {
Level level;
if (!ctx.isProjectStage(Production)) {
level = WARNING;
} else {
level = t != null ? WARNING : FINE;
}
if (libraryName != null) {
if (LOGGER.isLoggable(level)) {
LOGGER.log(level,
"jsf.application.resource.unable_to_serve_from_library",
new Object[]{resourceName, libraryName});
if (t != null) {
LOGGER.log(level, "", t);
}
}
} else {
if (LOGGER.isLoggable(level)) {
LOGGER.log(level,
"jsf.application.resource.unable_to_serve",
new Object[]{resourceName});
if (t != null) {
LOGGER.log(level, "", t);
}
}
}
}
/**
* Log a message indicating a particular resource (reference by name and/or
* library) could not be found. If this was due to an exception, the exception
* provided will be logged as well.
*
* @param ctx the {@link FacesContext} for the current request
* @param resourceName the resource name
* @param libraryName the resource library
* @param t the exception caught when attempting to find the resource
*/
private void logMissingResource(FacesContext ctx, String resourceId, Throwable t) {
Level level;
if (!ctx.isProjectStage(Production)) {
level = WARNING;
} else {
level = t != null ? WARNING : FINE;
}
if (LOGGER.isLoggable(level)) {
LOGGER.log(level,
"jsf.application.resource.unable_to_serve",
new Object[]{resourceId});
if (t != null) {
LOGGER.log(level, "", t);
}
}
}
/**
* @param resourceName the resource of interest. The resourceName in question
* may consist of zero or more path elements such that resourceName could
* be something like path1/path2/resource.jpg or resource.jpg
* @return the content type for this resource
*/
private String getContentType(FacesContext ctx, String resourceName) {
return ctx.getExternalContext().getMimeType(resourceName);
}
/**
* Normalize the request path to exclude JSF invocation information.
* If the FacesServlet servicing this request was prefix mapped, then
* the path to the FacesServlet will be removed.
* If the FacesServlet servicing this request was extension mapped, then
* the extension will be trimmed off.
*
* @param context the <code>FacesContext</code> for the current request
* @return the request path without JSF invocation information
*/
private String normalizeResourceRequest(FacesContext context) {
String path;
String facesServletMapping = getFacesMapping(context);
// If it is extension mapped
if (!isPrefixMapped(facesServletMapping)) {
path = context.getExternalContext().getRequestServletPath();
// strip off the extension
int i = path.lastIndexOf(".");
if (0 < i) {
path = path.substring(0, i);
}
} else {
path = context.getExternalContext().getRequestPathInfo();
}
return path;
}
/**
* @param resourceId the normalized request path as returned by
* {@link #normalizeResourceRequest(javax.faces.context.FacesContext)}
* @return <code>true</code> if the request matces an excluded resource,
* otherwise <code>false</code>
*/
private boolean isExcluded(String resourceId) {
for (Pattern pattern : excludePatterns) {
if (pattern.matcher(resourceId).matches()) {
return true;
}
}
return false;
}
/**
* Initialize the exclusions for this application.
* If no explicit exclusions are configured, the defaults of
* <ul>
* <li>.class</li>
* <li>.properties</li>
* <li>.xhtml</li>
* <li>.jsp</li>
* <li>.jspx</li>
* <ul>
* will be used.
*/
private void initExclusions(Map<String, Object> appMap) {
String excludesParam = webconfig.getOptionValue(ResourceExcludes);
String[] patterns = Util.split(appMap, excludesParam, " ");
excludePatterns = new ArrayList<>(patterns.length);
for (String pattern : patterns) {
excludePatterns.add(Pattern.compile(".*\\" + pattern));
}
}
private void initMaxAge() {
maxAge = Long.parseLong(webconfig.getOptionValue(DefaultResourceMaxAge));
}
private void handleHeaders(FacesContext ctx, Resource resource) {
ExternalContext extContext = ctx.getExternalContext();
for (Map.Entry<String, String> cur : resource.getResponseHeaders().entrySet()) {
extContext.setResponseHeader(cur.getKey(), cur.getValue());
}
}
private ByteBuffer allocateByteBuffer() {
int size;
try {
size = Integer.parseInt(webconfig.getOptionValue(ResourceBufferSize));
} catch (NumberFormatException nfe) {
if (LOGGER.isLoggable(WARNING)) {
LOGGER.log(WARNING,
"jsf.application.resource.invalid_resource_buffer_size",
new Object[] {
webconfig.getOptionValue(ResourceBufferSize),
ResourceBufferSize.getQualifiedName(),
ResourceBufferSize.getDefaultValue()
});
}
size = Integer.parseInt(ResourceBufferSize.getDefaultValue());
}
return ByteBuffer.allocate(size);
}
}