Java tutorial
/* * 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. * <p> * 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. * <p> * 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. * <p> * To enable the use of this implementation, the web.xml of the application must * set property ???? * <p> * For references to external resources (javascript files or css files), the url * rendered into the page has the special format: * <pre> * {contextPath}/faces/myFacesExtensionResource/ * {resourceLoaderName}/{cacheKey}/{resourceURI} * </pre> * Where: * <ul> * <li> {contextPath} is the context path of the current webapp * <li> {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. * <li> {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. * </ul> * <p> * 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. * <p> * 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. * <p> * 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 <i>always</i> 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$) * @version $Revision$ $Date$ */ 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". * <p> * 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. * <p> * 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. * <p> * If the script has already been referenced, it's added only once. * <p> * Note that this method <i>queues</i> 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. * <p> * The resource is expected to be in the classpath, at the same location as the * specified component + "/resource". * <p> * 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. * <p> * 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. * <p> * 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. * <p> * 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. * <p> * 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 <i>without</i> 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. * <p> * 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. * <p> * 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. * <p> * 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. * <p> * Note that the link to the virtual page must <i>always</i> 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). * <p> * 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. * <p> * 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; } }