org.apache.myfaces.component.html.util.StreamingAddResource Maven / Gradle / Ivy
Show all versions of tomahawk Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.myfaces.component.html.util;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.faces.FacesException;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.component.html.util.StreamingThreadManager.HeaderInfoEntry;
import org.apache.myfaces.renderkit.html.util.AddResource;
import org.apache.myfaces.renderkit.html.util.AddResource2;
import org.apache.myfaces.renderkit.html.util.MyFacesResourceHandler;
import org.apache.myfaces.renderkit.html.util.ResourceHandler;
import org.apache.myfaces.renderkit.html.util.ResourceLoader;
import org.apache.myfaces.renderkit.html.util.ResourcePosition;
import org.apache.myfaces.shared_tomahawk.config.MyfacesConfig;
import org.apache.myfaces.shared_tomahawk.renderkit.html.HTML;
import org.apache.myfaces.shared_tomahawk.util.ClassUtils;
import org.apache.myfaces.tomahawk.util.ExternalContextUtils;
import org.apache.myfaces.webapp.filter.PortletUtils;
/**
* This is a utility class to render link to resources used by custom components.
*
* This enables a JSF component that is within a page to register scripts and
* css stylesheets that it needs, and have them added to the page output.
* When multiple components in a page registers a need for the same script or
* stylesheet multiple times, only one reference is output in the page.
*
* The default DefaultAddResources implementation achieves this by buffering the
* complete page in memory until after rendering is complete, then post-processing
* the page; this implementation does not require buffering the output but does
* have some limitations.
*
* To enable the use of this implementation, the web.xml of the application must
* set property ????
*
* For references to external resources (javascript files or css files), the url
* rendered into the page has the special format:
*
* {contextPath}/faces/myFacesExtensionResource/
* {resourceLoaderName}/{cacheKey}/{resourceURI}
*
* Where:
*
* - {contextPath} is the context path of the current webapp
*
- {resourceLoaderName} is the fully-qualified name of a class which
* implements the ResourceLoader interface. When a browser app sends a request
* for the specified resource, an instance of the specified ResourceLoader class
* will be created and passed the resourceURI part of the URL for resolving to the
* actual resource to be served back. The standard MyFaces ResourceLoader
* implementation only serves resources for files stored beneath path
* org/apache/myfaces/custom in the classpath but non-myfaces code can provide their
* own ResourceLoader implementations.
*
- {cacheKey} is a value provided by the ResourceLoader. For the standard ResourceLoader
* that returns resources from the tomahawk jarfile, this timestamp is the datetime at which the
* tomahawk library was built. This means that browsers will cache these resources for efficiency but
* hen a new version of the tomahawk jarfile is deployed then the url changes, so that new versions
* of the resources are picked up. Where a "build timestamp" is not available, the startup time of
* the webserver is a reasonable alternative; that means that webbrowsers will cache resources until
* the webserver is restarted.
*
*
* As specified in the base AddResource interface, most methods come in two flavours: one that takes
* an explicit ResourceHandler parameter (so can be used by "user" code) and one that implicitly
* uses a ResourceHandler that serves only Tomahawk resources (ie is intended for use only by
* Tomahawk components). For the tomahawk-specific methods, the standard MyFacesResourceHandler is
* used, which in turn uses the standard MyFacesResourceLoader. However for resources that must be
* cached and served in a separate request (see below) the custom StreamingResourceHandler is used,
* which uses StreamingResourceLoader.
*
* The DefaultAddResource implementation inserts javascript file references into the head section of
* the generated page. This streaming implementation cannot do that, so it inserts them into the
* body of the page instead. This is not technically valid; according to the HTML spec references
* to external javascript files should be in the HEAD only. However all modern browsers do support this.
* There may be some corner cases where this does result in different behaviour of the page.
*
* The DefaultAddResource implementation inserts css file references into the head section of the
* generated page by post-processing the page output after all components have finished rendering.
* This streaming implementation cannot do that, and no browser supports references to stylesheets
* from within the HTML page body. Therefore this class implements a workaround: it expects that
* every page will always emits a single CSS link to a "virtual page" from its HEAD section and then
* handles the later request for that virtual page by serving any resources that really should
* have been embedded in the head of the original page. When the page uses the t:documentHead tag to
* rite the HEAD tags of the page, this link is emitted automatically. This does unfortunately mean
* that use of this StreamingAddResource always results in an extra GET request per page. It
* also means that there needs to be an application-scoped cache that holds per-request cached data,
* which introduces some issues regarding "cleanup" of the cache entries. See javadoc of method
* addStyleLoaderHere() for more details.
*
* @author Mario Ivankovits (latest modification by $Author: lu4242 $)
* @version $Revision: 954965 $ $Date: 2010-06-15 11:58:31 -0500 (Tue, 15 Jun 2010) $
*/
public class StreamingAddResource extends AddResource2
{
/**
* central place where all request store their "to be added" stylesheets
*/
//private final static Map headerInfos = new HashMap();
/**
* request counter
*/
private static long REQUEST_ID_COUNTER = 0;
/**
* own request
*/
private Long requestId;
/**
* own header infos - e.g holds the "to be added" stylesheets and a destroy time
*/
private HeaderInfoEntry headerInfoEntry;
/**
* helper to determines if the resource has already been added
*/
private Set alreadySeenResources = new TreeSet();
private static final String PATH_SEPARATOR = "/";
protected static final Log log = LogFactory.getLog(StreamingAddResource.class);
protected static final Log logSend = LogFactory.getLog(StreamingAddResource.class.getName() + ".SEND");
private static final String RESOURCE_VIRTUAL_PATH = "/faces/myFacesExtensionResource";
private static final String RESOURCES_CACHE_KEY = AddResource.class.getName() + ".CACHE_KEY";
protected String _contextPath;
private String resourceVirtualPath;
/*
public static class HeaderInfoEntry
{
private final long destroyTime = System.currentTimeMillis() + (1000 * 60); // one minute;
private final List addedInfos = new ArrayList(10);
private volatile boolean requestDone = false;
protected HeaderInfoEntry()
{
}
protected boolean isDestroyable(long now)
{
return destroyTime < now;
}
protected void addInfo(StreamablePositionedInfo positionedInfo)
{
synchronized (addedInfos)
{
addedInfos.add(positionedInfo);
addedInfos.notifyAll();
}
}
protected StreamablePositionedInfo fetchInfo() throws InterruptedException
{
synchronized (addedInfos)
{
while (addedInfos.size() < 1 && !requestDone)
{
addedInfos.wait(100);
}
if (addedInfos.size() < 1)
{
// request done
return null;
}
return (StreamablePositionedInfo) addedInfos.remove(0);
}
}
protected void setRequestDone()
{
requestDone = true;
}
}
private static class CleanupThread implements Runnable
{
// how many entries should be removed per run
private final static int CHECKS_PER_RUN = 10;
// but never reach this maximum
private final static int CACHE_LIMIT = 1000;
public void run()
{
while (!Thread.interrupted())
{
checkMap();
try
{
Thread.sleep(1000 * 30); // check every 30 sek
}
catch (InterruptedException e)
{
// ignore
}
}
}
private void checkMap()
{
synchronized (headerInfos)
{
long now = System.currentTimeMillis();
int checkNo = 0;
Iterator iterEntries = headerInfos.entrySet().iterator();
while (iterEntries.hasNext() && !Thread.currentThread().isInterrupted())
{
checkNo++;
if (headerInfos.size() < CACHE_LIMIT && checkNo > CHECKS_PER_RUN)
{
return;
}
Map.Entry entry = (Map.Entry) iterEntries.next();
HeaderInfoEntry headerInfoEntry = (HeaderInfoEntry) entry.getValue();
if (headerInfoEntry.isDestroyable(now))
{
iterEntries.remove();
}
}
}
}
}
static
{
Thread cleanupThread = new Thread(new CleanupThread(), "StreamingAddResource.CleanupThread");
cleanupThread.setDaemon(true);
cleanupThread.start();
}
*/
public StreamingAddResource()
{
}
/*
public static HeaderInfoEntry getHeaderInfo(Long requestId)
{
synchronized (headerInfos)
{
return (HeaderInfoEntry) headerInfos.get(requestId);
}
}
public static void removeHeaderInfo(Long requestId)
{
synchronized (headerInfos)
{
headerInfos.remove(requestId);
}
}*/
// Methods to add resources
public void setContextPath(String contextPath)
{
_contextPath = contextPath;
}
/**
* Insert a [script src="url"] entry at the current location in the response.
* The resource is expected to be in the classpath, at the same location as the
* specified component + "/resource".
*
* Example: when customComponent is class example.Widget, and
* resourceName is script.js, the resource will be retrieved from
* "example/Widget/resource/script.js" in the classpath.
*/
public void addJavaScriptHere(FacesContext context, Class myfacesCustomComponent,
String resourceName) throws IOException
{
addJavaScriptHere(context, new MyFacesResourceHandler(myfacesCustomComponent, resourceName));
}
/**
* Insert a [script src="url"] entry at the current location in the response.
*
* @param uri is the location of the desired resource, relative to the base
* directory of the webapp (ie its contextPath).
*/
public void addJavaScriptHere(FacesContext context, String uri) throws IOException
{
ResponseWriter writer = context.getResponseWriter();
writer.startElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_ELEM, null);
writer.writeAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_TYPE_ATTR, org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
String src = context.getExternalContext().encodeResourceURL(getResourceUri(context, uri));
writer.writeURIAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SRC_ATTR, src, null);
writer.endElement(HTML.SCRIPT_ELEM);
}
public void addJavaScriptHerePlain(FacesContext context, String uri) throws IOException
{
ResponseWriter writer = context.getResponseWriter();
writer.startElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_ELEM, null);
writer.writeAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_TYPE_ATTR, org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
String src = getResourceUri(context, uri);
writer.writeURIAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SRC_ATTR, src, null);
writer.endElement(HTML.SCRIPT_ELEM);
}
/**
* Insert a [script src="url"] entry at the current location in the response.
*
* @param context
*
* @param resourceHandler is an object which specifies exactly how to build the url
* that is emitted into the script tag. Code which needs to generate URLs in ways
* that this class does not support by default can implement a custom ResourceHandler.
*
* @throws IOException
*/
public void addJavaScriptHere(FacesContext context, ResourceHandler resourceHandler)
throws IOException
{
validateResourceHandler(resourceHandler);
ResponseWriter writer = context.getResponseWriter();
writer.startElement(HTML.SCRIPT_ELEM, null);
writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
String src = context.getExternalContext().encodeResourceURL(
getResourceUri(context, resourceHandler));
writer.writeURIAttribute(HTML.SRC_ATTR, src, null);
writer.endElement(HTML.SCRIPT_ELEM);
}
public void addResourceHere(FacesContext context, ResourceHandler resourceHandler)
throws IOException
{
validateResourceHandler(resourceHandler);
String path = getResourceUri(context, resourceHandler);
ResponseWriter writer = context.getResponseWriter();
writer.write(context.getExternalContext().encodeResourceURL(path));
}
/**
* Verify that the resource handler is acceptable. Null is not
* valid, and the getResourceLoaderClass method must return a
* Class object whose instances implements the ResourceLoader
* interface.
*
* @param resourceHandler
*/
protected void validateResourceHandler(ResourceHandler resourceHandler)
{
if (resourceHandler == null)
{
throw new IllegalArgumentException("ResourceHandler is null");
}
validateResourceLoader(resourceHandler.getResourceLoaderClass());
}
/**
* Given a Class object, verify that the instances of that class
* implement the ResourceLoader interface.
*
* @param resourceloader
*/
protected void validateResourceLoader(Class resourceloader)
{
if (!ResourceLoader.class.isAssignableFrom(resourceloader))
{
throw new FacesException("Class " + resourceloader.getName() + " must implement "
+ ResourceLoader.class.getName());
}
}
/**
* Adds the given Javascript resource to the document header at the specified
* document positioy by supplying a resourcehandler instance.
*
* Use this method to have full control about building the reference url
* to identify the resource and to customize how the resource is
* written to the response. In most cases, however, one of the convenience
* methods on this class can be used without requiring a custom ResourceHandler
* to be provided.
*
* If the script has already been referenced, it's added only once.
*
* Note that this method queues the javascript for insertion, and that
* the script is inserted into the buffered response by the ExtensionsFilter
* after the page is complete.
*/
public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position,
ResourceHandler resourceHandler)
{
addJavaScriptAtPosition(context, position, resourceHandler, false);
}
/**
* Insert a [script src="url"] entry into the document header at the
* specified document position. If the script has already been
* referenced, it's added only once.
*
* The resource is expected to be in the classpath, at the same location as the
* specified component + "/resource".
*
* Example: when customComponent is class example.Widget, and
* resourceName is script.js, the resource will be retrieved from
* "example/Widget/resource/script.js" in the classpath.
*/
public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position,
Class myfacesCustomComponent, String resourceName)
{
addJavaScriptAtPosition(context, position, new MyFacesResourceHandler(
myfacesCustomComponent, resourceName));
}
public void addJavaScriptAtPositionPlain(FacesContext context, ResourcePosition position, Class myfacesCustomComponent, String resourceName)
{
addJavaScriptAtPosition(context, position,
new MyFacesResourceHandler(myfacesCustomComponent, resourceName),
false, false);
}
/**
* Insert a [script src="url"] entry into the document header at the
* specified document position. If the script has already been
* referenced, it's added only once.
*
* @param defer specifies whether the html attribute "defer" is set on the
* generated script tag. If this is true then the browser will continue
* processing the html page without waiting for the specified script to
* load and be run.
*/
public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position,
Class myfacesCustomComponent, String resourceName, boolean defer)
{
addJavaScriptAtPosition(context, position, new MyFacesResourceHandler(
myfacesCustomComponent, resourceName), defer);
}
/**
* Insert a [script src="url"] entry into the document header at the
* specified document position. If the script has already been
* referenced, it's added only once.
*
* @param uri is the location of the desired resource, relative to the base
* directory of the webapp (ie its contextPath).
*/
public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position, String uri)
{
addJavaScriptAtPosition(context, position, uri, false);
}
/**
* Adds the given Javascript resource at the specified document position.
* If the script has already been referenced, it's added only once.
*/
public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position, String uri,
boolean defer)
{
WritablePositionedInfo info = (WritablePositionedInfo) getScriptInstance(context, uri, defer);
if (checkAlreadyAdded(info))
{
return;
}
HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
try
{
info.writePositionedInfo(response, context.getResponseWriter());
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public void addJavaScriptToBodyTag(FacesContext context, String javascriptEventName,
String addedJavaScript)
{
throw new UnsupportedOperationException();
}
/**
* Adds the given Javascript resource at the specified document position.
* If the script has already been referenced, it's added only once.
*/
public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position, ResourceHandler resourceHandler, boolean defer)
{
addJavaScriptAtPosition(context, position, resourceHandler, defer, false);
}
private void addJavaScriptAtPosition(FacesContext context, ResourcePosition position,
ResourceHandler resourceHandler, boolean defer, boolean encodeURL)
{
validateResourceHandler(resourceHandler);
WritablePositionedInfo info = (WritablePositionedInfo) getScriptInstance(context, resourceHandler, defer, encodeURL);
if (checkAlreadyAdded(info))
{
return;
}
HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
try
{
info.writePositionedInfo(response, context.getResponseWriter());
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
private boolean checkAlreadyAdded(PositionedInfo info)
{
Long key = new Long(info.hashCode());
if (alreadySeenResources.contains(key))
{
return true;
}
alreadySeenResources.add(key);
return false;
}
/**
* Adds the given Style Sheet at the specified document position.
* If the style sheet has already been referenced, it's added only once.
*/
public void addStyleSheet(FacesContext context, ResourcePosition position,
Class myfacesCustomComponent, String resourceName)
{
addStyleSheet(context, position, new MyFacesResourceHandler(myfacesCustomComponent,
resourceName));
}
/**
* Adds the given Style Sheet at the specified document position.
* If the style sheet has already been referenced, it's added only once.
*/
public void addStyleSheet(FacesContext context, ResourcePosition position, String uri)
{
uri = getAbsoluteUri(context, uri);
addStyleSheet(context, getStyleInstance(context, uri));
}
protected String getAbsoluteUri(FacesContext context, String uri)
{
if (uri.startsWith("/"))
{
return uri;
}
StringBuffer sb = new StringBuffer(80);
if (context.getExternalContext().getRequestPathInfo() != null)
{
sb.append(context.getExternalContext().getRequestPathInfo());
}
sb.append("/");
sb.append(uri);
return sb.toString();
}
private void addStyleSheet(FacesContext context, StreamablePositionedInfo styleInstance)
{
if (checkAlreadyAdded(styleInstance))
{
return;
}
StreamingThreadManager manager = (StreamingThreadManager) context.getExternalContext().getApplicationMap().get(StreamingThreadManager.KEY);
getHeaderInfoEntry().addInfo(styleInstance);
}
/**
* Adds the given Style Sheet at the specified document position.
* If the style sheet has already been referenced, it's added only once.
*/
public void addStyleSheet(FacesContext context, ResourcePosition position,
ResourceHandler resourceHandler)
{
validateResourceHandler(resourceHandler);
addStyleSheet(context, getStyleInstance(context, resourceHandler));
}
/**
* Adds the given Inline Style at the specified document position.
*/
public void addInlineStyleAtPosition(FacesContext context, ResourcePosition position, String inlineStyle)
{
addStyleSheet(context, getInlineStyleInstance(inlineStyle));
}
/**
* Adds the given Inline Script at the specified document position.
*/
public void addInlineScriptAtPosition(FacesContext context, ResourcePosition position,
String inlineScript)
{
WritablePositionedInfo info = (WritablePositionedInfo) getInlineScriptInstance(inlineScript);
if (checkAlreadyAdded(info))
{
return;
}
HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
try
{
info.writePositionedInfo(response, context.getResponseWriter());
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public String getResourceUri(FacesContext context, Class myfacesCustomComponent,
String resource, boolean withContextPath)
{
return getResourceUri(context,
new MyFacesResourceHandler(myfacesCustomComponent, resource), withContextPath);
}
public String getResourceUri(FacesContext context, Class myfacesCustomComponent, String resource)
{
return getResourceUri(context, new MyFacesResourceHandler(myfacesCustomComponent, resource));
}
/**
* Get the Path used to retrieve an resource.
*/
public String getResourceUri(FacesContext context, ResourceHandler resourceHandler)
{
String uri = resourceHandler.getResourceUri(context);
if (uri == null)
{
return getResourceUri(context, resourceHandler.getResourceLoaderClass(), true);
}
return getResourceUri(context, resourceHandler.getResourceLoaderClass(), true) + uri;
}
/**
* Get the Path used to retrieve an resource.
*/
public String getResourceUri(FacesContext context, ResourceHandler resourceHandler,
boolean withContextPath)
{
String uri = resourceHandler.getResourceUri(context);
if (uri == null)
{
return getResourceUri(context, resourceHandler.getResourceLoaderClass(),
withContextPath);
}
return getResourceUri(context, resourceHandler.getResourceLoaderClass(), withContextPath)
+ uri;
}
/**
* Get the Path used to retrieve an resource.
*/
public String getResourceUri(FacesContext context, String uri)
{
return getResourceUri(context, uri, true);
}
/**
* Get the Path used to retrieve an resource.
*/
public String getResourceUri(FacesContext context, String uri, boolean withContextPath)
{
if (withContextPath)
{
return context.getApplication().getViewHandler().getResourceURL(context, uri);
}
return uri;
}
/**
* Get the Path used to retrieve an resource.
*/
protected String getResourceUri(FacesContext context, Class resourceLoader,
boolean withContextPath)
{
StringBuffer sb = new StringBuffer(200);
sb.append(MyfacesConfig.getCurrentInstance(context.getExternalContext()).getResourceVirtualPath());
sb.append(PATH_SEPARATOR);
sb.append(resourceLoader.getName());
sb.append(PATH_SEPARATOR);
sb.append(getCacheKey(context));
sb.append(PATH_SEPARATOR);
return getResourceUri(context, sb.toString(), withContextPath);
}
/**
* Return a value used in the {cacheKey} part of a generated URL for a
* resource reference.
*
* Caching in browsers normally works by having files served to them
* include last-modified and expiry-time http headers. Until the expiry
* time is reached, a browser will silently use its cached version. After
* the expiry time, it will send a "get if modified since {time}" message,
* where {time} is the last-modified header from the version it has cached.
*
* Unfortunately this scheme only works well for resources represented as
* plain files on disk, where the webserver can easily and efficiently see
* the last-modified time of the resource file. When that query has to be
* processed by a servlet that doesn't scale well, even when it is possible
* to determine the resource's last-modified date from servlet code.
*
* Fortunately, for the AddResource class a static resource is only ever
* accessed because a URL was embedded by this class in a dynamic page.
* This makes it possible to implement caching by instead marking every
* resource served with a very long expiry time, but forcing the URL that
* points to the resource to change whenever the old cached version becomes
* invalid; the browser effectively thinks it is fetching a different
* resource that it hasn't seen before. This is implemented by embedding
* a "cache key" in the generated URL.
*
* Rather than using the actual modification date of a resource as the
* cache key, we simply use the webapp deployment time. This means that all
* data cached by browsers will become invalid after a webapp deploy (all
* the urls to the resources change). It also means that changes that occur
* to a resource without a webapp redeploy will not be seen by browsers.
*/
protected long getCacheKey(FacesContext context)
{
// cache key is hold in application scope so it is recreated on redeploying the webapp.
Map applicationMap = context.getExternalContext().getApplicationMap();
Long cacheKey = (Long) applicationMap.get(RESOURCES_CACHE_KEY);
if (cacheKey == null)
{
cacheKey = new Long(System.currentTimeMillis() / 100000);
applicationMap.put(RESOURCES_CACHE_KEY, cacheKey);
}
return cacheKey.longValue();
}
public boolean isResourceUri(ServletContext servletContext, HttpServletRequest request)
{
String path;
if (_contextPath != null)
{
path = _contextPath + getResourceVirtualPath(servletContext);
}
else
{
path = getResourceVirtualPath(servletContext);
}
//fix for TOMAHAWK-660; to be sure this fix is backwards compatible, the
//encoded context-path is only used as a first option to check for the prefix
//if we're sure this works for all cases, we can directly return the first value
//and not double-check.
try
{
if(request.getRequestURI().startsWith(URLEncoder.encode(path,"UTF-8")))
return true;
}
catch (UnsupportedEncodingException e)
{
log.error("Unsupported encoding UTF-8 used",e);
}
return request.getRequestURI().startsWith(path);
}
private Class getClass(String className) throws ClassNotFoundException
{
Class clazz = ClassUtils.classForName(className);
validateResourceLoader(clazz);
return clazz;
}
public void serveResource(ServletContext context, HttpServletRequest request,
HttpServletResponse response) throws IOException
{
String pathInfo = request.getPathInfo();
String uri = request.getContextPath() + request.getServletPath()
+ (pathInfo == null ? "" : pathInfo);
String classNameStartsAfter = getResourceVirtualPath(context) + '/';
int posStartClassName = uri.indexOf(classNameStartsAfter) + classNameStartsAfter.length();
int posEndClassName = uri.indexOf(PATH_SEPARATOR, posStartClassName);
String className = uri.substring(posStartClassName, posEndClassName);
int posEndCacheKey = uri.indexOf(PATH_SEPARATOR, posEndClassName + 1);
String resourceUri = null;
if (posEndCacheKey + 1 < uri.length())
{
resourceUri = uri.substring(posEndCacheKey + 1);
}
try
{
Class resourceLoader = getClass(className);
validateResourceLoader(resourceLoader);
((ResourceLoader) resourceLoader.newInstance()).serveResource(context, request,
response, resourceUri);
// response.flushBuffer();
// Do not call response.flushBuffer buffer here. There is no point, as if there
// ever were header data to write, this would fail as we have already written
// the response body. The only point would be to flush the output stream, but
// that will happen anyway when the servlet container closes the socket.
//
// In addition, flushing could fail here; it appears that Microsoft IE
// hasthe habit of hard-closing its socket as soon as it has received a complete
// gif file, rather than letting the server close it. The container will hopefully
// silently ignore exceptions on close.
}
catch (ClassNotFoundException e)
{
log.error("Could not find class for name: " + className, e);
sendError(response, HttpServletResponse.SC_NOT_FOUND,
"Could not find resourceloader class for name: " + className);
}
catch (InstantiationException e)
{
log.error("Could not instantiate class for name: " + className, e);
sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Could not instantiate resourceloader class for name: " + className);
}
catch (IllegalAccessException e)
{
log.error("Could not access class for name: " + className, e);
sendError(response, HttpServletResponse.SC_FORBIDDEN,
"Could not access resourceloader class for name: " + className);
}
catch (IOException e)
{
logSend.error("Error while serving resource: " +resourceUri+", message : "+ e.getMessage(), e);
sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
catch (Throwable e)
{
log.error("Unknown error while serving resource: " +resourceUri+", message : "+ e.getMessage(), e);
sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
}
protected void sendError(HttpServletResponse response, int errorCode, String errorText)
throws IOException
{
try
{
response.sendError(errorCode, errorText);
}
catch (IllegalStateException e)
{
logSend.error("Could not send error, maybe some data has already been sent.", e);
}
}
public boolean hasHeaderBeginInfos(HttpServletRequest request)
{
throw new UnsupportedOperationException();
}
/**
* Parses the response to mark the positions where code will be inserted
*/
public void parseResponse(HttpServletRequest request, String bufferedResponse,
HttpServletResponse response)
{
throw new UnsupportedOperationException();
}
/**
* Writes the javascript code necessary for myfaces in every page, just befode the closing </body> tag
*/
public void writeMyFacesJavascriptBeforeBodyEnd(HttpServletRequest request,
HttpServletResponse response) throws IOException
{
throw new UnsupportedOperationException();
}
/**
* Add the resources to the <head> of the page.
* If the head tag is missing, but the <body> tag is present, the head tag is added.
* If both are missing, no resource is added.
*
* The ordering is such that the user header CSS & JS override the MyFaces' ones.
*/
public void writeWithFullHeader(HttpServletRequest request,
HttpServletResponse response) throws IOException
{
throw new UnsupportedOperationException();
}
/**
* Writes the response
*/
public void writeResponse(HttpServletRequest request,
HttpServletResponse response) throws IOException
{
throw new UnsupportedOperationException();
}
private StylePositionedInfo getStyleInstance(FacesContext context, ResourceHandler resourceHandler)
{
return new StylePositionedInfo(getResourceUri(context, resourceHandler));
}
private PositionedInfo getScriptInstance(FacesContext context, ResourceHandler resourceHandler,
boolean defer, boolean encodeUrl)
{
return new ScriptPositionedInfo(getResourceUri(context, resourceHandler), defer, encodeUrl);
}
private StylePositionedInfo getStyleInstance(FacesContext context, String uri)
{
return new StylePositionedInfo(getResourceUri(context, uri));
}
protected PositionedInfo getScriptInstance(FacesContext context, String uri, boolean defer)
{
return new ScriptPositionedInfo(getResourceUri(context, uri), defer);
}
private PositionedInfo getInlineScriptInstance(String inlineScript)
{
return new InlineScriptPositionedInfo(inlineScript);
}
private InlineStylePositionedInfo getInlineStyleInstance(String inlineStyle)
{
return new InlineStylePositionedInfo(inlineStyle);
}
protected interface PositionedInfo
{
}
protected interface WritablePositionedInfo extends PositionedInfo
{
public abstract void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
throws IOException;
}
protected interface StreamablePositionedInfo extends PositionedInfo
{
public abstract void writePositionedInfo(HttpServletResponse response, PrintWriter writer)
throws IOException;
}
private abstract class AbstractResourceUri
{
protected final String _resourceUri;
protected AbstractResourceUri(String resourceUri)
{
_resourceUri = resourceUri;
}
public int hashCode()
{
return _resourceUri.hashCode();
}
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (obj == this)
{
return true;
}
if (obj instanceof AbstractResourceUri)
{
AbstractResourceUri other = (AbstractResourceUri) obj;
return _resourceUri.equals(other._resourceUri);
}
return false;
}
protected String getResourceUri()
{
return _resourceUri;
}
}
private class StylePositionedInfo extends AbstractResourceUri implements WritablePositionedInfo, StreamablePositionedInfo
{
protected StylePositionedInfo(String resourceUri)
{
super(resourceUri);
}
public void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
throws IOException
{
writer.startElement(HTML.LINK_ELEM, null);
writer.writeAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.REL_ATTR, org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.STYLESHEET_VALUE, null);
writer.writeAttribute(HTML.HREF_ATTR, response.encodeURL(this.getResourceUri()), null);
writer.writeAttribute(HTML.TYPE_ATTR, HTML.STYLE_TYPE_TEXT_CSS, null);
writer.endElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.LINK_ELEM);
}
public void writePositionedInfo(HttpServletResponse response, PrintWriter writer) throws IOException
{
writer.println();
writer.write("@import url(\"");
writer.write(response.encodeURL(this.getResourceUri()));
writer.write("\");");
writer.println();
}
}
private class ScriptPositionedInfo extends AbstractResourceUri implements
WritablePositionedInfo
{
protected final boolean _defer;
protected final boolean _encodeUrl;
public ScriptPositionedInfo(String resourceUri, boolean defer)
{
this(resourceUri, defer, true);
}
public ScriptPositionedInfo(String resourceUri, boolean defer, boolean encodeUrl)
{
super(resourceUri);
_defer = defer;
_encodeUrl = encodeUrl;
}
public int hashCode()
{
return new HashCodeBuilder()
.append(this.getResourceUri())
.append(_defer)
.append(_encodeUrl)
.toHashCode();
}
public boolean equals(Object obj)
{
if (super.equals(obj))
{
if (obj instanceof ScriptPositionedInfo)
{
ScriptPositionedInfo other = (ScriptPositionedInfo) obj;
return new EqualsBuilder()
.append(_defer, other._defer)
.append(_encodeUrl, other._encodeUrl)
.isEquals();
}
}
return false;
}
public void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
throws IOException
{
writer.startElement(HTML.SCRIPT_ELEM, null);
writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
if (_encodeUrl)
{
writer.writeAttribute(HTML.SRC_ATTR, response.encodeURL(this.getResourceUri()), null);
}
else
{
writer.writeAttribute(HTML.SRC_ATTR, this.getResourceUri(), null);
}
if (_defer)
{
writer.writeAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_ELEM_DEFER_ATTR, "true", null);
}
writer.endElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_ELEM);
}
}
private abstract class InlinePositionedInfo implements WritablePositionedInfo
{
private final String _inlineValue;
protected InlinePositionedInfo(String inlineValue)
{
_inlineValue = inlineValue;
}
public String getInlineValue()
{
return _inlineValue;
}
public int hashCode()
{
return new HashCodeBuilder().append(_inlineValue).toHashCode();
}
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (obj == this)
{
return true;
}
if (obj instanceof InlinePositionedInfo)
{
InlinePositionedInfo other = (InlinePositionedInfo) obj;
return new EqualsBuilder().append(_inlineValue, other._inlineValue).isEquals();
}
return false;
}
}
private class InlineScriptPositionedInfo extends InlinePositionedInfo
{
protected InlineScriptPositionedInfo(String inlineScript)
{
super(inlineScript);
}
public void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
throws IOException
{
writer.startElement(HTML.SCRIPT_ELEM, null);
writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
writer.writeText(getInlineValue(), null);
writer.endElement(HTML.SCRIPT_ELEM);
}
}
private class InlineStylePositionedInfo extends InlinePositionedInfo implements StreamablePositionedInfo
{
protected InlineStylePositionedInfo(String inlineStyle)
{
super(inlineStyle);
}
public void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
throws IOException
{
writer.startElement(HTML.STYLE_ELEM, null);
writer.writeAttribute(HTML.REL_ATTR, HTML.STYLESHEET_VALUE, null);
writer.writeAttribute(HTML.TYPE_ATTR, HTML.STYLE_TYPE_TEXT_CSS, null);
writer.writeText(getInlineValue(), null);
writer.endElement(HTML.STYLE_ELEM);
}
public void writePositionedInfo(HttpServletResponse response, PrintWriter writer) throws IOException
{
writer.println();
writer.write(getInlineValue());
writer.println();
}
}
public boolean requiresBuffer()
{
return false;
}
protected StreamingThreadManager.HeaderInfoEntry getHeaderInfoEntry()
{
if (headerInfoEntry == null)
{
throw new IllegalStateException("responseStarted() needs to be called first");
}
return headerInfoEntry;
}
public void responseStarted()
{
/*
synchronized(StreamingAddResource.class)
{
REQUEST_ID_COUNTER++;
requestId = new Long(REQUEST_ID_COUNTER);
}
headerInfoEntry = new HeaderInfoEntry();
synchronized (headerInfos)
{
headerInfos.put(requestId, headerInfoEntry);
}*/
}
public void responseFinished()
{
getHeaderInfoEntry().setRequestDone();
}
public void responseStarted(Object context, Object request)
{
if(ExternalContextUtils.getRequestType(context, request).isPortlet())
{
StreamingThreadManager manager = (StreamingThreadManager) PortletUtils.getAttribute(context, StreamingThreadManager.KEY);
requestId = manager.putNewHeaderInfoEntry();
headerInfoEntry = manager.getHeaderInfo(requestId);
}
else
{
StreamingThreadManager manager = (StreamingThreadManager) ((ServletContext)context).getAttribute(StreamingThreadManager.KEY);
requestId = manager.putNewHeaderInfoEntry();
headerInfoEntry = manager.getHeaderInfo(requestId);
}
}
public boolean hasHeaderBeginInfos()
{
return false;
}
/**
* Hack to allow pages to register CSS stylesheet files or inline CSS commands.
*
* As described in the class javadocs, the "streaming" approach for resources has problems
* when it comes to stylesheet links or inline stylesheet commands. These MUST go in the HEAD
* section of a page, but by the time a component is being rendered the HEAD section is long
* gone. The DefaultAddResource class can solve this because it buffers the page, but here
* a different approach is needed.
*
* This method should be called during rendering of the HEAD section of a page. For example,
* the t:documentHead tag (DocumentHeadRenderer) calls this automatically. A link tag of type
* CSS is written to the response, pointing at a virtual page "header.css" which does not actually exist.
*
* During rendering of the page body, component renderers may register inline CSS or CSS files. This
* info is just cached in the user session. After the page has been sent to the remote browser, the
* browser will then make a request to the virtual "header.css" page which this class intercepts and
* then serves up the resources needed by the page.
*
* Note that the link to the virtual page must always be rendered, as at this time we do
* not know whether the body of the page will contain components that need css resources or not.
* If no component did need CSS resources, then a zero-sized response is returned. And the value
* can change on each request, depending on which components are rendered or not, so a "requestId"
* is embedded into the url, making the url change for every request. This requestId is also used
* to find the relevant cached resources that need to be served (if any).
*
* The url is generated using the StreamingResourceHandler (ie StreamingResourceLoader is the class
* embedded in the url). This means that when the browser fetches this resource, the
* StreamingResourceLoader is invoked. It in turn extracts the requestId from the parameter and
* serves any "head" resources that were registered for the original page.
*
* Note that JSF2.0 solves this issue by having components queue "system events" during the "build tree"
* phase of rendering. Tomahawk could possibly provide a framework to allow its own classes to
* do this for JSF1.2. But for JSF1.1 there is no "build tree" phase so this approach is the only
* possibility.
*
* @param context
* @param myfacesCustomComponent
* @throws IOException
*/
public void addStyleLoaderHere(FacesContext context, Class myfacesCustomComponent) throws IOException
{
ResponseWriter writer = context.getResponseWriter();
writer.startElement(HTML.LINK_ELEM, null);
writer.writeAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.REL_ATTR, org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.STYLESHEET_VALUE, null);
writer.writeAttribute(HTML.HREF_ATTR,
getResourceUri(context,
new StreamingResourceHandler(requestId + "/header.css"),
true), null);
writer.writeAttribute(HTML.TYPE_ATTR, HTML.STYLE_TYPE_TEXT_CSS, null);
writer.endElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.LINK_ELEM);
}
private String getResourceVirtualPath(ServletContext servletContext)
{
if(resourceVirtualPath == null)
{
resourceVirtualPath = servletContext.getInitParameter(MyfacesConfig.INIT_PARAM_RESOURCE_VIRTUAL_PATH);
if(resourceVirtualPath == null)
{
resourceVirtualPath = MyfacesConfig.INIT_PARAM_RESOURCE_VIRTUAL_PATH_DEFAULT;
}
}
return resourceVirtualPath;
}
}