View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.component.html.util;
20  
21  import java.io.IOException;
22  import java.io.PrintWriter;
23  import java.io.UnsupportedEncodingException;
24  import java.net.URLEncoder;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.TreeSet;
28  
29  import javax.faces.FacesException;
30  import javax.faces.context.FacesContext;
31  import javax.faces.context.ResponseWriter;
32  import javax.servlet.ServletContext;
33  import javax.servlet.http.HttpServletRequest;
34  import javax.servlet.http.HttpServletResponse;
35  
36  import org.apache.commons.lang.builder.EqualsBuilder;
37  import org.apache.commons.lang.builder.HashCodeBuilder;
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  import org.apache.myfaces.component.html.util.StreamingThreadManager.HeaderInfoEntry;
41  import org.apache.myfaces.renderkit.html.util.AddResource;
42  import org.apache.myfaces.renderkit.html.util.AddResource2;
43  import org.apache.myfaces.renderkit.html.util.MyFacesResourceHandler;
44  import org.apache.myfaces.renderkit.html.util.ResourceHandler;
45  import org.apache.myfaces.renderkit.html.util.ResourceLoader;
46  import org.apache.myfaces.renderkit.html.util.ResourcePosition;
47  import org.apache.myfaces.shared_tomahawk.config.MyfacesConfig;
48  import org.apache.myfaces.shared_tomahawk.renderkit.html.HTML;
49  import org.apache.myfaces.shared_tomahawk.util.ClassUtils;
50  import org.apache.myfaces.tomahawk.util.ExternalContextUtils;
51  import org.apache.myfaces.webapp.filter.PortletUtils;
52  
53  /**
54   * This is a utility class to render link to resources used by custom components.
55   * <p>
56   * This enables a JSF component that is within a page to register scripts and
57   * css stylesheets that it needs, and have them added to the page output.
58   * When multiple components in a page registers a need for the same script or
59   * stylesheet multiple times, only one reference is output in the page.
60   * <p>
61   * The default DefaultAddResources implementation achieves this by buffering the
62   * complete page in memory until after rendering is complete, then post-processing
63   * the page; this implementation does not require buffering the output but does
64   * have some limitations.
65   * <p>
66   * To enable the use of this implementation, the web.xml of the application must
67   * set property ????
68   * <p>
69   * For references to external resources (javascript files or css files), the url
70   * rendered into the page has the special format:
71   * <pre>
72   * {contextPath}/faces/myFacesExtensionResource/
73   *    {resourceLoaderName}/{cacheKey}/{resourceURI}
74   * </pre>
75   * Where:
76   * <ul>
77   * <li> {contextPath} is the context path of the current webapp
78   * <li> {resourceLoaderName} is the fully-qualified name of a class which
79   *  implements the ResourceLoader interface. When a browser app sends a request
80   *  for the specified resource, an instance of the specified ResourceLoader class
81   *  will be created and passed the resourceURI part of the URL for resolving to the
82   *  actual resource to be served back. The standard MyFaces ResourceLoader
83   *  implementation only serves resources for files stored beneath path
84   *  org/apache/myfaces/custom in the classpath but non-myfaces code can provide their
85   *  own ResourceLoader implementations.
86   * <li> {cacheKey} is a value provided by the ResourceLoader. For the standard ResourceLoader
87   * that returns resources from the tomahawk jarfile, this timestamp is the datetime at which the
88   * tomahawk library was built. This means that browsers will cache these resources for efficiency but
89   * hen a new version of the tomahawk jarfile is deployed then the url changes, so that new versions
90   * of the resources are picked up. Where a "build timestamp" is not available, the startup time of
91   * the webserver is a reasonable alternative; that means that webbrowsers will cache resources until
92   * the webserver is restarted.
93   * </ul>
94   * <p>
95   * As specified in the base AddResource interface, most methods come in two flavours: one that takes
96   * an explicit ResourceHandler parameter (so can be used by "user" code) and one that implicitly
97   * uses a ResourceHandler that serves only Tomahawk resources (ie is intended for use only by
98   * Tomahawk components). For the tomahawk-specific methods, the standard MyFacesResourceHandler is
99   * used, which in turn uses the standard MyFacesResourceLoader. However for resources that must be
100  * cached and served in a separate request (see below) the custom StreamingResourceHandler is used,
101  * which uses StreamingResourceLoader.
102  * <p>
103  * The DefaultAddResource implementation inserts javascript file references into the head section of
104  * the generated page. This streaming implementation cannot do that, so it inserts them into the
105  * body of the page instead. This is not technically valid; according to the HTML spec references
106  * to external javascript files should be in the HEAD only. However all modern browsers do support this.
107  * There may be some corner cases where this does result in different behaviour of the page.
108  * <p>
109  * The DefaultAddResource implementation inserts css file references into the head section of the
110  * generated page by post-processing the page output after all components have finished rendering.
111  * This streaming implementation cannot do that, and no browser supports references to stylesheets
112  * from within the HTML page body. Therefore this class implements a workaround: it expects that
113  * every page will always emits a single CSS link to a "virtual page" from its HEAD section and then
114  * handles the later request for that virtual page by serving any resources that really should
115  * have been embedded in the head of the original page. When the page uses the t:documentHead tag to
116  * rite the HEAD tags of the page, this link is emitted automatically. This does unfortunately mean
117  * that use of this StreamingAddResource <i>always</i> results in an extra GET request per page. It
118  * also means that there needs to be an application-scoped cache that holds per-request cached data,
119  * which introduces some issues regarding "cleanup" of the cache entries. See javadoc of method
120  * addStyleLoaderHere() for more details.
121  *
122  * @author Mario Ivankovits (latest modification by $Author: lu4242 $)
123  * @version $Revision: 954965 $ $Date: 2010-06-15 11:58:31 -0500 (Tue, 15 Jun 2010) $
124  */
125 public class StreamingAddResource extends AddResource2
126 {
127     /**
128      * central place where all request store their "to be added" stylesheets
129      */
130     //private final static Map headerInfos = new HashMap();
131 
132     /**
133      * request counter
134      */
135     private static long REQUEST_ID_COUNTER = 0;
136 
137     /**
138      * own request
139      */
140     private Long requestId;
141 
142     /**
143      * own header infos - e.g holds the "to be added" stylesheets and a destroy time
144      */
145     private HeaderInfoEntry headerInfoEntry;
146 
147     /**
148      * helper to determines if the resource has already been added
149      */
150     private Set alreadySeenResources = new TreeSet();
151 
152     private static final String PATH_SEPARATOR = "/";
153 
154     protected static final Log log = LogFactory.getLog(StreamingAddResource.class);
155     protected static final Log logSend = LogFactory.getLog(StreamingAddResource.class.getName() + ".SEND");
156 
157     private static final String RESOURCE_VIRTUAL_PATH = "/faces/myFacesExtensionResource";
158 
159     private static final String RESOURCES_CACHE_KEY = AddResource.class.getName() + ".CACHE_KEY";
160 
161     protected String _contextPath;
162     private String resourceVirtualPath;
163 /*
164     public static class HeaderInfoEntry
165     {
166         private final long destroyTime = System.currentTimeMillis() + (1000 * 60); // one minute;
167         private final List addedInfos = new ArrayList(10);
168         private volatile boolean requestDone = false;
169 
170         protected HeaderInfoEntry()
171         {
172         }
173 
174         protected boolean isDestroyable(long now)
175         {
176             return destroyTime < now;
177         }
178 
179         protected void addInfo(StreamablePositionedInfo positionedInfo)
180         {
181             synchronized (addedInfos)
182             {
183                 addedInfos.add(positionedInfo);
184                 addedInfos.notifyAll();
185             }
186         }
187 
188         protected StreamablePositionedInfo fetchInfo() throws InterruptedException
189         {
190             synchronized (addedInfos)
191             {
192                 while (addedInfos.size() < 1 && !requestDone)
193                 {
194                     addedInfos.wait(100);
195                 }
196                 if (addedInfos.size() < 1)
197                 {
198                     // request done
199                     return null;
200                 }
201 
202                 return (StreamablePositionedInfo) addedInfos.remove(0);
203             }
204         }
205 
206         protected void setRequestDone()
207         {
208             requestDone = true;
209         }
210     }
211 
212     private static class CleanupThread implements Runnable
213     {
214         // how many entries should be removed per run
215         private final static int CHECKS_PER_RUN = 10;
216 
217         // but never reach this maximum
218         private final static int CACHE_LIMIT = 1000;
219 
220         public void run()
221         {
222             while (!Thread.interrupted())
223             {
224                 checkMap();
225 
226                 try
227                 {
228                     Thread.sleep(1000 * 30); // check every 30 sek
229                 }
230                 catch (InterruptedException e)
231                 {
232                     // ignore
233                 }
234             }
235         }
236 
237         private void checkMap()
238         {
239             synchronized (headerInfos)
240             {
241                 long now = System.currentTimeMillis();
242 
243                 int checkNo = 0;
244                 Iterator iterEntries = headerInfos.entrySet().iterator();
245                 while (iterEntries.hasNext() && !Thread.currentThread().isInterrupted())
246                 {
247                     checkNo++;
248                     if (headerInfos.size() < CACHE_LIMIT && checkNo > CHECKS_PER_RUN)
249                     {
250                         return;
251                     }
252                     Map.Entry entry = (Map.Entry) iterEntries.next();
253                     HeaderInfoEntry headerInfoEntry = (HeaderInfoEntry) entry.getValue();
254                     if (headerInfoEntry.isDestroyable(now))
255                     {
256                         iterEntries.remove();
257                     }
258                 }
259             }
260         }
261     }
262 
263     static
264     {
265         Thread cleanupThread = new Thread(new CleanupThread(), "StreamingAddResource.CleanupThread");
266         cleanupThread.setDaemon(true);
267         cleanupThread.start();
268     }
269     */
270 
271     public StreamingAddResource()
272     {
273     }
274     /*
275     public static HeaderInfoEntry getHeaderInfo(Long requestId)
276     {
277         synchronized (headerInfos)
278         {
279             return (HeaderInfoEntry) headerInfos.get(requestId);
280         }
281     }
282 
283     public static void removeHeaderInfo(Long requestId)
284     {
285         synchronized (headerInfos)
286         {
287             headerInfos.remove(requestId);
288         }
289     }*/
290 
291     // Methods to add resources
292 
293     public void setContextPath(String contextPath)
294     {
295         _contextPath = contextPath;
296     }
297 
298     /**
299      * Insert a [script src="url"] entry at the current location in the response.
300      * The resource is expected to be in the classpath, at the same location as the
301      * specified component + "/resource".
302      * <p>
303      * Example: when customComponent is class example.Widget, and
304      * resourceName is script.js, the resource will be retrieved from
305      * "example/Widget/resource/script.js" in the classpath.
306      */
307     public void addJavaScriptHere(FacesContext context, Class myfacesCustomComponent,
308                                   String resourceName) throws IOException
309     {
310         addJavaScriptHere(context, new MyFacesResourceHandler(myfacesCustomComponent, resourceName));
311     }
312 
313     /**
314      * Insert a [script src="url"] entry at the current location in the response.
315      *
316      * @param uri is the location of the desired resource, relative to the base
317      * directory of the webapp (ie its contextPath).
318      */
319     public void addJavaScriptHere(FacesContext context, String uri) throws IOException
320     {
321         ResponseWriter writer = context.getResponseWriter();
322 
323         writer.startElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_ELEM, null);
324         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);
325         String src = context.getExternalContext().encodeResourceURL(getResourceUri(context, uri));
326         writer.writeURIAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SRC_ATTR, src, null);
327         writer.endElement(HTML.SCRIPT_ELEM);
328     }
329 
330     public void addJavaScriptHerePlain(FacesContext context, String uri) throws IOException
331     {
332         ResponseWriter writer = context.getResponseWriter();
333 
334         writer.startElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_ELEM, null);
335         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);
336         String src = getResourceUri(context, uri);
337         writer.writeURIAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SRC_ATTR, src, null);
338         writer.endElement(HTML.SCRIPT_ELEM);
339     }
340 
341     /**
342      * Insert a [script src="url"] entry at the current location in the response.
343      *
344      * @param context
345      *
346      * @param resourceHandler is an object which specifies exactly how to build the url
347      * that is emitted into the script tag. Code which needs to generate URLs in ways
348      * that this class does not support by default can implement a custom ResourceHandler.
349      *
350      * @throws IOException
351      */
352     public void addJavaScriptHere(FacesContext context, ResourceHandler resourceHandler)
353             throws IOException
354     {
355         validateResourceHandler(resourceHandler);
356 
357         ResponseWriter writer = context.getResponseWriter();
358 
359         writer.startElement(HTML.SCRIPT_ELEM, null);
360         writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
361         String src = context.getExternalContext().encodeResourceURL(
362                 getResourceUri(context, resourceHandler));
363         writer.writeURIAttribute(HTML.SRC_ATTR, src, null);
364         writer.endElement(HTML.SCRIPT_ELEM);
365     }
366 
367     public void addResourceHere(FacesContext context, ResourceHandler resourceHandler)
368             throws IOException
369     {
370         validateResourceHandler(resourceHandler);
371 
372         String path = getResourceUri(context, resourceHandler);
373         ResponseWriter writer = context.getResponseWriter();
374         writer.write(context.getExternalContext().encodeResourceURL(path));
375     }
376 
377     /**
378      * Verify that the resource handler is acceptable. Null is not
379      * valid, and the getResourceLoaderClass method must return a
380      * Class object whose instances implements the ResourceLoader
381      * interface.
382      *
383      * @param resourceHandler
384      */
385     protected void validateResourceHandler(ResourceHandler resourceHandler)
386     {
387         if (resourceHandler == null)
388         {
389             throw new IllegalArgumentException("ResourceHandler is null");
390         }
391         validateResourceLoader(resourceHandler.getResourceLoaderClass());
392     }
393 
394     /**
395      * Given a Class object, verify that the instances of that class
396      * implement the ResourceLoader interface.
397      *
398      * @param resourceloader
399      */
400     protected void validateResourceLoader(Class resourceloader)
401     {
402         if (!ResourceLoader.class.isAssignableFrom(resourceloader))
403         {
404             throw new FacesException("Class " + resourceloader.getName() + " must implement "
405                     + ResourceLoader.class.getName());
406         }
407     }
408 
409     /**
410      * Adds the given Javascript resource to the document header at the specified
411      * document positioy by supplying a resourcehandler instance.
412      * <p>
413      * Use this method to have full control about building the reference url
414      * to identify the resource and to customize how the resource is
415      * written to the response. In most cases, however, one of the convenience
416      * methods on this class can be used without requiring a custom ResourceHandler
417      * to be provided.
418      * <p>
419      * If the script has already been referenced, it's added only once.
420      * <p>
421      * Note that this method <i>queues</i> the javascript for insertion, and that
422      * the script is inserted into the buffered response by the ExtensionsFilter
423      * after the page is complete.
424      */
425     public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position,
426                                         ResourceHandler resourceHandler)
427     {
428         addJavaScriptAtPosition(context, position, resourceHandler, false);
429     }
430 
431     /**
432      * Insert a [script src="url"] entry into the document header at the
433      * specified document position. If the script has already been
434      * referenced, it's added only once.
435      * <p>
436      * The resource is expected to be in the classpath, at the same location as the
437      * specified component + "/resource".
438      * <p>
439      * Example: when customComponent is class example.Widget, and
440      * resourceName is script.js, the resource will be retrieved from
441      * "example/Widget/resource/script.js" in the classpath.
442      */
443     public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position,
444                                         Class myfacesCustomComponent, String resourceName)
445     {
446         addJavaScriptAtPosition(context, position, new MyFacesResourceHandler(
447                 myfacesCustomComponent, resourceName));
448     }
449 
450     public void addJavaScriptAtPositionPlain(FacesContext context, ResourcePosition position, Class myfacesCustomComponent, String resourceName)
451     {
452         addJavaScriptAtPosition(context, position,
453                 new MyFacesResourceHandler(myfacesCustomComponent, resourceName),
454                 false, false);
455     }
456 
457     /**
458      * Insert a [script src="url"] entry into the document header at the
459      * specified document position. If the script has already been
460      * referenced, it's added only once.
461      *
462      * @param defer specifies whether the html attribute "defer" is set on the
463      * generated script tag. If this is true then the browser will continue
464      * processing the html page without waiting for the specified script to
465      * load and be run.
466      */
467     public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position,
468                                         Class myfacesCustomComponent, String resourceName, boolean defer)
469     {
470         addJavaScriptAtPosition(context, position, new MyFacesResourceHandler(
471                 myfacesCustomComponent, resourceName), defer);
472     }
473 
474     /**
475      * Insert a [script src="url"] entry into the document header at the
476      * specified document position. If the script has already been
477      * referenced, it's added only once.
478      *
479      * @param uri is the location of the desired resource, relative to the base
480      * directory of the webapp (ie its contextPath).
481      */
482     public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position, String uri)
483     {
484         addJavaScriptAtPosition(context, position, uri, false);
485     }
486 
487     /**
488      * Adds the given Javascript resource at the specified document position.
489      * If the script has already been referenced, it's added only once.
490      */
491     public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position, String uri,
492                                         boolean defer)
493     {
494         WritablePositionedInfo info = (WritablePositionedInfo) getScriptInstance(context, uri, defer);
495         if (checkAlreadyAdded(info))
496         {
497             return;
498         }
499         HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
500         try
501         {
502             info.writePositionedInfo(response, context.getResponseWriter());
503         }
504         catch (IOException e)
505         {
506             throw new RuntimeException(e);
507         }
508     }
509 
510     public void addJavaScriptToBodyTag(FacesContext context, String javascriptEventName,
511                                        String addedJavaScript)
512     {
513         throw new UnsupportedOperationException();
514     }
515 
516     /**
517      * Adds the given Javascript resource at the specified document position.
518      * If the script has already been referenced, it's added only once.
519      */
520     public void addJavaScriptAtPosition(FacesContext context, ResourcePosition position, ResourceHandler resourceHandler, boolean defer)
521     {
522         addJavaScriptAtPosition(context, position, resourceHandler, defer, false);
523     }
524 
525     private void addJavaScriptAtPosition(FacesContext context, ResourcePosition position,
526                                          ResourceHandler resourceHandler, boolean defer, boolean encodeURL)
527     {
528         validateResourceHandler(resourceHandler);
529         WritablePositionedInfo info = (WritablePositionedInfo) getScriptInstance(context, resourceHandler, defer, encodeURL);
530         if (checkAlreadyAdded(info))
531         {
532             return;
533         }
534         HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
535         try
536         {
537             info.writePositionedInfo(response, context.getResponseWriter());
538         }
539         catch (IOException e)
540         {
541             throw new RuntimeException(e);
542         }
543     }
544 
545     private boolean checkAlreadyAdded(PositionedInfo info)
546     {
547         Long key = new Long(info.hashCode());
548         if (alreadySeenResources.contains(key))
549         {
550             return true;
551         }
552 
553         alreadySeenResources.add(key);
554         return false;
555     }
556 
557     /**
558      * Adds the given Style Sheet at the specified document position.
559      * If the style sheet has already been referenced, it's added only once.
560      */
561     public void addStyleSheet(FacesContext context, ResourcePosition position,
562                               Class myfacesCustomComponent, String resourceName)
563     {
564         addStyleSheet(context, position, new MyFacesResourceHandler(myfacesCustomComponent,
565                 resourceName));
566     }
567 
568     /**
569      * Adds the given Style Sheet at the specified document position.
570      * If the style sheet has already been referenced, it's added only once.
571      */
572     public void addStyleSheet(FacesContext context, ResourcePosition position, String uri)
573     {
574         uri = getAbsoluteUri(context, uri);
575 
576         addStyleSheet(context, getStyleInstance(context, uri));
577     }
578 
579     protected String getAbsoluteUri(FacesContext context, String uri)
580     {
581         if (uri.startsWith("/"))
582         {
583             return uri;
584         }
585 
586         StringBuffer sb = new StringBuffer(80);
587         if (context.getExternalContext().getRequestPathInfo() != null)
588         {
589             sb.append(context.getExternalContext().getRequestPathInfo());
590         }
591         sb.append("/");
592         sb.append(uri);
593 
594         return sb.toString();
595     }
596 
597     private void addStyleSheet(FacesContext context, StreamablePositionedInfo styleInstance)
598     {
599         if (checkAlreadyAdded(styleInstance))
600         {
601             return;
602         }
603         StreamingThreadManager manager = (StreamingThreadManager) context.getExternalContext().getApplicationMap().get(StreamingThreadManager.KEY);
604         getHeaderInfoEntry().addInfo(styleInstance);
605     }
606 
607     /**
608      * Adds the given Style Sheet at the specified document position.
609      * If the style sheet has already been referenced, it's added only once.
610      */
611     public void addStyleSheet(FacesContext context, ResourcePosition position,
612                               ResourceHandler resourceHandler)
613     {
614         validateResourceHandler(resourceHandler);
615         addStyleSheet(context, getStyleInstance(context, resourceHandler));
616     }
617 
618     /**
619      * Adds the given Inline Style at the specified document position.
620      */
621     public void addInlineStyleAtPosition(FacesContext context, ResourcePosition position, String inlineStyle)
622     {
623         addStyleSheet(context, getInlineStyleInstance(inlineStyle));
624     }
625 
626     /**
627      * Adds the given Inline Script at the specified document position.
628      */
629     public void addInlineScriptAtPosition(FacesContext context, ResourcePosition position,
630                                           String inlineScript)
631     {
632         WritablePositionedInfo info = (WritablePositionedInfo) getInlineScriptInstance(inlineScript);
633         if (checkAlreadyAdded(info))
634         {
635             return;
636         }
637         HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
638         try
639         {
640             info.writePositionedInfo(response, context.getResponseWriter());
641         }
642         catch (IOException e)
643         {
644             throw new RuntimeException(e);
645         }
646     }
647 
648     public String getResourceUri(FacesContext context, Class myfacesCustomComponent,
649                                  String resource, boolean withContextPath)
650     {
651         return getResourceUri(context,
652                 new MyFacesResourceHandler(myfacesCustomComponent, resource), withContextPath);
653     }
654 
655     public String getResourceUri(FacesContext context, Class myfacesCustomComponent, String resource)
656     {
657         return getResourceUri(context, new MyFacesResourceHandler(myfacesCustomComponent, resource));
658     }
659 
660     /**
661      * Get the Path used to retrieve an resource.
662      */
663     public String getResourceUri(FacesContext context, ResourceHandler resourceHandler)
664     {
665         String uri = resourceHandler.getResourceUri(context);
666         if (uri == null)
667         {
668             return getResourceUri(context, resourceHandler.getResourceLoaderClass(), true);
669         }
670         return getResourceUri(context, resourceHandler.getResourceLoaderClass(), true) + uri;
671     }
672 
673     /**
674      * Get the Path used to retrieve an resource.
675      */
676     public String getResourceUri(FacesContext context, ResourceHandler resourceHandler,
677                                  boolean withContextPath)
678     {
679         String uri = resourceHandler.getResourceUri(context);
680         if (uri == null)
681         {
682             return getResourceUri(context, resourceHandler.getResourceLoaderClass(),
683                     withContextPath);
684         }
685         return getResourceUri(context, resourceHandler.getResourceLoaderClass(), withContextPath)
686                 + uri;
687     }
688 
689     /**
690      * Get the Path used to retrieve an resource.
691      */
692     public String getResourceUri(FacesContext context, String uri)
693     {
694         return getResourceUri(context, uri, true);
695     }
696 
697     /**
698      * Get the Path used to retrieve an resource.
699      */
700     public String getResourceUri(FacesContext context, String uri, boolean withContextPath)
701     {
702         if (withContextPath)
703         {
704             return context.getApplication().getViewHandler().getResourceURL(context, uri);
705         }
706         return uri;
707     }
708 
709     /**
710      * Get the Path used to retrieve an resource.
711      */
712     protected String getResourceUri(FacesContext context, Class resourceLoader,
713                                     boolean withContextPath)
714     {
715         StringBuffer sb = new StringBuffer(200);
716         sb.append(MyfacesConfig.getCurrentInstance(context.getExternalContext()).getResourceVirtualPath());
717         sb.append(PATH_SEPARATOR);
718         sb.append(resourceLoader.getName());
719         sb.append(PATH_SEPARATOR);
720         sb.append(getCacheKey(context));
721         sb.append(PATH_SEPARATOR);
722         return getResourceUri(context, sb.toString(), withContextPath);
723     }
724 
725     /**
726      * Return a value used in the {cacheKey} part of a generated URL for a
727      * resource reference.
728      * <p>
729      * Caching in browsers normally works by having files served to them
730      * include last-modified and expiry-time http headers. Until the expiry
731      * time is reached, a browser will silently use its cached version. After
732      * the expiry time, it will send a "get if modified since {time}" message,
733      * where {time} is the last-modified header from the version it has cached.
734      * <p>
735      * Unfortunately this scheme only works well for resources represented as
736      * plain files on disk, where the webserver can easily and efficiently see
737      * the last-modified time of the resource file. When that query has to be
738      * processed by a servlet that doesn't scale well, even when it is possible
739      * to determine the resource's last-modified date from servlet code.
740      * <p>
741      * Fortunately, for the AddResource class a static resource is only ever
742      * accessed because a URL was embedded by this class in a dynamic page.
743      * This makes it possible to implement caching by instead marking every
744      * resource served with a very long expiry time, but forcing the URL that
745      * points to the resource to change whenever the old cached version becomes
746      * invalid; the browser effectively thinks it is fetching a different
747      * resource that it hasn't seen before. This is implemented by embedding
748      * a "cache key" in the generated URL.
749      * <p>
750      * Rather than using the actual modification date of a resource as the
751      * cache key, we simply use the webapp deployment time. This means that all
752      * data cached by browsers will become invalid after a webapp deploy (all
753      * the urls to the resources change). It also means that changes that occur
754      * to a resource <i>without</i> a webapp redeploy will not be seen by browsers.
755      */
756     protected long getCacheKey(FacesContext context)
757     {
758         // cache key is hold in application scope so it is recreated on redeploying the webapp.
759         Map applicationMap = context.getExternalContext().getApplicationMap();
760         Long cacheKey = (Long) applicationMap.get(RESOURCES_CACHE_KEY);
761         if (cacheKey == null)
762         {
763             cacheKey = new Long(System.currentTimeMillis() / 100000);
764             applicationMap.put(RESOURCES_CACHE_KEY, cacheKey);
765         }
766         return cacheKey.longValue();
767     }
768 
769     public boolean isResourceUri(ServletContext servletContext, HttpServletRequest request)
770     {
771         String path;
772         if (_contextPath != null)
773         {
774             path = _contextPath + getResourceVirtualPath(servletContext);
775         }
776         else
777         {
778             path = getResourceVirtualPath(servletContext);
779         }
780 
781         //fix for TOMAHAWK-660; to be sure this fix is backwards compatible, the
782         //encoded context-path is only used as a first option to check for the prefix
783         //if we're sure this works for all cases, we can directly return the first value
784         //and not double-check.
785         try
786         {
787             if(request.getRequestURI().startsWith(URLEncoder.encode(path,"UTF-8")))
788                 return true;
789         }
790         catch (UnsupportedEncodingException e)
791         {
792             log.error("Unsupported encoding UTF-8 used",e);
793 
794         }
795 
796         return request.getRequestURI().startsWith(path);
797     }
798 
799     private Class getClass(String className) throws ClassNotFoundException
800     {
801         Class clazz = ClassUtils.classForName(className);
802         validateResourceLoader(clazz);
803         return clazz;
804     }
805 
806     public void serveResource(ServletContext context, HttpServletRequest request,
807                               HttpServletResponse response) throws IOException
808     {
809         String pathInfo = request.getPathInfo();
810         String uri = request.getContextPath() + request.getServletPath()
811                 + (pathInfo == null ? "" : pathInfo);
812         String classNameStartsAfter = getResourceVirtualPath(context) + '/';
813 
814         int posStartClassName = uri.indexOf(classNameStartsAfter) + classNameStartsAfter.length();
815         int posEndClassName = uri.indexOf(PATH_SEPARATOR, posStartClassName);
816         String className = uri.substring(posStartClassName, posEndClassName);
817         int posEndCacheKey = uri.indexOf(PATH_SEPARATOR, posEndClassName + 1);
818         String resourceUri = null;
819         if (posEndCacheKey + 1 < uri.length())
820         {
821             resourceUri = uri.substring(posEndCacheKey + 1);
822         }
823 
824         try
825         {
826             Class resourceLoader = getClass(className);
827             validateResourceLoader(resourceLoader);
828             ((ResourceLoader) resourceLoader.newInstance()).serveResource(context, request,
829                     response, resourceUri);
830             // response.flushBuffer();
831             // Do not call response.flushBuffer buffer here. There is no point, as if there
832             // ever were header data to write, this would fail as we have already written
833             // the response body. The only point would be to flush the output stream, but
834             // that will happen anyway when the servlet container closes the socket.
835             //
836             // In addition, flushing could fail here; it appears that Microsoft IE
837             // hasthe habit of hard-closing its socket as soon as it has received a complete
838             // gif file, rather than letting the server close it. The container will hopefully
839             // silently ignore exceptions on close.            
840         }
841         catch (ClassNotFoundException e)
842         {
843             log.error("Could not find class for name: " + className, e);
844             sendError(response, HttpServletResponse.SC_NOT_FOUND,
845                     "Could not find resourceloader class for name: " + className);
846         }
847         catch (InstantiationException e)
848         {
849             log.error("Could not instantiate class for name: " + className, e);
850             sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
851                     "Could not instantiate resourceloader class for name: " + className);
852         }
853         catch (IllegalAccessException e)
854         {
855             log.error("Could not access class for name: " + className, e);
856             sendError(response, HttpServletResponse.SC_FORBIDDEN,
857                     "Could not access resourceloader class for name: " + className);
858         }
859         catch (IOException e)
860         {
861             logSend.error("Error while serving resource: " +resourceUri+", message : "+ e.getMessage(), e);
862             sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
863         }
864         catch (Throwable e)
865         {
866             log.error("Unknown error while serving resource: " +resourceUri+", message : "+ e.getMessage(), e);
867             sendError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
868         }
869     }
870 
871     protected void sendError(HttpServletResponse response, int errorCode, String errorText)
872         throws IOException
873     {
874         try
875         {
876             response.sendError(errorCode, errorText);
877         }
878         catch (IllegalStateException e)
879         {
880             logSend.error("Could not send error, maybe some data has already been sent.", e);
881         }
882     }
883 
884     public boolean hasHeaderBeginInfos(HttpServletRequest request)
885     {
886         throw new UnsupportedOperationException();
887     }
888 
889     /**
890      * Parses the response to mark the positions where code will be inserted
891      */
892     public void parseResponse(HttpServletRequest request, String bufferedResponse,
893                               HttpServletResponse response)
894     {
895         throw new UnsupportedOperationException();
896     }
897 
898     /**
899      * Writes the javascript code necessary for myfaces in every page, just befode the closing &lt;/body&gt; tag
900      */
901     public void writeMyFacesJavascriptBeforeBodyEnd(HttpServletRequest request,
902                                                     HttpServletResponse response) throws IOException
903     {
904         throw new UnsupportedOperationException();       
905     }
906 
907     /**
908      * Add the resources to the &lt;head&gt; of the page.
909      * If the head tag is missing, but the &lt;body&gt; tag is present, the head tag is added.
910      * If both are missing, no resource is added.
911      *
912      * The ordering is such that the user header CSS & JS override the MyFaces' ones.
913      */
914     public void writeWithFullHeader(HttpServletRequest request,
915                                     HttpServletResponse response) throws IOException
916     {
917         throw new UnsupportedOperationException();
918     }
919 
920     /**
921      * Writes the response
922      */
923     public void writeResponse(HttpServletRequest request,
924                               HttpServletResponse response) throws IOException
925     {
926         throw new UnsupportedOperationException();
927     }
928 
929     private StylePositionedInfo getStyleInstance(FacesContext context, ResourceHandler resourceHandler)
930     {
931         return new StylePositionedInfo(getResourceUri(context, resourceHandler));
932     }
933 
934     private PositionedInfo getScriptInstance(FacesContext context, ResourceHandler resourceHandler,
935                                              boolean defer, boolean encodeUrl)
936     {
937         return new ScriptPositionedInfo(getResourceUri(context, resourceHandler), defer, encodeUrl);
938     }
939 
940     private StylePositionedInfo getStyleInstance(FacesContext context, String uri)
941     {
942         return new StylePositionedInfo(getResourceUri(context, uri));
943     }
944 
945     protected PositionedInfo getScriptInstance(FacesContext context, String uri, boolean defer)
946     {
947         return new ScriptPositionedInfo(getResourceUri(context, uri), defer);
948     }
949 
950     private PositionedInfo getInlineScriptInstance(String inlineScript)
951     {
952         return new InlineScriptPositionedInfo(inlineScript);
953     }
954 
955     private InlineStylePositionedInfo getInlineStyleInstance(String inlineStyle)
956     {
957         return new InlineStylePositionedInfo(inlineStyle);
958     }
959 
960     protected interface PositionedInfo
961     {
962     }
963 
964     protected interface WritablePositionedInfo extends PositionedInfo
965     {
966         public abstract void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
967                 throws IOException;
968     }
969 
970     protected interface StreamablePositionedInfo extends PositionedInfo
971     {
972         public abstract void writePositionedInfo(HttpServletResponse response, PrintWriter writer)
973                 throws IOException;
974     }
975 
976     private abstract class AbstractResourceUri
977     {
978         protected final String _resourceUri;
979 
980         protected AbstractResourceUri(String resourceUri)
981         {
982             _resourceUri = resourceUri;
983         }
984 
985         public int hashCode()
986         {
987             return _resourceUri.hashCode();
988         }
989 
990         public boolean equals(Object obj)
991         {
992             if (obj == null)
993             {
994                 return false;
995             }
996             if (obj == this)
997             {
998                 return true;
999             }
1000             if (obj instanceof AbstractResourceUri)
1001             {
1002                 AbstractResourceUri other = (AbstractResourceUri) obj;
1003                 return _resourceUri.equals(other._resourceUri);
1004             }
1005             return false;
1006         }
1007 
1008         protected String getResourceUri()
1009         {
1010             return _resourceUri;
1011         }
1012     }
1013 
1014     private class StylePositionedInfo extends AbstractResourceUri implements WritablePositionedInfo, StreamablePositionedInfo
1015     {
1016         protected StylePositionedInfo(String resourceUri)
1017         {
1018             super(resourceUri);
1019         }
1020 
1021         public void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
1022                 throws IOException
1023         {
1024             writer.startElement(HTML.LINK_ELEM, null);
1025             writer.writeAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.REL_ATTR, org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.STYLESHEET_VALUE, null);
1026             writer.writeAttribute(HTML.HREF_ATTR, response.encodeURL(this.getResourceUri()), null);
1027             writer.writeAttribute(HTML.TYPE_ATTR, HTML.STYLE_TYPE_TEXT_CSS, null);
1028             writer.endElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.LINK_ELEM);
1029         }
1030 
1031         public void writePositionedInfo(HttpServletResponse response, PrintWriter writer) throws IOException
1032         {
1033             writer.println();
1034             writer.write("@import url(\"");
1035             writer.write(response.encodeURL(this.getResourceUri()));
1036             writer.write("\");");
1037             writer.println();
1038         }
1039     }
1040 
1041     private class ScriptPositionedInfo extends AbstractResourceUri implements
1042             WritablePositionedInfo
1043     {
1044         protected final boolean _defer;
1045         protected final boolean _encodeUrl;
1046 
1047         public ScriptPositionedInfo(String resourceUri, boolean defer)
1048         {
1049             this(resourceUri, defer, true);
1050         }
1051 
1052         public ScriptPositionedInfo(String resourceUri, boolean defer, boolean encodeUrl)
1053         {
1054             super(resourceUri);
1055             _defer = defer;
1056             _encodeUrl = encodeUrl;
1057         }
1058 
1059         public int hashCode()
1060         {
1061             return new HashCodeBuilder()
1062                 .append(this.getResourceUri())
1063                 .append(_defer)
1064                 .append(_encodeUrl)
1065                 .toHashCode();
1066         }
1067 
1068         public boolean equals(Object obj)
1069         {
1070             if (super.equals(obj))
1071             {
1072                 if (obj instanceof ScriptPositionedInfo)
1073                 {
1074                     ScriptPositionedInfo other = (ScriptPositionedInfo) obj;
1075                     return new EqualsBuilder()
1076                         .append(_defer, other._defer)
1077                         .append(_encodeUrl, other._encodeUrl)
1078                         .isEquals();
1079                 }
1080             }
1081             return false;
1082         }
1083 
1084         public void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
1085                 throws IOException
1086         {
1087             writer.startElement(HTML.SCRIPT_ELEM, null);
1088             writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
1089             if (_encodeUrl)
1090             {
1091                 writer.writeAttribute(HTML.SRC_ATTR, response.encodeURL(this.getResourceUri()), null);
1092             }
1093             else
1094             {
1095                 writer.writeAttribute(HTML.SRC_ATTR, this.getResourceUri(), null);
1096             }
1097 
1098             if (_defer)
1099             {
1100                 writer.writeAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_ELEM_DEFER_ATTR, "true", null);
1101             }
1102             writer.endElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_ELEM);
1103         }
1104     }
1105 
1106     private abstract class InlinePositionedInfo implements WritablePositionedInfo
1107     {
1108         private final String _inlineValue;
1109 
1110         protected InlinePositionedInfo(String inlineValue)
1111         {
1112             _inlineValue = inlineValue;
1113         }
1114 
1115         public String getInlineValue()
1116         {
1117             return _inlineValue;
1118         }
1119 
1120         public int hashCode()
1121         {
1122             return new HashCodeBuilder().append(_inlineValue).toHashCode();
1123         }
1124 
1125         public boolean equals(Object obj)
1126         {
1127             if (obj == null)
1128             {
1129                 return false;
1130             }
1131             if (obj == this)
1132             {
1133                 return true;
1134             }
1135             if (obj instanceof InlinePositionedInfo)
1136             {
1137                 InlinePositionedInfo other = (InlinePositionedInfo) obj;
1138                 return new EqualsBuilder().append(_inlineValue, other._inlineValue).isEquals();
1139             }
1140             return false;
1141         }
1142     }
1143 
1144     private class InlineScriptPositionedInfo extends InlinePositionedInfo
1145     {
1146         protected InlineScriptPositionedInfo(String inlineScript)
1147         {
1148             super(inlineScript);
1149         }
1150 
1151         public void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
1152                 throws IOException
1153         {
1154             writer.startElement(HTML.SCRIPT_ELEM, null);
1155             writer.writeAttribute(HTML.SCRIPT_TYPE_ATTR, org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.SCRIPT_TYPE_TEXT_JAVASCRIPT, null);
1156             writer.writeText(getInlineValue(), null);
1157             writer.endElement(HTML.SCRIPT_ELEM);
1158         }
1159     }
1160 
1161     private class InlineStylePositionedInfo extends InlinePositionedInfo implements StreamablePositionedInfo
1162     {
1163         protected InlineStylePositionedInfo(String inlineStyle)
1164         {
1165             super(inlineStyle);
1166         }
1167 
1168         public void writePositionedInfo(HttpServletResponse response, ResponseWriter writer)
1169                 throws IOException
1170         {
1171             writer.startElement(HTML.STYLE_ELEM, null);
1172             writer.writeAttribute(HTML.REL_ATTR, HTML.STYLESHEET_VALUE, null);
1173             writer.writeAttribute(HTML.TYPE_ATTR, HTML.STYLE_TYPE_TEXT_CSS, null);
1174             writer.writeText(getInlineValue(), null);
1175             writer.endElement(HTML.STYLE_ELEM);
1176         }
1177 
1178         public void writePositionedInfo(HttpServletResponse response, PrintWriter writer) throws IOException
1179         {
1180             writer.println();
1181             writer.write(getInlineValue());
1182             writer.println();
1183         }
1184     }
1185 
1186     public boolean requiresBuffer()
1187     {
1188         return false;
1189     }
1190 
1191     protected StreamingThreadManager.HeaderInfoEntry getHeaderInfoEntry()
1192     {
1193         if (headerInfoEntry == null)
1194         {
1195             throw new IllegalStateException("responseStarted() needs to be called first");
1196         }
1197 
1198         return headerInfoEntry;
1199     }
1200 
1201     public void responseStarted()
1202     {
1203         /*
1204         synchronized(StreamingAddResource.class)
1205         {
1206             REQUEST_ID_COUNTER++;
1207             requestId = new Long(REQUEST_ID_COUNTER);
1208         }
1209         headerInfoEntry = new HeaderInfoEntry();
1210         synchronized (headerInfos)
1211         {
1212             headerInfos.put(requestId, headerInfoEntry);
1213         }*/
1214     }
1215 
1216     public void responseFinished()
1217     {
1218         getHeaderInfoEntry().setRequestDone();
1219     }
1220     
1221     public void responseStarted(Object context, Object request)
1222     {
1223         if(ExternalContextUtils.getRequestType(context, request).isPortlet())
1224         {
1225             StreamingThreadManager manager = (StreamingThreadManager) PortletUtils.getAttribute(context, StreamingThreadManager.KEY);
1226             requestId = manager.putNewHeaderInfoEntry();
1227             headerInfoEntry = manager.getHeaderInfo(requestId);
1228         }
1229         else
1230         {
1231             StreamingThreadManager manager = (StreamingThreadManager) ((ServletContext)context).getAttribute(StreamingThreadManager.KEY);
1232             requestId = manager.putNewHeaderInfoEntry();
1233             headerInfoEntry = manager.getHeaderInfo(requestId);
1234         }
1235     }
1236 
1237     public boolean hasHeaderBeginInfos()
1238     {
1239         return false;
1240     }
1241 
1242     /**
1243      * Hack to allow pages to register CSS stylesheet files or inline CSS commands.
1244      * <p>
1245      * As described in the class javadocs, the "streaming" approach for resources has problems
1246      * when it comes to stylesheet links or inline stylesheet commands. These MUST go in the HEAD
1247      * section of a page, but by the time a component is being rendered the HEAD section is long
1248      * gone. The DefaultAddResource class can solve this because it buffers the page, but here
1249      * a different approach is needed.
1250      * <p>
1251      * This method should be called during rendering of the HEAD section of a page. For example,
1252      * the t:documentHead tag (DocumentHeadRenderer) calls this automatically. A link tag of type
1253      * CSS is written to the response, pointing at a virtual page "header.css" which does not actually exist.
1254      * <p>
1255      * During rendering of the page body, component renderers may register inline CSS or CSS files. This
1256      * info is just cached in the user session. After the page has been sent to the remote browser, the
1257      * browser will then make a request to the virtual "header.css" page which this class intercepts and
1258      * then serves up the resources needed by the page.
1259      * <p>
1260      * Note that the link to the virtual page must <i>always</i> be rendered, as at this time we do
1261      * not know whether the body of the page will contain components that need css resources or not.
1262      * If no component did need CSS resources, then a zero-sized response is returned. And the value
1263      * can change on each request, depending on which components are rendered or not, so a "requestId"
1264      * is embedded into the url, making the url change for every request. This requestId is also used
1265      * to find the relevant cached resources that need to be served (if any).
1266      * <p>
1267      * The url is generated using the StreamingResourceHandler (ie StreamingResourceLoader is the class
1268      * embedded in the url). This means that when the browser fetches this resource, the 
1269      * StreamingResourceLoader is invoked. It in turn extracts the requestId from the parameter and
1270      * serves any "head" resources that were registered for the original page.
1271      * <p>
1272      * Note that JSF2.0 solves this issue by having components queue "system events" during the "build tree"
1273      * phase of rendering. Tomahawk could possibly provide a framework to allow its own classes to
1274      * do this for JSF1.2. But for JSF1.1 there is no "build tree" phase so this approach is the only
1275      * possibility.
1276      *   
1277      * @param context
1278      * @param myfacesCustomComponent
1279      * @throws IOException
1280      */
1281     public void addStyleLoaderHere(FacesContext context, Class myfacesCustomComponent) throws IOException
1282     {
1283         ResponseWriter writer = context.getResponseWriter();
1284 
1285         writer.startElement(HTML.LINK_ELEM, null);
1286         writer.writeAttribute(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.REL_ATTR, org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.STYLESHEET_VALUE, null);
1287         writer.writeAttribute(HTML.HREF_ATTR,
1288                 getResourceUri(context,
1289                         new StreamingResourceHandler(requestId + "/header.css"),
1290                         true), null);
1291         writer.writeAttribute(HTML.TYPE_ATTR, HTML.STYLE_TYPE_TEXT_CSS, null);
1292         writer.endElement(org.apache.myfaces.shared_tomahawk.renderkit.html.HTML.LINK_ELEM);
1293     }
1294 
1295     private String getResourceVirtualPath(ServletContext servletContext)
1296     {
1297         if(resourceVirtualPath == null)
1298         {
1299             resourceVirtualPath = servletContext.getInitParameter(MyfacesConfig.INIT_PARAM_RESOURCE_VIRTUAL_PATH);
1300 
1301             if(resourceVirtualPath == null)
1302             {
1303                 resourceVirtualPath = MyfacesConfig.INIT_PARAM_RESOURCE_VIRTUAL_PATH_DEFAULT;
1304             }
1305         }
1306 
1307         return resourceVirtualPath;
1308     }
1309 }