 |
This file contains notes regarding migrating Jacob Hookom's Facelets
branch release_1-2 code into jsf-extensions branch trunk.
Migration issues tagged with ISSUE:
Using FaceletFilter
=========================================================
I. How it works: order entry "Add item"
A. Map the FaceletFilter:
<filter>
<filter-name>facelets</filter-name>
<filter-class>com.sun.facelets.webapp.FaceletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>facelets</filter-name>
<url-pattern>*.jsf</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>facelets</filter-name>
<url-pattern>/FACES-INF/*</url-pattern>
</filter-mapping>
The FaceletFilter handles two kinds of requests. All other kinds of
requests are delegated to the filter chain (and thus the
FacesServlet).
1. requests to path /FACES-INF
2. requests with header javax.faces.Async
As for (1), I would like to see any novelty of this approach folded
into Shale Remoting. I would like to see (2) folded into Shale
Remoting also. (2) is used to handle AJAX requests coming from the
browser.
Jacob and Adam, can you please enumerate your issues with Shale
Remoting? What issues do you have with not using the /FACES-INF
approach and instead just using Shale Remoting? One reason why I like
shale remoting is that it autoconfigs itself and doesn't cause any
endpoint explosion, such as the need for a separate filter.
[Adam]: How does Shale Remoting address resource loading? I thought
it was a generic EL endpoint thing, and so I don't see how it applies here.
The lack of docs for Shale Remoting
(http://struts.apache.org/struts-shale/features-remoting.html) make it awfully
difficult to comment on its applicability. How does it autoconfig? Sure,
no web.xml, but you do need faces-config.xml config to add managed beans.
("endpoint explosion" applies just as much to EL methods as it does to
web.xml servlets.) I do wish there was a zero config way to add filters,
but that's got to be done by the Servlet API. I'm also guessing (though it's
not obvious) that this has to be a Filter, not a PhaseListener.
[edburns]: Adam, please look at the Shale Remoting appendix I sent for the book. You need to tech-review it anyway, and it should answer all your questions.
[jacob] instead of a filter, we could use an alternate lifecycle wrapper. one of the
issues solved in the facelets solution vs. other partials is that you can allow code
to freely write to the response without affecting the rendering of other components
on the screen. with an alternate lifecycle wrapper on the *one* FacesServlet instance,
we can check for the request header up front, if it exists, eagerly set reference to
a buffered output that can be combined later-- this is what we need anyways for supporting
client scripts, events, etc.
ISSUE: Jacob has hard-coded references to scripts in his <head> section.
I don't want to require users to do this. The requirement to manually
put this in the <head> contradicts our claim that "all you have to do is
add onclick='new Faces.Event()' to an existing commandButton and you
have AJAXified your page". This claim is not true because you also have
to put these hard-coded script elements in your <head> section:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>JavaServer Faces 1.2 JavaOne Demo</title>
<link href="style/j1/screen.css" rel="stylesheet" type="text/css" />
<script src="js/prototype.js" type="text/javascript"></script>
<script src="js/effects.js" type="text/javascript"></script>
<script src="js/dragdrop.js" type="text/javascript"></script>
<script src="js/controls.js" type="text/javascript"></script>
<script src="FACES-INF/javax.faces.js" type="text/javascript" ></script>
<ui:insert name="head"/>
</head>
[Adam] Jacob was planning on removing the dependency on prototype;
at that point, the only true dependency here would be on FACES-INF/javax.faces.js.
[jacob] this is partially true. we could go with the faces-inf convention within this
space, but as we saw in our meetings at J1, there should be a solution across the
board within the servlet spec. weblets is one other solution.
ISSUE_END
ISSUE: We shouldn't use javax.faces names, since these are reserved for
official EG projects. Let's stick to com.sun.faces for now.
[Adam] I agree.
[jacob] sure
ISSUE_END
ISSUE: new Faces.Event() currently sends a POST with all the name/value
pairs in the current form, not just the ones that pertain to this
request. It would be nice to allow pruning the name/value pair list.
[Adam]: I agree, but that's a follow-on feature. I'd like the default to be that
you submit everything, with optional JS parameters to reduce the POST
(and trigger finer-grained invokeOnComponents on the server).
[jacob] again the theme is to make everything as normal as possible by
convention, allowing you to filter the traditional lifecycle from the client or
the server.
ISSUE_END
When clicking on a button that has an onclick of new Faces.Event, we
have the following POST request.
POST http://localhost:8080/j1/eg.orderentry.jsf HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3
Accept: text/javascript, text/html, application/xml, text/xml, */*
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Proxy-Connection: keep-alive
X-Requested-With: XMLHttpRequest
X-Prototype-Version: 1.5.0_rc0
Content-Type: application/x-www-form-urlencoded
Connection: close
javax.faces.Async: true
javax.faces.Encode: details,orderTotal
Content-Length: 291
Cookie: JSESSIONID=66d8e594cb477ffffffff842229905f10c0d; test_cookie=test cookie
Pragma: no-cache
Cache-Control: no-cache
javax.faces.ViewState=j_id14%3Aj_id18&j_id4=j_id4&datagrid%3A0%3Auom=BX&datagrid%3A1%3Auom=BX&datagrid%3A2%3Auom=DZ&datagrid%3A3%3Auom=EA&datagrid%3A4%3Auom=EA&datagrid%3A5%3Auom=EA&datagrid%3A6%3Auom=EA&datagrid%3A7%3Auom=EA&datagrid%3A8%3Auom=BX&datagrid%3A9%3Auom=EA&details%3A0%3Aj_id9=x
This goes to the FaceletFilter and is picked up by the doAsync()
codepath. doAsync() creates an AsyncResponse instance in ThreadLocal
storage.
ISSUE: UIFacelet extends UIViewRoot and is hard-coded to interact with
the FaceletFilter and the AsyncResponse. To make this solution work
outside (and still with) Facelets, we can't have this dependency. The
state management interception is also baked into UIFacelet. This too
would have to move out, possibly to the custom Faces Lifecycle.
[Adam]: I agree that this will need to be renamed; but I'm unhappy with a
custom Lifecycle, since that requires a separate URL instead of just posting
back to the form "action".
[jacob] the more i played around with the custom lifecycle, the more evident it
became that the most normal solution would be to provide an alternate UIViewRoot,
allowing the lifecycle to process as normal by convention
ISSUE_END
B. Use static methods on the AsyncResponse class to craft the response
to the AJAX request issued in response to doing new Faces.Event().
The order entry example does this:
public void addProduct() throws Exception {
this.product = (Product) this.data.getRowData();
String uomVal = (String) this.uom.getValue();
Number qtyVal = (Number) this.quantity.getValue();
if (qtyVal == null || qtyVal.intValue() == 0) {
qtyVal = new Integer(1);
}
this.order.getLines().add(0,
new OrderLine(this.product, uomVal, qtyVal.intValue()));
//=========================================
// Fan-out AJAX to update the UI based on Action Logic
AsyncResponse.encode(this.total);
AsyncResponse.encode(this.header);
// optional JS effects
ClientWriter cw = AsyncResponse.getClientWriter();
cw.startScript();
cw.select(this.total, Effect.pulsate());
cw.select(this.uom, Effect.highlight());
cw.select(this.quantity, Effect.highlight(), Field.clear());
cw.endScript();
}
AsyncResponse.encode() talks to the UIFacelet ViewRoot and adds the
argument clientId to the set of ids to be encoded.
ISSUE: note the above code bakes presentation details into the business
logic code in the form of the "optional JS effects". Is there any other
use to the ClientWriter.select() call other than to add presentation
details to a component?
[Adam]: I don't see this as an issue per se; the ability to put optional JS effects
into the server code is a reasonable feature. (Not all apps need to be perfectly
architected). I would like an optional JS completion callback on Faces.Event
so JS effects can be implemented in JS.
[jacob] people want richer UIs, this can be handled on the client or on the server.
the example shown above shows how it can be done on the server.
ISSUE_END
Eventually the UIFacelet.encodeAll() method is called. This adds a
name-value pair to a Map on the AsyncResponse. The name is the
clientId. The value is the rendered markup.
After all the ids to encode have been rendered to the
AsyncResponse(), the ResponseWriter is replaced with a clever
StateCapture class that extends ResponseWriter that grabs the value
of the hidden field for the state. The AsyncResponse now has all
the information it needs to render the AJAX response.
C. Back in the doAsync() method of the filter, we write out the XML of
the AJAX response. It has the following syntax.
<async-resp state="j_id3:j_id26">
<encode id="details">
<![CDATA[
<table id="details" style="width: 100%">
<tbody>
<tr>
<td><input id="details:0:j_id9" name="details:0:j_id9" type="image" src="style/j1/delete.png" onclick="new Faces.Event(this, { encode: 'details, orderTotal' }); return false;;clear_j_5Fid4();" class="delete" /></td>
<td>1</td>
<td>EA</td>
<td>24" Aluminum Cane</td>
<td>Harold</td>
<td>$25.00</td>
</tr>
</tbody>
</table>
]]>
</encode>
<encode id="orderTotal">
<![CDATA[
<span id="orderTotal">$25.00</span>
]]>
</encode>
<data>
<![CDATA[
<script type="text/javascript">
new Effect.Pulsate($('orderTotal'));
new Effect.Highlight($('datagrid:7:uom'));
new Effect.Highlight($('datagrid:7:qty'));
$('datagrid:7:qty').value='';
</script>
]]>
</data>
</async-resp>
The javax.faces.js file knows how to take this and update the dom
accordingly.
C. The EventCallback mechanism.
If the javax.faces.Event header is present, the javax.faces.Async
header must also be present. The javax.faces.Event header value must
be the client id of a component in the tree, followed by a method name
on that component (or the renderer for that component). There must be
one and only one component identified in the javax.faces.Event header.
This method is responsible for writing content out to the response
that is handled on the client. There is an EventPhaseListener on the
after APPLY_REQUEST_VALUES phase that looks for this header and
creates an EventCallback instance. If the immediate property is set
on the callback, the EventPhaseListener executes the callback
immediately. Otherwise, the EventCallback instance is discarded, and
request processing continues as normal.
Eventually we reach the UIFacelet.encodeAll() and here we check again
for the header and create an EventCallback instance that is identical
to the one we created in the EventPhaseListener.
ISSUE: Why not queue the event somehow?
[jacob] these events happen at the component level, not at the business level.
so we either want to provide a short circuit with the optional immediate flag or
allow it to be fired when the full component tree is available.
ISSUE_END
ISSUE: Why are we doing this in the render response phase? This is
event processing. Shouldn't it be done in the invoke application phase?
I mean, I see that the method is really doing rendering, after all, so
it makes sense to put it in Render Response. However, you do have the
immediate facility.
[Adam]: Keeping rendering in Render Response is a good move to
ensure PhaseListeners fire as appropriate; this is rendering. (I wish
we'd supported a PhaseId of RENDER_RESPONSE for events; such
events would get delivered at the end of UIViewRoot.encodeAll()).
[jacob] see above
I have some concerns about the event system with respect to validation.
For example, in your suggest example, you really don't want to update
the model with each value for the suggest. In fact, doing so would most
likely yield a validation error.
[Adam]: I think we need to call FacesContext.renderResponse(), prob.
after Process Validators.
ISSUE_END.
II. Lifecycle deep-dive.
A. When does Jacob's code interrogate the request headers, and for what?
1. During APPLY_REQUEST_VALUES, the UIFacelet.decode() method populates
the list of client ids to encode from the value of the
javax.faces.Encode header. The client ids are stored as a Set on
UIFacelet in the ivar encode. It also populates the list of client ids
to update from the value of the javax.faces.Update header. For each
element in the list, an "EventCallback" is created and stored in a Set
on UIFacelet in the ivar "update".
2. After APPLY_REQUEST_VALUES, an EventPhaseListener will look at the
javax.faces.Event header, which has some syntax to contain a clientId
and a method to invoke on that component. It also has syntax to
indicate if the event should be "immediate" or not. Default is no. No
facility exists for passing parameters to the method. The current
implementation only allows for one event per HTTP request.
3. During RENDER_RESPONSE, the UIFacelet again looks at the
javax.faces.Event header and executes any event per the syntax and
constraints above.
B. What does it do with the values from the headers, once it has them.
1. javax.faces.Encode
In UIFacelet.encodeAll(), the list of ids to encode is used to
short-circuit rendering and only render those ids.
ISSUE: This means we run the whole lifecycle on *all* the tree except
for RENDER_RESPONSE, which is only run on the sub-tree. I think this is
a good idea.
ISSUE_END.
2. javax.faces.Update
In processUpdates() the UIFacelet looks if there were any
javax.faces.Update values, and if so, uses the invokeOnComponent() on
each of them.
UIFacelet.encodeAll() also checks if there are values to update, and
if so, it does not render the response, assuming that the value was correct
as entered in the client. This assumption is unsafe, since the update
may have caused validations to occurr, and thus the value
shown in the client will be incorrect.
I think we need to consider this case when doing update. I'll investigate
making the update handler check for the existence of messages, and if so,
look for a zone in the view that is a parent of the component getting updated.
If not found, the entire view will re-render. If found, just the zones that will be
affected will be rendered.
3. javax.faces.Event
see above.
I have decided not to do an AsyncResponse concept. I don't see the
benefit, and I'd rather not pay the cost of the buffering. Therefore,
I'm going to have my AjaxLifecycle just have the setupResponseWriter()
as I do now.
Also, I like the idea of only running the lifecycle on the sub-trees.
However, I can imagine there would be a need to run it on the whole
tree, perhaps only doing sub-trees for a specific phase.
I'm going to re-name the header names like this:
com.sun.faces.Encode -> com.sun.faces.RenderResponse
com.sun.faces.Update -> com.sun.faces.UpdateModelValues
com.sun.faces.Event -> un-changed.
I can imagine there will be a need for AJAX to run up to and including a
specific phase in the request processing lifecycle.
-- Main.edburns - 24 May 2006
|