com.sun.faces.application.resource.ResourceManager Maven / Gradle / Ivy
Show all versions of jakarta.faces Show documentation
/*
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package com.sun.faces.application.resource;
import static com.sun.faces.util.Util.ensureLeadingSlash;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Stream;
import com.sun.faces.config.WebConfiguration;
import com.sun.faces.util.FacesLogger;
import com.sun.faces.util.Util;
import jakarta.faces.application.ProjectStage;
import jakarta.faces.application.ResourceHandler;
import jakarta.faces.application.ResourceVisitOption;
import jakarta.faces.component.UIViewRoot;
import jakarta.faces.context.FacesContext;
/**
* This class is used to lookup {@link ResourceInfo} instances and cache any that are successfully looked up to reduce
* the computational overhead with the scanning/version checking.
*
* @since 2.0
*/
public class ResourceManager {
private static final Logger LOGGER = FacesLogger.RESOURCE.getLogger();
/**
* {@link Pattern} for valid mime types to configure compression.
*/
private static final Pattern CONFIG_MIMETYPE_PATTERN = Pattern.compile("[a-z-]*/[a-z0-9.\\*-]*");
/**
* {@link ResourceHelper} used for looking up webapp-based resources.
*/
private final WebappResourceHelper webappResourceHelper = new WebappResourceHelper();
/**
* {@link ResourceHelper} used for looking up webapp-based facelets resources.
*/
private final FaceletWebappResourceHelper faceletWebappResourceHelper = new FaceletWebappResourceHelper(webappResourceHelper);
/**
* {@link ResourceHelper} used for looking up classpath-based resources.
*/
private final ClasspathResourceHelper classpathResourceHelper = new ClasspathResourceHelper();
/**
* Cache for storing {@link ResourceInfo} instances to reduce the cost of the resource lookups.
*/
private final ResourceCache cache;
/**
* Patterns used to find {@link ResourceInfo} instances that may have their content compressed.
*/
private List compressableTypes;
/**
* This lock is used to ensure the lookup of compressable {@link ResourceInfo} instances are atomic to prevent theading
* issues when writing the compressed content during a lookup.
*/
private final ReentrantLock lock = new ReentrantLock();
// ------------------------------------------------------------ Constructors
/*
* This ctor is only ever called by test code.
*/
public ResourceManager(ResourceCache cache) {
this.cache = cache;
Map throwAwayMap = new HashMap<>();
initCompressableTypes(throwAwayMap);
}
/**
* Constructs a new ResourceManager
. Note: if the current {@link ProjectStage} is
* {@link ProjectStage#Development} caching or {@link ResourceInfo} instances will not occur.
* @param appMap the application map
* @param cache the resource cache
*/
public ResourceManager(Map appMap, ResourceCache cache) {
this.cache = cache;
initCompressableTypes(appMap);
}
// ------------------------------------------------------ Public Methods
/**
*
* Attempt to lookup a {@link ResourceInfo} based on the specified libraryName
and resourceName
*
*
*
* Implementation Note: Synchronization is necessary when looking up compressed resources. This ensures the atomicity of
* the content being compressed. As such, the cost of doing this is low as once the resource is in the cache, the lookup
* won't be performed again until the cache is cleared. That said, it's not a good idea to have caching disabled in a
* production environment if leveraging compression.
*
* If the resource isn't compressable, then we don't worry about creating a few extra copies of ResourceInfo until the
* cache is populated.
*
*
* @param libraryName the name of the library (if any)
* @param resourceName the name of the resource
* @param contentType the content type of the resource. This will be used to determine if the resource is compressable
* @param ctx the {@link jakarta.faces.context.FacesContext} for the current request
*
* @return a {@link ResourceInfo} if a resource if found matching the provided arguments, otherwise, return
* null
*/
public ResourceInfo findResource(String libraryName, String resourceName, String contentType, FacesContext ctx) {
return findResource(libraryName, resourceName, contentType, false, ctx);
}
public ResourceInfo findViewResource(String resourceName, String contentType, FacesContext facesContext) {
String localePrefix = getLocalePrefix(facesContext);
List contracts = getResourceLibraryContracts(facesContext);
ResourceInfo info = getFromCache(resourceName, null, localePrefix, contracts);
if (info == null) {
if (isCompressable(contentType, facesContext)) {
info = findResourceCompressed(null, resourceName, true, localePrefix, contracts, facesContext);
} else {
info = findResourceNonCompressed(null, resourceName, true, localePrefix, contracts, facesContext);
}
}
return info;
}
public ResourceInfo findResource(String libraryName, String resourceName, String contentType, boolean isViewResource, FacesContext ctx) {
String localePrefix = getLocalePrefix(ctx);
List contracts = getResourceLibraryContracts(ctx);
ResourceInfo info = getFromCache(resourceName, libraryName, localePrefix, contracts);
if (info == null) {
if (isCompressable(contentType, ctx)) {
info = findResourceCompressed(libraryName, resourceName, isViewResource, localePrefix, contracts, ctx);
} else {
info = findResourceNonCompressed(libraryName, resourceName, isViewResource, localePrefix, contracts, ctx);
}
}
return info;
}
public Stream getViewResources(FacesContext facesContext, String path, int maxDepth, ResourceVisitOption... options) {
return faceletWebappResourceHelper.getViewResources(facesContext, path, maxDepth, options);
}
public String getBaseContractsPath() {
return faceletWebappResourceHelper.getBaseContractsPath();
}
public boolean isContractsResource(String path) {
return ensureLeadingSlash(path).startsWith(getBaseContractsPath());
}
// ----------------------------------------------------- Private Methods
private ResourceInfo findResourceCompressed(String libraryName, String resourceName, boolean isViewResource, String localePrefix, List contracts,
FacesContext ctx) {
ResourceInfo info = null;
lock.lock();
try {
info = getFromCache(resourceName, libraryName, localePrefix, contracts);
if (info == null) {
info = doLookup(libraryName, resourceName, localePrefix, true, isViewResource, contracts, ctx);
if (info != null) {
addToCache(info, contracts);
}
}
} finally {
lock.unlock();
}
return info;
}
private ResourceInfo findResourceNonCompressed(String libraryName, String resourceName, boolean isViewResource, String localePrefix, List contracts,
FacesContext ctx) {
ResourceInfo info = doLookup(libraryName, resourceName, localePrefix, false, isViewResource, contracts, ctx);
if (info == null && contracts != null) {
info = doLookup(libraryNameFromContracts(libraryName, contracts), resourceName, localePrefix, false, isViewResource, contracts, ctx);
}
if (info != null && !info.isDoNotCache()) {
addToCache(info, contracts);
}
return info;
}
private String libraryNameFromContracts(String libraryName, List contracts) {
// If the library name is equal to one of the contracts,
// assume the resource to be found is within that contract
for (String contract : contracts) {
if (contract.equals(libraryName)) {
return null;
}
}
return libraryName;
}
/**
* Attempt to look up the Resource based on the provided details.
*
* @param libraryName the name of the library (if any)
* @param resourceName the name of the resource
* @param localePrefix the locale prefix for this resource (if any)
* @param compressable if this resource can be compressed
* @param isViewResource
* @param contracts the contracts to consider
* @param ctx the {@link jakarta.faces.context.FacesContext} for the current request
*
* @return a {@link ResourceInfo} if a resource if found matching the provided arguments, otherwise, return
* null
*/
private ResourceInfo doLookup(String libraryName, String resourceName, String localePrefix, boolean compressable, boolean isViewResource,
List contracts, FacesContext ctx) {
// Loop over the contracts as described in deriveResourceIdConsideringLocalePrefixAndContracts in the spec
for (String contract : contracts) {
ResourceInfo info = getResourceInfo(libraryName, resourceName, localePrefix, contract, compressable, isViewResource, ctx, null);
if (info != null) {
return info;
}
}
return getResourceInfo(libraryName, resourceName, localePrefix, null, compressable, isViewResource, ctx, null);
}
private ResourceInfo getResourceInfo(String libraryName, String resourceName, String localePrefix, String contract, boolean compressable,
boolean isViewResource, FacesContext ctx, LibraryInfo library) {
if (libraryName != null && !nameContainsForbiddenSequence(libraryName)) {
library = findLibrary(libraryName, localePrefix, contract, ctx);
if (library == null && localePrefix != null) {
// no localized library found. Try to find a library that isn't localized.
library = findLibrary(libraryName, null, contract, ctx);
}
if (library == null) {
// If we don't have one by now, perhaps it's time to consider scanning directories.
library = findLibraryOnClasspathWithZipDirectoryEntryScan(libraryName, localePrefix, contract, ctx, false);
if (library == null && localePrefix != null) {
// no localized library found. Try to find
// a library that isn't localized.
library = findLibraryOnClasspathWithZipDirectoryEntryScan(libraryName, null, contract, ctx, false);
}
if (library == null) {
return null;
}
}
} else if (nameContainsForbiddenSequence(libraryName)) {
return null;
}
String resName = trimLeadingSlash(resourceName);
if (nameContainsForbiddenSequence(resName) || !isViewResource && resName.startsWith("WEB-INF")) {
return null;
}
ResourceInfo info = findResource(library, resourceName, localePrefix, compressable, isViewResource, ctx);
if (info == null && localePrefix != null) {
// no localized resource found, try to find a
// resource that isn't localized
info = findResource(library, resourceName, null, compressable, isViewResource, ctx);
}
// If no resource has been found so far, and we have a library that
// was found in the webapp filesystem, see if there is a matching
// library on the classpath. If one is found, try to find a matching
// resource in that library.
if (info == null && library != null && library.getHelper() instanceof WebappResourceHelper) {
LibraryInfo altLibrary = classpathResourceHelper.findLibrary(libraryName, localePrefix, contract, ctx);
if (altLibrary != null) {
VersionInfo originalVersion = library.getVersion();
VersionInfo altVersion = altLibrary.getVersion();
if (originalVersion == null && altVersion == null) {
library = altLibrary;
} else if (originalVersion == null && altVersion != null) {
library = null;
} else if (originalVersion != null && altVersion == null) {
library = null;
} else if (originalVersion.compareTo(altVersion) == 0) {
library = altLibrary;
}
}
if (library != null) {
info = findResource(library, resourceName, localePrefix, compressable, isViewResource, ctx);
if (info == null && localePrefix != null) {
// no localized resource found, try to find a
// resource that isn't localized
info = findResource(library, resourceName, null, compressable, isViewResource, ctx);
}
}
}
return info;
}
/**
* @param s input String
* @return the String without a leading slash if it has one.
*/
private String trimLeadingSlash(String s) {
if (s.charAt(0) == '/') {
return s.substring(1);
} else {
return s;
}
}
static boolean nameContainsForbiddenSequence(String name) {
boolean result = false;
if (name != null) {
name = name.toLowerCase();
result = name.startsWith(".") || name.contains("../") || name.contains("..\\") || name.startsWith("/") || name.startsWith("\\")
|| name.endsWith("/") ||
name.contains("..%2f") || name.contains("..%5c") || name.startsWith("%2f") || name.startsWith("%5c") || name.endsWith("%2f") ||
name.contains("..\\u002f") || name.contains("..\\u005c") || name.startsWith("\\u002f") || name.startsWith("\\u005c")
|| name.endsWith("\\u002f")
;
}
return result;
}
/**
*
* @param name the resource name
* @param library the library name
* @param localePrefix the Locale prefix
* @param contracts
* @return the {@link ResourceInfo} from the cache or null
if no cached entry is found
*/
private ResourceInfo getFromCache(String name, String library, String localePrefix, List contracts) {
if (cache == null) {
return null;
}
return cache.get(name, library, localePrefix, contracts);
}
/**
* Adds the the specified {@link ResourceInfo} to the cache.
*
* @param info the @{link ResourceInfo} to add.
* @param contracts the contracts
*/
private void addToCache(ResourceInfo info, List contracts) {
if (cache == null) {
return;
}
cache.add(info, contracts);
}
/**
*
* Attempt to lookup and return a {@link LibraryInfo} based on the specified arguments
.
*
*
* The lookup process will first search the file system of the web application *within the resources directory*. If the
* library is not found, then it processed to searching the classpath, if not found there, search from the webapp root
* *excluding* the resources directory.
*
*
*
* If a library is found, this method will return a {@link LibraryInfo} instance that contains the name, version, and
* {@link ResourceHelper}.
*
*
*
* @param libraryName the library to find
* @param localePrefix the prefix for the desired locale
* @param contract the contract to use
* @param ctx the {@link jakarta.faces.context.FacesContext} for the current request
* @return the Library instance for the specified library
*/
LibraryInfo findLibrary(String libraryName, String localePrefix, String contract, FacesContext ctx) {
LibraryInfo library = webappResourceHelper.findLibrary(libraryName, localePrefix, contract, ctx);
if (library == null) {
library = classpathResourceHelper.findLibrary(libraryName, localePrefix, contract, ctx);
}
if (library == null && contract == null) {
// FCAPUTO facelets in contracts should have been found by the webapphelper already
library = faceletWebappResourceHelper.findLibrary(libraryName, localePrefix, contract, ctx);
}
// if not library is found at this point, let the caller deal with it
return library;
}
LibraryInfo findLibraryOnClasspathWithZipDirectoryEntryScan(String libraryName, String localePrefix, String contract, FacesContext ctx, boolean forceScan) {
return classpathResourceHelper.findLibraryWithZipDirectoryEntryScan(libraryName, localePrefix, contract, ctx, forceScan);
}
/**
*
* Attempt to lookup and return a {@link ResourceInfo} based on the specified arguments
.
*
*
* The lookup process will first search the file system of the web application. If the library is not found, then it
* processed to searching the classpath.
*
*
*
* If a library is found, this method will return a {@link LibraryInfo} instance that contains the name, version, and
* {@link ResourceHelper}.
*
*
* @param library the library the resource should be found in
* @param resourceName the name of the resource
* @param localePrefix the prefix for the desired locale
* @param compressable true
if the resource can be compressed
* @param ctx the {@link jakarta.faces.context.FacesContext} for the current request
*
* @return the Library instance for the specified library
*/
private ResourceInfo findResource(LibraryInfo library, String resourceName, String localePrefix, boolean compressable, boolean skipToFaceletResourceHelper,
FacesContext ctx) {
if (library != null) {
return library.getHelper().findResource(library, resourceName, localePrefix, compressable, ctx);
} else {
ResourceInfo resource = null;
if (!skipToFaceletResourceHelper) {
resource = webappResourceHelper.findResource(null, resourceName, localePrefix, compressable, ctx);
}
if (resource == null && !skipToFaceletResourceHelper) {
resource = classpathResourceHelper.findResource(null, resourceName, localePrefix, compressable, ctx);
}
if (resource == null) {
resource = faceletWebappResourceHelper.findResource(library, resourceName, localePrefix, compressable, ctx);
}
return resource;
}
}
ResourceInfo findResource(String resourceId) {
// PENDING(fcaputo) do we need to handle contracts here?
String libraryName = null;
String resourceName = null;
int end = 0, start = 0;
if (-1 != (end = resourceId.lastIndexOf("/"))) {
resourceName = resourceId.substring(end + 1);
if (-1 != (start = resourceId.lastIndexOf("/", end - 1))) {
libraryName = resourceId.substring(start + 1, end);
} else {
libraryName = resourceId.substring(0, end);
}
}
FacesContext context = FacesContext.getCurrentInstance();
LibraryInfo info = findLibrary(libraryName, null, null, context);
ResourceInfo resourceInfo = this.findResource(info, resourceName, libraryName, true, false, context);
return resourceInfo;
}
/**
*
* Obtains the application configured message resources for the current request locale. If a ResourceBundle is found and
* contains the key jakarta.faces.resource.localePrefix
, use the value associated with that key as the
* prefix for locale specific resources.
*
*
*
* For example, say the request locale is en_US, and jakarta.faces.resourceLocalePrefix
is found with a
* value of en
, a resource path within a web application might look like
* /resources/en/corp/images/greetings.jpg
*
*
* @param context the {@link FacesContext} for the current request
* @return the localePrefix based on the current request, or null
if no prefix can be determined
*/
private String getLocalePrefix(FacesContext context) {
String localePrefix = null;
localePrefix = context.getExternalContext().getRequestParameterMap().get("loc");
if (localePrefix != null && !nameContainsForbiddenSequence(localePrefix)) {
return localePrefix;
} else {
localePrefix = null;
}
String appBundleName = context.getApplication().getMessageBundle();
if (null != appBundleName) {
Locale locale = null;
if (context.getViewRoot() != null) {
locale = context.getViewRoot().getLocale();
} else {
locale = context.getApplication().getViewHandler().calculateLocale(context);
}
try {
ResourceBundle appBundle = ResourceBundle.getBundle(appBundleName, locale, Util.getCurrentLoader(ResourceManager.class));
localePrefix = appBundle.getString(ResourceHandler.LOCALE_PREFIX);
} catch (MissingResourceException mre) {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "Ignoring missing resource", mre);
}
}
}
return localePrefix;
}
private List getResourceLibraryContracts(FacesContext context) {
UIViewRoot viewRoot = context.getViewRoot();
if (viewRoot == null) {
if (context.getApplication().getResourceHandler().isResourceRequest(context)) {
// it is a resource request. look at the parameter con=.
String param = context.getExternalContext().getRequestParameterMap().get("con");
if (!nameContainsForbiddenSequence(param) && param != null && param.trim().length() > 0) {
return Arrays.asList(param);
}
}
// PENDING(edburns): calculate the contracts!
return Collections.emptyList();
}
return context.getResourceLibraryContracts();
}
/**
* @param contentType content-type in question
* @param ctx the @{link FacesContext} for the current request
* @return true
if this resource can be compressed, otherwise false
*/
private boolean isCompressable(String contentType, FacesContext ctx) {
// No compression when developing.
if (contentType == null || ctx.isProjectStage(ProjectStage.Development)) {
return false;
} else {
if (compressableTypes != null && !compressableTypes.isEmpty()) {
for (Pattern p : compressableTypes) {
boolean matches = p.matcher(contentType).matches();
if (matches) {
return true;
}
}
}
}
return false;
}
/**
* Init compressableTypes
from the configuration.
*/
private void initCompressableTypes(Map appMap) {
WebConfiguration config = WebConfiguration.getInstance();
String value = config.getOptionValue(WebConfiguration.WebContextInitParameter.CompressableMimeTypes);
if (value != null && value.length() > 0) {
String[] values = Util.split(appMap, value, ",");
if (values != null) {
for (String s : values) {
String pattern = s.trim();
if (!isPatternValid(pattern)) {
continue;
}
if (pattern.endsWith("/*")) {
pattern = pattern.substring(0, pattern.indexOf("/*"));
pattern += "/[a-z0-9.-]*";
}
if (compressableTypes == null) {
compressableTypes = new ArrayList<>(values.length);
}
try {
compressableTypes.add(Pattern.compile(pattern));
} catch (PatternSyntaxException pse) {
if (LOGGER.isLoggable(Level.WARNING)) {
// PENDING i18n
LOGGER.log(Level.WARNING, "faces.resource.mime.type.configration.invalid", new Object[] { pattern, pse.getPattern() });
}
}
}
}
}
}
/**
* @param input input mime-type pattern from the configuration
* @return true
if the input matches the expected pattern, otherwise false
*/
private boolean isPatternValid(String input) {
return CONFIG_MIMETYPE_PATTERN.matcher(input).matches();
}
}