My JSF Books/Videos My JSF Tutorials OmniFaces/JSF PPTs
JSF 2.3 Tutorial
JSF Caching Tutorial
JSF Navigation Tutorial
JSF Scopes Tutorial
JSF Page Author Beginner's Guide
OmniFaces 2.3 Tutorial Examples
OmniFaces 2.2 Tutorial Examples
JSF Events Tutorial
OmniFaces Callbacks Usages
JSF State Tutorial
JSF and Design Patterns
JSF 2.3 New Features (2.3-m04)
Introduction to OmniFaces
25+ Reasons to use OmniFaces in JSF
OmniFaces Validators
OmniFaces Converters
JSF Design Patterns
Mastering OmniFaces
Reusable and less-verbose JSF code

My JSF Resources ...

Java EE Guardian
Member of JCG Program
Member MVB DZone
Blog curated on ZEEF
OmniFaces is an utility library for JSF, including PrimeFaces, RichFaces, ICEfaces ...

.

.

.

.

.

.

.

.


[OmniFaces Utilities] - Find the right JSF OmniFaces 2 utilities methods/functions

Search on blog

Petition by Java EE Guardians

Twitter

luni, 16 februarie 2015

JSF 2.2 auto-generated IDs/clientIds Demystified

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

JSF BOOKS COLLECTION

Postări populare

OmniFaces/JSF Fans

Visitors Starting 4 September 2015

Locations of Site Visitors