Reintroducing the JSFTemplating FileStreamer
Tuesday, March 18, 2008 |In a blog entry last year, Ken Paulsen gave a short introduction to the FileStreamer utility in JSFTemplating. Since Scales is now using JSFTemplating to make the component authoring process easier, I have been able to use this facility, allowing me to deprecate some custom code. In the process of making the migration, I’ve made changes to JSFTemplating that will be of benefit to all. In this entry, I’d like to highlight those changes, and show you how you, too, can use this great facility.// more
Prior to my changes, JSFTemplating offered two ways to use the FileStreamer
. One way is as a Servlet
. This approach is intended for non-JSF users, though JSF users can certainly use it as well — many other frameworks have "resource servlets" as well. Another method is a ViewHandler
-based approached, the approach Ken uses in his blog entry, which can only be used if the JSFTemplating ViewHandler
is in play. Since I want to avoid as much external configuration as possible, and I don’t want to dictate a JSFTemplating-only approach to JSF app authoring (not that there’s anything wrong with that), I needed another method, so I created a PhaseListener
-based approach: FileStreamerPhaseListener
. Using a PhaseListener
, we are able to process the request from inside the JSF lifecycle, giving us access to application state, etc. For my purposes (namely, modifying the FileDownload
component to use the FileStreamer
), that is very important. The FileDownload
component currently works by stuffing a reference to itself in the HttpSession, which is then retrieved when the resource request is made, a process made difficult, though not impossible in a Servlet
-based approach.
While the PhaseListener
solves how to handle the incoming request, it doesn’t solve the problem of getting the data from the component. This is where FileStreamer
really shines. As Ken notes in his blog, FileStreamer
supports the idea of a ContentSource
, which is a class that handles getting the content of a source (if you can imagine that) from an arbitrary location. His examples show getting the resource from the file-system as well as a remote server, all through the same API. Leveraging that capability, I added to Scales the FileDownloadContentSource
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class FileDownloadContentSource implements ContentSource {
public final static String CONTENT_SOURCE_ID = "fileDownloadCs";
public String getId() {
return CONTENT_SOURCE_ID;
}
public InputStream getInputStream(Context context) throws IOException {
InputStream in = (InputStream) context.getAttribute("inputStream");
if (in != null) {
return in;
}
String componentId = (String) context.getAttribute(Context.FILE_PATH);
FacesContext fc = FacesContext.getCurrentInstance();
FileDownload comp = (FileDownload) fc.getExternalContext()
.getSessionMap().get("HtmlDownload-" + componentId);
if (comp != null) {
Object value = comp.getData();
if (value != null) {
String mimeType = comp.getMimeType();
context.setAttribute(Context.CONTENT_TYPE, mimeType);
if (FileDownload.METHOD_DOWNLOAD.equals(comp.getMethod())) {
context.setAttribute(Context.CONTENT_DISPOSITION, comp.getMethod());
} else {
context.setAttribute(Context.CONTENT_DISPOSITION, "inline");
}
byte[] data = null;
if (value instanceof byte[]) {
in = new ByteArrayInputStream ((byte[]) value);
} else if (value instanceof ByteArrayOutputStream) {
in = new ByteArrayInputStream (((ByteArrayOutputStream) value)
.toByteArray()); // EEEK!
} else if (value instanceof InputStream) {
in = (InputStream)value;
} else {
throw new FacesException(
"HtmlDownload: an unsupported data type was found: " +
value.getClass().getName());
}
}
}
context.setAttribute("inputStream", in);
return in;
}
public void cleanUp(Context context) {
InputStream is = (InputStream) context.getAttribute("inputStream");
// Close the InputStream
if (is != null) {
try {
is.close();
} catch (Exception ex) {
// Ignore...
}
}
context.removeAttribute("inputStream");
}
public long getLastModified(Context context) {
return -1; // We don't/can't know so make it redownload every time
}
}
The really interesting part is in getInputStream()
. We query the request for the component ID, then look it up in the session. If it is found, we then query the data returned to determine its type, then return an InputStream
which the PhaseListener
will use to stream the data to the client. I really like this ContentSource
approach. It’s very simple and elegant, and, as you can see, very easy to implement. We still have two problems remaining, though: how do I tell JSFTemplating about this new ContentSource
, and how do I generate a URL to access the data?
In Ken’s example, he used an init-param
in web.xml
to register his ContentSource`s. Again, I wasn’t too comfortable with that, as it requires more external configuration. I want people to be able to drop this component on a page and not have to worry about configuration (though there’s still the `PhaseListener
registration, which I’m working on). After some discussion with Ken, I developed, with much help from him, a mechanism by which JSFTemplating will register ContentSources
based on a properties file. This file, META-INF/jsftempalting/fileStreamer.properties
, looks something like this:
1
contentSources=com.sun.mojarra.scales.util.FileDownloadContentSource
The FileStreamer
constructor finds any of these files that might be in any JAR in the web application’s lib directory (WEB-INF/lib
) and registers the ContentSource`s specified in the comma-delimited list (i.e., `contentSources=org.example.contentSources.ExampleContentSource, org.example.contentSources.ProxyContentSource
). For the performance sensitive, this happens only once during a web app’s lifecycle, as the FileStreamer
reference is a singleton. At any rate, for the curious, here’s how the files are found, a new public and static method on FileUtil
called getJarResource()
that can be used by any application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static List<Tuple> getJarResources(FacesContext facesContext, String resourcePath,
String... searchPaths) throws IOException {
if (searchPaths == null) {
// Use default jar search path...
searchPaths = DEFAULT_SEARCH_PATH;
}
List<Tuple> entries = new ArrayList<Tuple>();
ExternalContext ec = facesContext.getExternalContext();
for (String searchPath : searchPaths) {
Set<String> paths = ec.getResourcePaths(searchPath);
for (String path : paths) {
if ("jar".equalsIgnoreCase(path.substring(path.length() - 3))) {
JarFile jarFile = new JarFile(new File(ec.getResource(path).getFile()));
JarEntry jarEntry = jarFile.getJarEntry(resourcePath);
if (jarEntry != null) {
entries.add(new Tuple(jarFile, jarEntry));
}
}
}
}
return entries;
}
The return from this method is tuple containing the JarFile
and JarEntry
for the properties file, which FileStreamer
then loops through and processes.
The only remaining issue, then, is URL creation. The new FileStreamerPhaseListener
has a utility method to handle that for us:
1
2
3
public static String createResourceUrl(FacesContext context,
String contentSourceId,
String path)
If contentSourceId
is null, the default ContentSource
is used, which is JSFTemplating’s ResourceContentSource
. In Scales' case, though, we want to use our custom ContentSource
, so our call to this method looks like this (from FileDownloadRenderer
):
1
2
3
4
protected String generateUri(FacesContext context, FileDownload comp) {
return FileStreamerPhaseListener.createResourceUrl(context,
FileDownloadContentSource.CONTENT_SOURCE_ID, comp.getClientId(context));
}
That results in a URL like this:
1
/mojarra-scales-demo-facelets/jsft_resource.jsf?contentSourceId=fileDownloadCs&filename=j_id5
The browser can request that URL, and the FileStreamerPhaseListener
will recognize that it should process it, determine and acquire the ContentSource
, then query that for the data, setting the mime type, etc., as it streams the data to the client. I am also now using this exact approach, though with the default ContentSource
, to serve up from the Scales jar file the Javascript and CSS needed for the components, demonstrating clearly, I think, the power and flexibility of this great facility.