![JAR search and dependency download from the Maven repository](/logo.png)
org.omnifaces.resourcehandler.CDNResourceHandler Maven / Gradle / Ivy
/*
* Copyright OmniFaces
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.omnifaces.resourcehandler;
import static java.lang.Boolean.parseBoolean;
import static org.omnifaces.util.Faces.evaluateExpressionGet;
import static org.omnifaces.util.Faces.getInitParameter;
import static org.omnifaces.util.Utils.isEmpty;
import java.util.HashMap;
import java.util.Map;
import jakarta.faces.application.Resource;
import jakarta.faces.application.ResourceDependency;
import jakarta.faces.application.ResourceHandler;
/**
*
* This {@link ResourceHandler} implementation allows the developer to provide external (CDN) URLs instead of the
* default local URLs for Faces resources. This also works on auto-included resources provided as
* {@link ResourceDependency} by the Faces implementation and/or component libraries. For example, Faces's own
* jakarta.faces:jsf.js
resource or PrimeFaces' primefaces:jquery/jquery.js
resource could be
* pointed to a CDN.
*
*
Installation
*
* To get it to run, this handler needs be registered as follows in faces-config.xml
:
*
* <application>
* <resource-handler>org.omnifaces.resourcehandler.CDNResourceHandler</resource-handler>
* </application>
*
*
* Standard configuration
*
* To configure the CDN URLs, a {@value org.omnifaces.resourcehandler.CDNResourceHandler#PARAM_NAME_CDN_RESOURCES}
* context parameter has to be provided wherein the CDN resources are been specified as a comma separated string of
* libraryName:resourceName=https://cdn.example.com/url
key=value pairs. The key represents the default
* Faces resource identifier and the value represents the full CDN URL, including the scheme. The CDN URL is not validated
* by this resource handler, so you need to make absolutely sure yourself that it is valid.
*
* Here is an example configuration:
*
* <context-param>
* <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_URLS</param-name>
* <param-value>
* js/script1.js=https://cdn.example.com/js/script1.js,
* somelib:js/script2.js=https://cdn.example.com/somelib/js/script2.js,
* otherlib:style.css=https://cdn.example.com/otherlib/style.css,
* somelib:images/logo.png=https://cdn.example.com/somelib/logo.png
* </param-value>
* </context-param>
*
*
* With the above configuration, the following resources:
*
* <h:outputScript name="js/script1.js" />
* <h:outputScript library="somelib" name="js/script2.js" />
* <h:outputStylesheet library="otherlib" name="style.css" />
* <h:graphicImage library="somelib" name="images/logo.png" />
*
*
* Will be rendered as:
*
* <script type="text/javascript" src="https://cdn.example.com/js/script1.js"></script>
* <script type="text/javascript" src="https://cdn.example.com/somelib/js/script2.js"></script>
* <link type="text/css" rel="stylesheet" href="https://cdn.example.com/otherlib/style.css" />
* <img src="https://cdn.example.com/logo.png" />
*
*
* Here is a real world example with Bootstrap:
*
* <context-param>
* <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_URLS</param-name>
* <param-value>
* cdn:bootstrap.css=https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css,
* cdn:bootstrap.js=https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js
* </param-value>
* </context-param>
*
*
* With the above configuration, the following resources:
*
* <h:outputStylesheet library="cdn" name="bootstrap.css" />
* <h:outputScript library="cdn" name="bootstrap.js" />
*
*
* Will be rendered as:
*
* <link type="text/css" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
* <script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
*
*
* Wildcard configuration
*
* You can also use the wildcard syntax to map every single resource of a specific library to a common CDN URL. To
* achieve that, just use *
as the sole resource name and make sure that the CDN URL ends with
* /*
. Here's an example:
*
* <context-param>
* <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_URLS</param-name>
* <param-value>jquery-cdn:*=https://code.jquery.com/*</param-value>
* </context-param>
*
* With the above configuration, the following resources:
*
* <h:outputScript library="jquery-cdn" name="jquery-1.9.1.js" />
* <h:outputScript library="jquery-cdn" name="ui/1.10.3/jquery-ui.js" />
*
*
* Will be rendered as:
*
* <script type="text/javascript" src="https://code.jquery.com/jquery-1.9.1.js"></script>
* <script type="text/javascript" src="https://code.jquery.com/ui/1.10.3/jquery-ui.js"></script>
*
*
* EL expressions
* The CDN resource handler supports evaluating EL expessions in the CDN URL. Here's an example:
*
* <context-param>
* <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_URLS</param-name>
* <param-value>jquery-cdn:*=https://#{settings.jqueryCDN}/*</param-value>
* </context-param>
*
* The EL expression is resolved on a per-request basis.
*
* Conditionally disable CDN resource handler
*
* If you'd like to supply a context parameter which conditionally disables the CDN resource handler, then set the
* context parameter {@value org.omnifaces.resourcehandler.CDNResourceHandler#PARAM_NAME_CDN_DISABLED} accordingly.
*
* <context-param>
* <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_DISABLED</param-name>
* <param-value>true</param-value>
* </context-param>
* <!-- or -->
* <context-param>
* <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_DISABLED</param-name>
* <param-value>#{facesContext.application.projectStage eq 'Development'}</param-value>
* </context-param>
* <!-- or -->
* <context-param>
* <param-name>org.omnifaces.CDN_RESOURCE_HANDLER_DISABLED</param-name>
* <param-value>#{someBean.someBooleanProperty}</param-value>
* </context-param>
*
* The EL expression is resolved on a per-request basis.
*
* CombinedResourceHandler
*
* If you're also using the {@link CombinedResourceHandler}, then you need to understand that CDN resources can
* simply not be combined, as that would defeat the CDN purpose. The {@link CombinedResourceHandler} will therefore
* automatically exclude all CDN resources from combining.
*
* @author Bauke Scholtz
* @since 1.2
* @see RemappedResource
* @see DefaultResourceHandler
*/
public class CDNResourceHandler extends DefaultResourceHandler {
// Constants ------------------------------------------------------------------------------------------------------
/** The context parameter name to specify CDN URLs for the given resource identifiers. */
public static final String PARAM_NAME_CDN_RESOURCES = "org.omnifaces.CDN_RESOURCE_HANDLER_URLS";
/** The context parameter name to conditionally disable CDN resource handler. @since 2.0 */
public static final String PARAM_NAME_CDN_DISABLED = "org.omnifaces.CDN_RESOURCE_HANDLER_DISABLED";
private static final String ERROR_MISSING_INIT_PARAM =
"Context parameter '" + PARAM_NAME_CDN_RESOURCES + "' is missing in web.xml or web-fragment.xml.";
private static final String ERROR_INVALID_INIT_PARAM =
"Context parameter '" + PARAM_NAME_CDN_RESOURCES + "' is in invalid syntax."
+ " It must follow 'resourceId=URL,resourceId=URL,resourceId=URL' syntax.";
private static final String ERROR_INVALID_WILDCARD =
"Context parameter '" + PARAM_NAME_CDN_RESOURCES + "' is in invalid syntax."
+ " Wildcard can only represent entire resource name '*' and URL suffix '/*' as in"
+ " 'libraryName:*=https://cdn.example.com/*'.";
// Properties -----------------------------------------------------------------------------------------------------
private String disabledParam;
private Map cdnResources;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* Creates a new instance of this CDN resource handler which wraps the given resource handler. The CDN resources
* will be initialized based on the {@value org.omnifaces.resourcehandler.CDNResourceHandler#PARAM_NAME_CDN_RESOURCES}
* context parameter.
* @param wrapped The resource handler to be wrapped.
* @throws IllegalArgumentException When the context parameter is missing or is in invalid format.
*/
public CDNResourceHandler(ResourceHandler wrapped) {
super(wrapped);
disabledParam = getInitParameter(PARAM_NAME_CDN_DISABLED);
cdnResources = initCDNResources();
if (cdnResources == null) {
throw new IllegalArgumentException(ERROR_MISSING_INIT_PARAM);
}
}
// Actions --------------------------------------------------------------------------------------------------------
/**
* If the given resource is not null
and the CDN resource handler is not (conditionally) disabled for
* the current request, then the CDN resources will be consulted if any CDN URL is available for the given resource.
* If there is none, then just return the Faces default resource, otherwise return a wrapped resource whose
* {@link Resource#getRequestPath()} returns the CDN URL as is been set in the
* {@value org.omnifaces.resourcehandler.CDNResourceHandler#PARAM_NAME_CDN_RESOURCES} context parameter.
*/
@Override
public Resource decorateResource(Resource resource, String resourceName, String libraryName) {
if (disabledParam != null && parseBoolean(String.valueOf((Object) evaluateExpressionGet(disabledParam)))) {
return resource;
}
String requestPath = null;
if (cdnResources != null) {
requestPath = cdnResources.get(new ResourceIdentifier(libraryName, resourceName));
if (requestPath == null) {
requestPath = cdnResources.get(new ResourceIdentifier(libraryName, "*"));
if (requestPath != null) {
requestPath = requestPath.substring(0, requestPath.length() - 1) + resourceName;
}
}
}
if (requestPath == null) {
return resource;
}
String evaluatedRequestPath = evaluateExpressionGet(requestPath);
return new RemappedResource(resourceName, libraryName, evaluatedRequestPath);
}
// Helpers --------------------------------------------------------------------------------------------------------
/**
* Initialize the CDN resources.
* @return The CDN resources, or null
if the context parameter has not been set.
* @throws IllegalArgumentException When the context parameter value is in invalid format.
*/
static Map initCDNResources() {
String cdnResourcesParam = getInitParameter(PARAM_NAME_CDN_RESOURCES);
if (isEmpty(cdnResourcesParam)) {
return null;
}
Map cdnResources = new HashMap<>();
for (String cdnResource : cdnResourcesParam.split("\\s*,\\s*")) {
String[] cdnResourceIdAndURL = cdnResource.split("\\s*=\\s*", 2);
if (cdnResourceIdAndURL.length != 2) {
throw new IllegalArgumentException(ERROR_INVALID_INIT_PARAM);
}
ResourceIdentifier id = new ResourceIdentifier(cdnResourceIdAndURL[0]);
if (id.getName().contains("*") && (!"*".equals(id.getName()) || !cdnResourceIdAndURL[1].endsWith("/*"))) {
throw new IllegalArgumentException(ERROR_INVALID_WILDCARD);
}
cdnResources.put(id, cdnResourceIdAndURL[1]);
}
return cdnResources;
}
}