This post explains how JSF generates the components IDs/clientIds. Well, the story begins in the UniqueIdVendor
interface. Conforming to documentation this interface is "implemented by UIComponents that also implement NamingContainer so that they can provide unique ids based
on their own clientId. This will reduce the amount of id generation variance
between different renderings of the same view and is helpful for improved state
saving" - source: JSF API Documentation.
So, the UniqueIdVendor
inteface is the starting point. It defines a single method that is implemented
for generating an unique ID:
java.lang.String
createUniqueId(FacesContext context, java.lang.String seed)
Via this
method, JSF generates an ID like the one in figure below:
The UNIQUE_ID_PREFIX
is a constant defined in UIViewRoot, as j_id. This prefix is
concatenated with a seed of type tn
(e.g. t1,
t2,
t3,
... tn).
Actually, the tn seed is the JSF default seed,
but we can instruct JSF to use a custom seed by explicitly calling one of createUniqueId()
implementations from UINamingContainer, UIViewRoot, UIForm
or UIData.
So we can say that JSF creates IDs via view root or via a naming container.
In Mojarra,
the JSF default seed comes from an internal class named IdMapper that
"hides" a concurrent caching mechanism. Basically, the Facelets
generated unique IDs are quite long (e.g. 2059540600_7ac21875), which
means that a generated ID should be like: j_id2059540600_7ac21875.
Obviously, this is at least not nice, so JSF uses the IdMapper to provide
aliases of type tn. Each long ID have an alias
that can be obtained from IdMapper via getAliasedId() method.
When the
view is created, the view root ID will be "computed" via createUniqueId()
method without arguments. This method will actually call the UIViewRoot.createUniqueId(FacesContext
context, String seed) by passing null for the seed. This will
make the view root ID equal to j_id1 (no seed here). In a common JSF page as
below, the first "visible" ID in the source code will be the ID
generated for the <h:head> component, as j_idt2 (actually, this is
the clientId) - practically, JSF assign
a generated ID to each component/HTML tag/text in the page, even if that ID is not visible in
markup:
<?xml
version='1.0' encoding='UTF-8' ?>
<!DOCTYPE
html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
Hello from Facelets
</h:body>
</html>
The source
code of the rendered markup is below (basically, we can say that, in this case,
the <h:head>
is the second component in component tree (UIViewRoot is the first), or the second child of view root,
because the j_idt1
is the ID of the <html> tag, which is the first child of view root):
<?xml
version='1.0' encoding='UTF-8' ?>
<!DOCTYPE
html>
<html
xmlns="http://www.w3.org/1999/xhtml">
<head id="j_idt2">
<title>Facelet
Title</title>
</head>
<body>
Hello from Facelets
</body>
</html>
The creation
of the view IDs (excepting the creation of the view root ID, first child ID and
plain HTML tags/text IDs) is "orchestrated" from the ComponentTagHandlerDelegateImpl
class. Each time an ID needs to be created, the ComponentTagHandlerDelegateImpl
will decide who is responsible to accomplish this task via the below protected
method (if the returned ID is not null, then this become the component ID
via setId()
method):
// Mojarra
2.2.9 source code, class ComponentTagHandlerDelegateImpl
protected
String createUniqueId(FaceletContext ctx, UIComponent parent, String id) {
String uniqueId = null;
if (this.id != null &&
!(this.id.isLiteral() && IterationIdManager.registerLiteralId(ctx,
this.id.getValue()))) {
uniqueId = this.id.getValue(ctx);
} else {
UIViewRoot root =
ComponentSupport.getViewRoot(ctx, parent);
if (root != null) {
String uid;
IdMapper mapper =
IdMapper.getMapper(ctx.getFacesContext());
String mid = ((mapper != null) ?
mapper.getAliasedId(id) : id);
UIComponent ancestorNamingContainer =
parent.getNamingContainer();
if (null != ancestorNamingContainer
&& ancestorNamingContainer instanceof UniqueIdVendor) {
uid = ((UniqueIdVendor)
ancestorNamingContainer).createUniqueId(ctx.getFacesContext(), mid);
} else {
uid =
root.createUniqueId(ctx.getFacesContext(), mid);
}
uniqueId = uid;
}
}
return uniqueId;
}
So, we
distinguish here three cases:
·
When the current component has an explicit ID, this
method will return that ID (uniqueId = this.id.getValue(ctx);)
·
Find the closest component in the ancestry that
is a NamingContainer
(controls the state of its children and affect how the clientId is generated), and if such
component exist, then delegate it to generate an unique ID based on the passed
seed (obtained from IdMapper).
·
If a NamingContainer cannot be found, then
delegate the task of generating the component unique ID to the view root
(again, based on the passed seed (obtained from IdMapper)).
We can easily conclude that each JSF generated ID is created by the view root or by the closest naming container of the current component.
When the ID should be created by the UIViewRoot, the below method will do it:
// Mojarra
2.2.9 source code, class UIViewRoot
public
String createUniqueId(FacesContext context, String seed) {
if (seed != null) {
return UIViewRoot.UNIQUE_ID_PREFIX + seed;
} else {
Integer i = (Integer)
getStateHelper().get(PropertyKeys.lastId);
int lastId = ((i != null) ? i : 0);
getStateHelper().put(PropertyKeys.lastId, ++lastId);
return UIViewRoot.UNIQUE_ID_PREFIX +
lastId;
}
}
So, if no
seed is provided, then JSF uses a integer incremented by 1 (lastId)
which is stored in state. But, if a seed is provided (e.g. t1, t2, t3,
... tn; or a custom one) then the ID
will be the result of concatenating the j_id prefix with the seed (e.g. j_idt1, j_idt2,
j_idt3
... j_idtn).
When the NamingContainer
is found, the corresponding createUniqueId() is called. Per example, if
the current component is a <h:inputText>, then most probably its
naming container will be a <h:form> (UIForm), so the createUniqueId()
method from UIForm will be called:
// Mojarra
2.2.9 source code, class UIForm
public
String createUniqueId(FacesContext context, String seed) {
if (isPrependId()) {
Integer i = (Integer)
getStateHelper().get(PropertyKeys.lastId);
int lastId = ((i != null) ? i : 0);
getStateHelper().put(PropertyKeys.lastId, ++lastId);
return UIViewRoot.UNIQUE_ID_PREFIX + (seed
== null ? lastId : seed);
} else {
UIComponent ancestorNamingContainer =
(getParent() == null) ? null : getParent().getNamingContainer();
String uid = null;
if (null != ancestorNamingContainer
&&
ancestorNamingContainer instanceof
UniqueIdVendor) {
uid = ((UniqueIdVendor)
ancestorNamingContainer).createUniqueId(context, seed);
} else {
uid =
context.getViewRoot().createUniqueId(context, seed);
}
return uid;
}
}
Obviously,
in this case, the creation of ID revolves around the value of prependId
attribute. By default, the value of this attribute is true, which means that
this form should prepend its ID to its descendent's ID during the clientId generation process. But, if
this value is false,
then JSF uses the technique presented earlier - it delegates the task to the
closest naming container (the parent naming container of this UIForm)
or to the view root. Now, you know how prependId affects the generated IDs
(sometimes you will need to set the prependId value to false
- e.g. in login forms that uses fix IDs, like
j_username
and j_password).
Finally,
let's focus on JSF data tables. The HtmlDataTable (extends UIData)
is a NamingContainer.
The columns of a data table (UIColumn) are not naming container, so their
IDs are computed via createUniqueId() from UIData class, since this is
the closest naming container (a similar implementation is found in UINamingContainer
also):
// Mojarra
2.2.9 source code, class UIData
public
String createUniqueId(FacesContext context, String seed) {
Integer i = (Integer)
getStateHelper().get(PropertyKeys.lastId);
int lastId = ((i != null) ? i : 0);
getStateHelper().put(PropertyKeys.lastId, ++lastId);
return UIViewRoot.UNIQUE_ID_PREFIX + (seed == null ? lastId : seed);
}
Notice that
even when a seed is provided, the lastId is incremented and stored in
state.
Now, we know
how the IDs are generated and set using the setId() for each component
(the getId()
returns the ID). But, when we look at the page markup, we see the clientIds, instead of IDs. Well, the clientId is like a "path"
composed of individual IDs separated by the char separator (e.g. colon, ":"
- this is returned by, UINamingContainer.getSeparatorChar(javax.faces.context.FacesContext)
and can be altered in JSF via the context parameter, javax.faces.SEPARATOR_CHAR
- you may want to do this to avoid conflicts with jQuery, which uses the colon
for other purposes). The clientId is
obtained from different implementations of UIComponent.getClientId(FacesContext)
method, like UIComponentBase.getClientId(FacesContext)
method and UIData.getClientId(FacesContext)
method - the later is responsible to add the row index in clientId (e.g. j_idt13:0:j_idt22).
Note The
container can also add a namespace to the clientId. Per example, this happens in portlet views, where
multiple views may be rendered to a single HTML page.
Note
When UIComponentBase.getClientId(FacesContext)
cannot find a generated ID, it will instruct JSF to create one via createUniqueId()
of the closest naming container or of the view root.
In addition,
"special" IDs, like view state IDs (e.g. j_id1:javax.faces.ViewState:0)
or client window IDs (e.g. j_id1:javax.faces.ClientWindow:0) are
computed via two methods from com.sun.faces.util.Util class, listed below:
// view
state IDs
// Mojarra
2.2.9 source code, class Util
public
static String getViewStateId(FacesContext context) {
String result = null;
final String viewStateCounterKey =
"com.sun.faces.util.ViewStateCounterKey";
Map<Object, Object> contextAttrs =
context.getAttributes();
Integer counter = (Integer)
contextAttrs.get(viewStateCounterKey);
if (null == counter) {
counter = Integer.valueOf(0);
}
char sep =
UINamingContainer.getSeparatorChar(context);
UIViewRoot root = context.getViewRoot();
result = root.getContainerClientId(context) +
sep +
ResponseStateManager.VIEW_STATE_PARAM
+ sep + counter;
contextAttrs.put(viewStateCounterKey,
++counter);
return result;
}
// client
window IDs
// Mojarra
2.2.9 source code, class Util
public
static String getClientWindowId(FacesContext context) {
String result = null;
final String clientWindowIdCounterKey =
"com.sun.faces.util.ClientWindowCounterKey";
Map<Object, Object> contextAttrs =
context.getAttributes();
Integer counter = (Integer)
contextAttrs.get(clientWindowIdCounterKey);
if (null == counter) {
counter = Integer.valueOf(0);
}
char sep =
UINamingContainer.getSeparatorChar(context);
result =
context.getViewRoot().getContainerClientId(context) + sep +
ResponseStateManager.CLIENT_WINDOW_PARAM + sep + counter;
contextAttrs.put(clientWindowIdCounterKey,
++counter);
return result;
}
So, let's
have a JSF page, and let's see how the IDs are generated:
<?xml
version='1.0' encoding='UTF-8' ?>
<!DOCTYPE
html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
<title></title>
</h:head>
<h:body>
<h:panelGroup
layout="block">--- ATP START ---</h:panelGroup>
<hr/>
<h:panelGrid columns="1">
<f:facet name="header">
Singles Today
</f:facet>
<h:form>
Max Rank: <h:inputText
value="#{playersBean.max}"/>
<h:commandButton id="maxBtnId"
value="Load"/>
</h:form>
<h:dataTable
value="#{playersBean.data}" var="t"
border="1">
<h:column>
<f:facet name="header">
Ranking
</f:facet>
<h:panelGroup id="rankingId"
layout="block">#{t.ranking}</h:panelGroup>
</h:column>
<h:column>
<f:facet name="header">
Name
</f:facet>
<h:panelGroup
layout="block">#{t.player}</h:panelGroup>
</h:column>
<h:column>
<h:form>
<h:commandButton value="Delete"
action="#{playersBean.delete(t.ranking)}"/>
</h:form>
</h:column>
</h:dataTable>
</h:panelGrid>
<hr/>
<h:panelGroup
id="end" layout="block">--- ATP END
---</h:panelGroup>
</h:body>
</html>
In the below
figure you can see the generated IDs/clientIds
for this page as they are in the component tree:
Now, if we
project these IDs/clientIds on the
markup, we will obtain the below figure:
Done! You may be also interested in OmniFaces NoAutoGeneratedIdViewHandler.
Niciun comentariu :
Trimiteți un comentariu