/*
* Copyright 2017 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
*
* http://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 java.lang.String.format;
import static org.omnifaces.util.Events.subscribeToApplicationEvent;
import static org.omnifaces.util.Faces.evaluateExpressionGet;
import static org.omnifaces.util.Faces.getInitParameter;
import static org.omnifaces.util.Faces.isDevelopment;
import static org.omnifaces.util.FacesLocal.isAjaxRequestWithPartialRendering;
import static org.omnifaces.util.Hacks.isMyFacesUsed;
import static org.omnifaces.util.Renderers.RENDERER_TYPE_CSS;
import static org.omnifaces.util.Renderers.RENDERER_TYPE_JS;
import static org.omnifaces.util.Utils.isNumber;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.faces.application.Resource;
import javax.faces.application.ResourceHandler;
import javax.faces.component.UIComponent;
import javax.faces.component.UIOutput;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.PreRenderViewEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;
import org.omnifaces.component.output.cache.Cache;
import org.omnifaces.component.script.DeferredScript;
import org.omnifaces.renderer.DeferredScriptRenderer;
import org.omnifaces.renderer.InlineResourceRenderer;
import org.omnifaces.renderer.InlineScriptRenderer;
import org.omnifaces.renderer.InlineStylesheetRenderer;
import org.omnifaces.util.Faces;
import org.omnifaces.util.Hacks;
/**
* <p>
* This {@link ResourceHandler} implementation will remove all separate script and stylesheet resources which have the
* <code>target</code> attribute set to <code>"head"</code> from the {@link UIViewRoot} and create a combined one
* for all scripts and another combined one for all stylesheets. In most cases your application's pages will load
* considerably. Optionally, the combined resource files can be cached on the server during non-development stage,
* giving your application another boost (at the expense of some heap memory on the server side).
*
* <h3>Installation</h3>
* <p>
* To get it to run, this handler needs be registered as follows in <code>faces-config.xml</code>:
* <pre>
* <application>
* <resource-handler>org.omnifaces.resourcehandler.CombinedResourceHandler</resource-handler>
* </application>
* </pre>
*
* <h3>Usage</h3>
* <p>
* Noted should be that the <code>target</code> attribute of <code><h:outputStylesheet></code> already defaults to
* <code>"head"</code> but the one of <code><h:outputScript></code> not. So if you have placed this inside the
* <code><h:head></code>, then you would still need to explicitly set its <code>target</code> attribute to
* <code>"head"</code>, otherwise it will be treated as an inline script and not be combined. This is a design
* limitation. This is not necessary for <code><o:deferredScript></code>.
* <pre>
* <h:head>
* ...
* <h:outputStylesheet name="style.css" />
* <h:outputScript name="script.js" target="head" />
* <o:deferredScript name="onload.js" />
* </h:head>
* </pre>
* <p>
* If you want them to appear <em>after</em> any auto-included resources of standard JSF implementation or JSF component
* libraries, then move the declarations to top of the <code><h:body></code>. This is not necessary for
* <code><o:deferredScript></code>.
* <pre>
* <h:body>
* <h:outputStylesheet name="style.css" />
* <h:outputScript name="script.js" target="head" />
* ...
* </h:body>
* </pre>
* <p>
* The generated combined resource URL also includes the "<code>v</code>" request parameter which is the last modified
* time of the newest individual resource in minutes, so that the browser will always be forced to request the latest
* version whenever one of the individual resources has changed.
*
* <h3>Caching</h3>
* <p>
* Optionally you can activate server-side caching of the combined resource content by specifying the below context
* parameter in <code>web.xml</code> with the amount of seconds to cache the combined resource content.
* <pre>
* <context-param>
* <param-name>org.omnifaces.COMBINED_RESOURCE_HANDLER_CACHE_TTL</param-name>
* <param-value>3628800</param-value> <!-- 6 weeks -->
* </context-param>
* </pre>
* <p>
* This is only considered when the JSF project stage is <strong>not</strong> set to <code>Development</code> as per
* {@link Faces#isDevelopment()}.
* <p>
* This can speed up the initial page load considerably. In general, subsequent page loads are served from the browser
* cache, so caching doesn't make a difference on postbacks, but only on initial requests. The combined resource content
* is by default cached in an application scoped cache in heap space. This can be customized as per instructions in
* {@link Cache} javadoc. As to the heap space consumption, note that without caching the same amount of heap space is
* allocated and freed for each request that can't be served from the browser cache, so chances are you won't notice the
* memory penalty of caching.
*
* <h3>Configuration</h3>
* <p>
* The following context parameters are available:
* <table summary="All available context parameters">
* <tr><td class="colFirst">
* <code>{@value org.omnifaces.resourcehandler.CombinedResourceHandler#PARAM_NAME_EXCLUDED_RESOURCES}</code>
* </td><td>
* Comma separated string of resource identifiers of <code><h:head></code> resources which needs to be excluded
* from combining. For example:
* <br><code><param-value>primefaces:primefaces.css, javax.faces:jsf.js</param-value></code>
* <br>Any combined resource will be included <i>after</i> any of those excluded resources.
* </td></tr>
* <tr><td class="colFirst">
* <code>{@value org.omnifaces.resourcehandler.CombinedResourceHandler#PARAM_NAME_SUPPRESSED_RESOURCES}</code>
* </td><td>
* Comma separated string of resource identifiers of <code><h:head></code> resources which needs to be suppressed
* and removed. For example:
* <br><code><param-value>skinning.ecss, primefaces:jquery/jquery.js</param-value></code>
* </td></tr>
* <tr><td class="colFirst">
* <code>{@value org.omnifaces.resourcehandler.CombinedResourceHandler#PARAM_NAME_INLINE_CSS}</code>
* </td><td>
* Set to <code>true</code> if you want to render the combined CSS resources inline (embedded in HTML) instead of as a
* resource.
* </td></tr>
* <tr><td class="colFirst">
* <code>{@value org.omnifaces.resourcehandler.CombinedResourceHandler#PARAM_NAME_INLINE_JS}</code>
* </td><td>
* Set to <code>true</code> if you want to render the combined JS resources inline (embedded in HTML) instead of as a
* resource.
* </td></tr>
* <tr><td class="colFirst">
* <code>{@value org.omnifaces.resourcehandler.CombinedResourceHandler#PARAM_NAME_CACHE_TTL}</code>
* </td><td>
* Set with a value greater than 0 to activate server-side caching of the combined resource files. The value is
* interpreted as cache TTL (time to live) in seconds and is only effective when the JSF project stage is
* <strong>not</strong> set to <code>Development</code> as per {@link Faces#isDevelopment()}. Combined resource files
* are removed from the cache if they are older than this parameter indicates (and regenerated if newly requested).
* The default value is 0 (i.e. not cached). For global cache settings refer {@link Cache} javadoc.
* </td></tr>
* </table>
* <p>
* Here, the "resource identifier" is the unique combination of library name and resource name, separated by a colon,
* exactly the syntax as you would use in <code>#{resource}</code> in EL. If there is no library name, then just omit
* the colon. Valid examples of resource identifiers are <code>filename.ext</code>, <code>folder/filename.ext</code>,
* <code>library:filename.ext</code> and <code>library:folder/filename.ext</code>.
* <p>
* Note that this combined resource handler is <strong>not</strong> able to combine resources which are <em>not</em>
* been added as a component resource, but are been hardcoded in some renderer (such as <code>theme.css</code> in case
* of PrimeFaces and several JavaScript files in case of RichFaces), or are been definied using plain HTML
* <code><link></code> or <code><script></code> elements. Also, when you're using RichFaces with the context
* parameter <code>org.richfaces.resourceOptimization.enabled</code> set to <code>true</code>, then the to-be-combined
* resource cannot be resolved by a classpath URL due to RichFaces design limitations, so this combined resource handler
* will use an internal workaround to get it to work anyway, but this involves firing a HTTP request for every resource.
* The impact should however be relatively negligible as this is performed on localhost.
*
* <h3>Conditionally disable combined resource handler</h3>
* <p>
* If you'd like to supply a context parameter which conditionally disables the combined resource handler, then set the
* context parameter {@value org.omnifaces.resourcehandler.CombinedResourceHandler#PARAM_NAME_DISABLED} accordingly.
* <pre>
* <context-param>
* <param-name>org.omnifaces.COMBINED_RESOURCE_HANDLER_DISABLED</param-name>
* <param-value>true</param-value>
* </context-param>
* <!-- or -->
* <context-param>
* <param-name>org.omnifaces.COMBINED_RESOURCE_HANDLER_DISABLED</param-name>
* <param-value>#{facesContext.application.projectStage eq 'Development'}</param-value>
* </context-param>
* <!-- or -->
* <context-param>
* <param-name>org.omnifaces.COMBINED_RESOURCE_HANDLER_DISABLED</param-name>
* <param-value>#{someApplicationScopedBean.someBooleanProperty}</param-value>
* </context-param>
* </pre>
* <p>The EL expression is resolved on a per-request basis.</p>
*
* <h3>CDNResourceHandler</h3>
* <p>
* If you're also using the {@link CDNResourceHandler} or, at least, have configured its context parameter
* {@value org.omnifaces.resourcehandler.CDNResourceHandler#PARAM_NAME_CDN_RESOURCES}, then those CDN resources will
* automatically be added to the set of excluded resources.
*
* @author Bauke Scholtz
* @author Stephan Rauh {@literal <www.beyondjava.net>}
*
* @see CombinedResource
* @see CombinedResourceInfo
* @see CombinedResourceInputStream
* @see DynamicResource
* @see DefaultResourceHandler
* @see InlineScriptRenderer
* @see InlineStylesheetRenderer
* @see InlineResourceRenderer
*/
public class CombinedResourceHandler extends DefaultResourceHandler implements SystemEventListener {
// Constants ------------------------------------------------------------------------------------------------------
/** The default library name of a combined resource. Make sure that this is never used for other libraries. */
public static final String LIBRARY_NAME = "omnifaces.combined";
/** The context parameter name to conditionally disable combined resource handler. @since 2.0 */
public static final String PARAM_NAME_DISABLED =
"org.omnifaces.COMBINED_RESOURCE_HANDLER_DISABLED";
/** The context parameter name to specify resource identifiers which needs to be excluded from combining. */
public static final String PARAM_NAME_EXCLUDED_RESOURCES =
"org.omnifaces.COMBINED_RESOURCE_HANDLER_EXCLUDED_RESOURCES";
/** The context parameter name to specify resource identifiers which needs to be suppressed and removed. */
public static final String PARAM_NAME_SUPPRESSED_RESOURCES =
"org.omnifaces.COMBINED_RESOURCE_HANDLER_SUPPRESSED_RESOURCES";
/** The context parameter name to enable rendering CSS inline instead of as resource link. */
public static final String PARAM_NAME_INLINE_CSS =
"org.omnifaces.COMBINED_RESOURCE_HANDLER_INLINE_CSS";
/** The context parameter name to enable rendering JS inline instead of as resource link. */
public static final String PARAM_NAME_INLINE_JS =
"org.omnifaces.COMBINED_RESOURCE_HANDLER_INLINE_JS";
/** The context parameter name to specify cache TTL of combined resources. @since 2.1 */
public static final String PARAM_NAME_CACHE_TTL =
"org.omnifaces.COMBINED_RESOURCE_HANDLER_CACHE_TTL";
private static final String ERROR_INVALID_CACHE_TTL_PARAM =
"Context parameter '" + PARAM_NAME_CACHE_TTL + "' is in invalid syntax."
+ " It must represent a valid time in seconds between 0 and " + Integer.MAX_VALUE + "."
+ " Encountered an invalid value of '%s'.";
private static final String TARGET_HEAD = "head";
private static final String TARGET_BODY = "body";
// Properties -----------------------------------------------------------------------------------------------------
private String disabledParam;
private Set<ResourceIdentifier> excludedResources;
private Set<ResourceIdentifier> suppressedResources;
private boolean inlineCSS;
private boolean inlineJS;
private Integer cacheTTL;
// Constructors ---------------------------------------------------------------------------------------------------
/**
* Creates a new instance of this combined resource handler which wraps the given resource handler. This will also
* register this resource handler as a pre render view event listener, so that it can do the job of removing the
* CSS/JS resources and adding combined ones.
* @param wrapped The resource handler to be wrapped.
*/
public CombinedResourceHandler(ResourceHandler wrapped) {
super(wrapped);
disabledParam = getInitParameter(PARAM_NAME_DISABLED);
excludedResources = initResources(PARAM_NAME_EXCLUDED_RESOURCES);
excludedResources.addAll(initCDNResources());
suppressedResources = initResources(PARAM_NAME_SUPPRESSED_RESOURCES);
excludedResources.addAll(suppressedResources);
inlineCSS = parseBoolean(getInitParameter(PARAM_NAME_INLINE_CSS));
inlineJS = parseBoolean(getInitParameter(PARAM_NAME_INLINE_JS));
cacheTTL = initCacheTTL(getInitParameter(PARAM_NAME_CACHE_TTL));
subscribeToApplicationEvent(PreRenderViewEvent.class, this);
}
// Actions --------------------------------------------------------------------------------------------------------
/**
* Returns true if the source is an instance of {@link UIViewRoot}.
*/
@Override
public boolean isListenerForSource(Object source) {
return (source instanceof UIViewRoot);
}
/**
* Before rendering of a freshly created view, perform the following actions:
* <ul>
* <li>Collect all component resources from the head.
* <li>Check and collect the script and stylesheet resources separately and remove them from the head.
* <li>If there are any resources in the collection of script and/or stylesheet resources, then create a
* component resource component pointing to the combined resource info and add it to the head at the location of
* the first resource.
* </ul>
*/
@Override
public void processEvent(SystemEvent event) {
if (disabledParam != null && parseBoolean(String.valueOf(evaluateExpressionGet(disabledParam)))) {
return;
}
FacesContext context = FacesContext.getCurrentInstance();
UIViewRoot view = context.getViewRoot();
CombinedResourceBuilder builder = new CombinedResourceBuilder();
for (UIComponent component : view.getComponentResources(context, TARGET_HEAD)) {
if (component.getAttributes().get("name") == null) {
continue; // It's likely an inline script, they can't be combined as it might contain EL expressions.
}
builder.add(context, component, component.getRendererType(), new ResourceIdentifier(component), TARGET_HEAD);
}
for (UIComponent component : view.getComponentResources(context, TARGET_BODY)) {
if (!(component instanceof DeferredScript)) {
continue; // We currently only support deferred scripts. TODO: support body scripts as well?
}
builder.add(context, component, component.getRendererType(), new ResourceIdentifier(component), TARGET_BODY);
}
builder.create(context);
}
/**
* Returns {@link #LIBRARY_NAME}.
*/
@Override
public String getLibraryName() {
return LIBRARY_NAME;
}
/**
* Returns a new {@link CombinedResource}.
*/
@Override
public Resource createResourceFromLibrary(String resourceName, String contentType) {
return new CombinedResource(resourceName, cacheTTL);
}
// Helpers --------------------------------------------------------------------------------------------------------
/**
* Generic method to initialize set of resources based on given application initialization parameter name.
* @param name The application initialization parameter name.
* @return The set of resources which are set by the given application initialization parameter name, or an empty
* set if the parameter is not been set.
*/
private static Set<ResourceIdentifier> initResources(String name) {
Set<ResourceIdentifier> resources = new HashSet<>(1);
String configuredResources = getInitParameter(name);
if (configuredResources != null) {
for (String resourceIdentifier : configuredResources.split("\\s*,\\s*")) {
resources.add(new ResourceIdentifier(resourceIdentifier));
}
}
return resources;
}
/**
* Initialize the set of CDN resources based on {@link CDNResourceHandler} configuration.
* @return The set of CDN resources.
*/
private static Set<ResourceIdentifier> initCDNResources() {
Map<ResourceIdentifier, String> cdnResources = CDNResourceHandler.initCDNResources();
return (cdnResources != null) ? cdnResources.keySet() : Collections.<ResourceIdentifier>emptySet();
}
/**
* Initialize combined resource content cache TTL based on given application initialization parameter value.
*/
private static Integer initCacheTTL(String cacheTTLParam) {
if (!isDevelopment() && cacheTTLParam != null) {
if (isNumber(cacheTTLParam)) {
int cacheTTL = Integer.parseInt(cacheTTLParam);
if (cacheTTL > 0) {
return cacheTTL;
}
}
throw new IllegalArgumentException(format(ERROR_INVALID_CACHE_TTL_PARAM, cacheTTLParam));
}
else {
return null;
}
}
private static void removeComponentResources(FacesContext context, List<UIComponent> componentResourcesToRemove, String target) {
UIViewRoot view = context.getViewRoot();
for (UIComponent resourceToRemove : componentResourcesToRemove) {
if (resourceToRemove != null) {
UIComponent container = isMyFacesUsed() ? resourceToRemove.getParent() : resourceToRemove;
// setInView(false) forces JSF to not save dynamic remove action in state.
// Otherwise JSF will re-execute dynamic remove during restore view phase.
// This is unnecessary as CombinedResourceHandler already takes care of it.
// See also https://github.com/omnifaces/omnifaces/issues/135
container.setInView(false);
view.removeComponentResource(context, resourceToRemove, target);
container.setInView(true);
}
}
}
// Inner classes --------------------------------------------------------------------------------------------------
/**
* General builder to collect, exclude and suppress stylesheet and script component resources.
*
* @author Bauke Scholtz
*/
private final class CombinedResourceBuilder {
// Constants --------------------------------------------------------------------------------------------------
private static final String EXTENSION_CSS = ".css";
private static final String EXTENSION_JS = ".js";
// General stylesheet/script builder --------------------------------------------------------------------------
private Builder stylesheets;
private Builder scripts;
private Map<String, Builder> deferredScripts;
private List<UIComponent> componentResourcesToRemove;
public CombinedResourceBuilder() {
stylesheets = new Builder(EXTENSION_CSS, TARGET_HEAD);
scripts = new Builder(EXTENSION_JS, TARGET_HEAD);
deferredScripts = new LinkedHashMap<>();
componentResourcesToRemove = new ArrayList<>();
}
private void add(FacesContext context, UIComponent component, String rendererType, ResourceIdentifier id, String target) {
if (LIBRARY_NAME.equals(id.getLibrary())) {
addCombined(context, component, rendererType, id, target); // Found an already combined resource. Extract and recombine it.
}
else if (rendererType.equals(RENDERER_TYPE_CSS)) {
addStylesheet(context, component, id);
}
else if (rendererType.equals(RENDERER_TYPE_JS)) {
addScript(context, component, id);
}
else if (component instanceof DeferredScript) {
addDeferredScript(component, id);
}
// WARNING: START OF HACK! --------------------------------------------------------------------------------
// HACK for RichFaces4 because of its non-standard resource library handling. Resources with the extension
// ".reslib" have special treatment by RichFaces specific resource library renderer. They represent multiple
// resources which are supposed to be dynamically constructed/added with the purpose to keep resource
// dependencies in RichFaces components "DRY". So far, it are usually only JS resources.
else if (Hacks.isRichFacesResourceLibraryRenderer(rendererType)) {
Set<ResourceIdentifier> resourceIdentifiers = Hacks.getRichFacesResourceLibraryResources(id);
ResourceHandler handler = context.getApplication().getResourceHandler();
for (ResourceIdentifier identifier : resourceIdentifiers) {
add(context, null, handler.getRendererTypeForResourceName(identifier.getName()), identifier, target);
}
componentResourcesToRemove.add(component);
}
// --------------------------------------------------------------------------------------------------------
}
private void addCombined(FacesContext context, UIComponent component, String rendererType, ResourceIdentifier id, String target) {
String[] resourcePathParts = id.getName().split("\\.", 2)[0].split("/");
String resourceId = resourcePathParts[resourcePathParts.length - 1];
CombinedResourceInfo info = CombinedResourceInfo.get(resourceId);
boolean added = false;
if (info != null) {
for (ResourceIdentifier combinedId : info.getResourceIdentifiers()) {
add(context, added ? null : component, rendererType, combinedId, target);
added = true;
}
}
if (!added) {
componentResourcesToRemove.add(component);
}
}
private void addStylesheet(FacesContext context, UIComponent component, ResourceIdentifier id) {
if (stylesheets.add(component, id)) {
Hacks.setStylesheetResourceRendered(context, id); // Prevents future forced additions by libs.
}
}
private void addScript(FacesContext context, UIComponent component, ResourceIdentifier id) {
if (Hacks.isScriptResourceRendered(context, id)) { // This is true when o:deferredScript is used.
componentResourcesToRemove.add(component);
}
else if (scripts.add(component, id)) {
Hacks.setScriptResourceRendered(context, id); // Prevents future forced additions by libs.
}
}
private void addDeferredScript(UIComponent component, ResourceIdentifier id) {
String group = (String) component.getAttributes().get("group");
Builder builder = deferredScripts.get(group);
if (builder == null) {
builder = new Builder(EXTENSION_JS, TARGET_BODY);
deferredScripts.put(group, builder);
}
builder.add(component, id);
}
public void create(FacesContext context) {
stylesheets.create(context, inlineCSS ? InlineStylesheetRenderer.RENDERER_TYPE : RENDERER_TYPE_CSS);
scripts.create(context, inlineJS ? InlineScriptRenderer.RENDERER_TYPE : RENDERER_TYPE_JS);
for (Builder builder : deferredScripts.values()) {
builder.create(context, DeferredScriptRenderer.RENDERER_TYPE);
}
removeComponentResources(context, componentResourcesToRemove, TARGET_HEAD);
}
}
// Specific stylesheet/script builder -----------------------------------------------------------------------------
private final class Builder {
private String extension;
private String target;
private CombinedResourceInfo.Builder infoBuilder;
private UIComponent componentResource;
private List<UIComponent> componentResourcesToRemove;
private Builder(String extension, String target) {
this.extension = extension;
this.target = target;
infoBuilder = new CombinedResourceInfo.Builder();
componentResourcesToRemove = new ArrayList<>();
}
private boolean add(UIComponent componentResource, ResourceIdentifier resourceIdentifier) {
if ((componentResource != null && !componentResource.isRendered()) || containsResourceIdentifier(suppressedResources, resourceIdentifier)) {
componentResourcesToRemove.add(componentResource);
return true;
}
else if (!containsResourceIdentifier(excludedResources, resourceIdentifier)) {
infoBuilder.add(resourceIdentifier);
if (this.componentResource == null) {
this.componentResource = componentResource;
}
else {
if (componentResource instanceof DeferredScript) {
mergeAttribute(this.componentResource, componentResource, "onbegin");
mergeAttribute(this.componentResource, componentResource, "onsuccess");
mergeAttribute(this.componentResource, componentResource, "onerror");
}
componentResourcesToRemove.add(componentResource);
}
return true;
}
return false;
}
private boolean containsResourceIdentifier(Set<ResourceIdentifier> ids, ResourceIdentifier id) {
return !ids.isEmpty() && (ids.contains(id) || ids.contains(new ResourceIdentifier(id.getLibrary(), "*")));
}
private void mergeAttribute(UIComponent originalComponent, UIComponent newComponent, String name) {
String originalAttribute = getAttribute(originalComponent, name);
String newAttribute = getAttribute(newComponent, name);
String separator = (originalAttribute.isEmpty() || originalAttribute.endsWith(";") ? "" : ";");
originalComponent.getAttributes().put(name, originalAttribute + separator + newAttribute);
}
private String getAttribute(UIComponent component, String name) {
String attribute = (String) component.getAttributes().get(name);
return (attribute == null) ? "" : attribute.trim();
}
private void create(FacesContext context, String rendererType) {
if (!infoBuilder.isEmpty() && !isAjaxRequestWithPartialRendering(context)) { // #273, #301
if (componentResource == null) {
componentResource = new UIOutput();
context.getViewRoot().addComponentResource(context, componentResource, target);
}
componentResource.getAttributes().put("library", LIBRARY_NAME);
componentResource.getAttributes().put("name", infoBuilder.create() + extension);
componentResource.setRendererType(rendererType);
if (RENDERER_TYPE_JS.equals(rendererType)) {
componentResource.getPassThroughAttributes().put("crossorigin", "anonymous");
}
}
removeComponentResources(context, componentResourcesToRemove, target);
}
}
}