2010/05/14

Two Ways for Partial Facelets Adoption

Summary

This article explains two ways to use JSP-based JSF and Facelets-based JSF in the same project, one of which is well known while the other is less known and is worth writing about.

Motivation and Prefix Mapping Technique

Say, you have hundreds of JSP-based JSF files and you want to switch to Facelets. Some people say it is fairly easy to manually conver them and it would take a day or two, but you don't want to risk. So you need to use JSP and Facelets in the same JSF project. A well known way to do this is to use prefix mapping technique as described here. With this technique, *.xhtml files will use Facelets while *.jsp files will use JSP. A drawback is that you must use prefix mapping, thus URLs will have to change. It is a pain in some cases, when your pages have many incoming links, for example.

Suffix Mapping Technique

Fortunately, there is another way that use suffix mapping. With this technique, URLs don't have to change for your old JSP-based JSF. This could be done with some sacrifice: Facelets-based JSF files must have jsp extention instead of xhtml. To distinguish JSP and Facelets, name Facelets files *_.jsp for example. Then all *_.jsp files will use Facelets while the other *.jsp files will use JSP. In some cases, this is an easier way to adopt Facelets partially. To use this technique, you just configure your web.xml as below:
web.xml
<web-app>
    <!-- Default suffix for jsf files -->
    <context-param>
        <param-name>javax.faces.DEFAULT_SUFFIX</param-name>
        <param-value>.jsp</param-value>
    </context-param>

    <!--
    Only jsf files with _.jsp suffix are processed with Facelets.
    Other .jsp files are processed as JSP-based JSF.
    -->
    <context-param>
        <param-name>facelets.VIEW_MAPPINGS</param-name>
        <param-value>*_.jsp</param-value>
    </context-param>

    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    </servlet>

    <!-- Url with .jsf suffix are passed to facesServlet -->
    <servlet-mapping>
        <servlet-name>facesServlet</servlet-name>
        <url-pattern>*.jsf</url-pattern>
    </servlet-mapping>
</web-app>
URL with .jsf extention are handled by FacesServlet. A suffix ".jsp" is used to determine the JSF view file, as specified by javax.faces.DEFAULT_SUFFIX param. If the view file ends with "_.jsp" then Facelets processes the view file, otherwise the view processing is deligated to the default JSP engine.
posted by apptaro at 17:30 | Comment(0) | TrackBack(0) | JSF

2008/10/10

Corrupted Characters in JSF due to Expired Session

Summary

When a session is expired, JSF may parse form data using incorrect character encoding. This leads to corrupted characters on a page, or in a worst case, corrupted data in a database.

How form data can be corrupted

As I wrote in the previous article on some tips for JSF character encoding, JSF mostly depends on javax.faces.request.charset session attribute to determine a correct character encoding to decode form data. JSF 1.1 and 1.2 are designed this way so that you can use existing mechanism such as JSP page directive or servlet filter to change character encoding. I think people usually just set charset in JSP page directive. Then, JSF seems to take care of the rest.

My application uses client side state saving, and view-scoped beans (using t:saveState for request-scoped managed beans.) This is the best way because this way I can let users use multiple browser windows to access the same JSF form at once. If you use session-scoped beans, beans are shared between accesses from different browser windows, and it messes things up. I thought my application does not depend on sessions in any way, so I set short session-timeout. But there was a pitfall!

Users sometimes leave JSF page open, and later come back and do updates on the page. The session has been expired by that time, and JSF falls back to use ISO-8859-1 in decoding form data, when it should be using, for example, Windows-31J for Japanese characters. Corrupted data are then stored in a backing bean, leading to corrupted characters on the response page, or corrupted data can even be saved on a database.

So what to do?

So what to do to avoid this problem? You could ...
  • Set long session-timeout

    I don't like this idea because I wanted to set it short for the first place for some reasons.

  • Set up a servlet filter to set request encoding

    I feel it waste because then JSF spec would look meaningless, and if you changed charset in JSP page directive, you would also have to modify the filter. If they don't match, it will messes things up. A servlet filter sets a request character encoding first, then JSF sets a request character encoding when found in a session, then actual encoding used to decode the form data may be the former or the latter. (Note: The servlet will use the last request character encoding set to parse form data, on an initial call to request.getParameter.)

  • Add a logic to disallow form data if a session is null or new

    This would make it worse in terms of application usability. What would the error message say? "You were away too long, so I forgot the character encoding?"

I wonder if there are other ways to solve this issue. For now, I would go with the second option.
posted by apptaro at 13:52 | Comment(0) | TrackBack(0) | JSF

2007/05/11

Tips for JSF character encoding

Summary

This article explains some tips for JSF character encoding.

How request character encoding is determined by JSF.

As described in section 2.5.2.2 of JSF specification 1.1 and 1.2, request character encoding is determined in the following order:
  • Charset of Content-Type is used, if exists.
  • Charset of a previous response is used, if stored in a session.
  • Otherwise, request character encoding is unmodified.

Determining correct encoding is crutial because it is used to decode POST parameters.

Obtaining charset from Content-Type is considered the best way, but this usually does not happen because most browsers currently do not send it.

JSF stores previous response character encoding in a session ("javax.faces.request.charset" in case of MyFaces.) Most browsers send requests using the same character encoding as the HTML, so following requests to JSF can assumingly use previous response character encoding as request character encoding.

On an initial request of a session to JSF, whatever request character encoding set by servlet/container is used unmodified. Thus it can fall back to ISO-8859-1 as specified by Servlet Specification 2.4 and 2.5. A common way to specify request character encoding is to build a small servlet filter which call request.setCharacterEncoding.

Tips

Here's the tips:
  • Always use servlet filter to set request character encoding. Otherwise, an initial request to JSF will fail to decode parameters correctly.

    If you have a JSF page which uses incoming POST parameters, and a calling page is a simple html page with a form, chances are JSF will fail to process the parameter correctly for the first request, but will succeed for the following requests.

  • Always use only one encoding throughout the application. Otherwise, JSF may fail to decode parameters correctly.

    Note that response encoding is stored by session, not by session and page. If some JSF pages renders response by encoding A, while others renders response by encoding B, accessing the former pages after accessing the latter pages will result in decode failure. This may also happen if response encoding is changed dynamically based on user's preferences, and multiple windows are open while changing the preferences.

Notes

  • Even if you set a character encoding in a servlet filter, JSF will override it with the encoding found in Content-Type or the previous response encoding stored in a session. The servlet will use the last request character encoding set to parse form data, on an initial call to request.getParameter. (Modifying q request character encoding after a call to request.getParameter is ignored.) (Added: 2008/10/10)
posted by apptaro at 17:06 | Comment(2) | TrackBack(0) | JSF

2007/04/04

MyFaces Tomahawk: Tree2 Component Improvement

Summary

MyFaces Tomahawk's Tree2 is a great JSF component to display hierarchic data, such as directory structure or sitemap. When used with dynamic data, however, its default implementation has some issues with tree state as described in TOMAHAWK-244 and TOMAHAWK-296. In this article, I will solve the issues by providing custom implementations of TreeModel and TreeWalker interfaces.

Scope

This article focuses on usage of tree2 component with the following attributes:
  • clientSideToggle="true"
  • preserveToggle="true"
Tested with MyFaces Core 1.1.3 and MyFaces Tomahawk 1.1.3. For server side toggle, no test has been done.

Problem Description

Tree2 component stores tree state (if each node is expanded or collapsed) in index-based node naming schema such that:
  • root node is named "0"
  • first child is named "0:0"
  • second child is named "0:1"
  • third child of second child is named "0:1:2"
This simple schema does not work as expected when tree data is dynamic. For example, in the diagram below, a root node has two nodes A and B, and node B is expanded. This tree state is described as "node 0:1 is expanded."
  • ROOT
  • + A
  • - B
  • + B1
  • + B2
Let's look at the situation when node C is inserted as a first child of ROOT. Then what you would expect is:
  • ROOT
  • + C
  • + A
  • - B
  • + B1
  • + B2
However, this is what really happens. Because tree state is stored as "node 0:1 is expaned," in this case what is expanded is node A:
  • ROOT
  • + C
  • - A
  • + A1
  • + A2
  • + B
Because tree state is index-based, when data is inserted/deleted/moved/sorted, tree state becomes unfavorable.

Solution Description

Good news is, Tree2 component has a mechanism to solve this problem by supplying custom naming schema implementation. Bad news is, there is no working example or good documentation on the Internet (as far as I know.) So, I have done some investigation and created one!
Idea is to use naming schema based on node identifier, as suggested in TOMAHAWK-244. In the above example, "node 0:1 is expanded" is instead described as "node ROOT:B is expanded" or "node 0:200 is expanded" if ROOT's identifier is 0 and B's identifier is 200 (I think this is typical case where record ids are integers generated by database sequence.)

Implementation

The following two files are the implementation of the solution.
  • CustomTreeModel.java
  • CustomTreeWalker.java
Naming schema can be customized by implementating TreeWalker interface, but besides that, a custom implementation of TreeModel is needed.
The code uses Java5 generics syntax, but you can remove them if you are using Java1.4.
CustomTreeModel.java
package example;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;

import org.apache.myfaces.custom.tree2.TreeModel;
import org.apache.myfaces.custom.tree2.TreeNode;
import org.apache.myfaces.custom.tree2.TreeState;
import org.apache.myfaces.custom.tree2.TreeStateBase;
import org.apache.myfaces.custom.tree2.TreeWalker;

public class CustomTreeModel implements TreeModel {

    private TreeNode rootTreeNode;
    private TreeState treeState;

    public CustomTreeModel(TreeNode rootTreeNode) {
        this.rootTreeNode = rootTreeNode;
        this.treeState = new TreeStateBase();
    }

    //

    public String[] getPathInformation(String nodeId) {
        if (nodeId == null) {
            throw new IllegalArgumentException("Cannot determine path for a null node.");
        }

        ArrayList pathList = new ArrayList();
        pathList.add(nodeId);
        
        while (nodeId.lastIndexOf(SEPARATOR) != -1) {
            nodeId = nodeId.substring(0, nodeId.lastIndexOf(SEPARATOR));
            pathList.add(nodeId);
        }

        Collections.reverse(pathList);

        return pathList.toArray(new String[0]);
    }

    public boolean isLastChild(String nodeId) {
        if (nodeId.lastIndexOf(SEPARATOR) == -1) { // return true for root node
            return true;
        }

        String parentNodeId = nodeId.substring(0, nodeId.lastIndexOf(SEPARATOR));
        String childIdentifier = nodeId.substring(nodeId.lastIndexOf(SEPARATOR) + 1);

        TreeNode parentNode = getNodeById(parentNodeId);
        TreeNode lastChildNode = (TreeNode)parentNode.getChildren().get(parentNode.getChildCount() - 1);

        return lastChildNode.getIdentifier().equals(childIdentifier);
    }

    public TreeNode getNodeById(String nodeId) {
        if (nodeId == null) return null;

        StringTokenizer st = new StringTokenizer(nodeId, SEPARATOR);

        // check first node identifier equals root node identifier
        if (!rootTreeNode.getIdentifier().equals(st.nextElement())) {
            throw new IllegalArgumentException("Cannot find a root node.");
        }
        
        TreeNode node = rootTreeNode;
        while (st.hasMoreTokens()) {
            String nodeIdentifier = st.nextToken();
            boolean found = false;
            for (TreeNode childNode : (List)node.getChildren()) {
                if (childNode.getIdentifier().equals(nodeIdentifier)) {
                    node = childNode;
                    found = true;
                    break;
                }
            }
            if (!found) {
                throw new IllegalArgumentException("Cannot find a node. (" + nodeId + ")");
            }
        }

        return node;
    }

    public void setTreeState(TreeState treeState) {
        this.treeState = treeState;
    }

    public TreeState getTreeState() {
        return treeState;
    }

    public TreeWalker getTreeWalker() {
        return new CustomTreeWalker();
    }

    //
    
    public TreeNode getRootTreeNode() {
        return rootTreeNode;
    }

}
To use a custom TreeWalker implementation, getTreeWalker() method of TreeModel must be implemented to return it. I first tried just to override getTreeWalker() method of TreeModelBase (the default implementation of TreeModel,) but I found it not possible because part of the code depends of index-based naming schema. So I created a new CustomTreeModel class. getRootTreeNode() is added so that CustomTreeWalker can get access to TreeNode's.
CustomTreeWalker.java
package example;

import java.util.List;
import java.util.Stack;

import org.apache.commons.lang.StringUtils;
import org.apache.myfaces.custom.tree2.TreeModel;
import org.apache.myfaces.custom.tree2.TreeNode;
import org.apache.myfaces.custom.tree2.TreeWalker;
import org.apache.myfaces.custom.tree2.UITreeData;

public class CustomTreeWalker implements TreeWalker {

    private static final String SEPARATOR = TreeModel.SEPARATOR;

    private UITreeData treeData;
    private Stack idStack = new Stack();
    private Stack nodeStack = new Stack();
    private boolean checkState = true;
    private boolean startedWalking = false;

    //

    public boolean isCheckState() {
        return checkState;
    }

    public void setCheckState(boolean checkState) {
        this.checkState = checkState;
    }

    public boolean next() {
        if (!startedWalking) { // first step of walking : start from the root
            CustomTreeModel treeModel = (CustomTreeModel)treeData.getDataModel();
            TreeNode rootTreeNode = treeModel.getRootTreeNode();
            treeData.setNodeId(rootTreeNode.getIdentifier());
            idStack.push(rootTreeNode.getIdentifier());
            nodeStack.push(rootTreeNode);

            startedWalking = true;
            return true;
        }

        if (nodeStack.isEmpty()) { // if stack is empty, end the walking
            return false;
        }

        String prevNodeId = idStack.peek();
        TreeNode prevNode = nodeStack.peek();

        if (prevNode.isLeaf()) { // if previous stacked node is a leaf, go up the hierarchy
            idStack.pop();
            nodeStack.pop();
            return next();

        } else {

            String nextNodeId = null;
            TreeNode nextNode = null;

            if (prevNodeId.equals(treeData.getNodeId())) { // going down the hierarchy

                if (checkState) { // if checkState is true, check if the node is expanded and skip it if not.
                    if (!treeData.getDataModel().getTreeState().isNodeExpanded(prevNodeId)) {
                        idStack.pop();
                        nodeStack.pop();
                        return next();
                    }
                }

                nextNode = (TreeNode)prevNode.getChildren().get(0);
                nextNodeId = prevNodeId + SEPARATOR + nextNode.getIdentifier();

            } else { // walk on the same level

                String currentNodeId = treeData.getNodeId();
                String currentNodeIdentifier = treeData.getNode().getIdentifier();
                String parentNodeId = StringUtils.substringBeforeLast(currentNodeId, SEPARATOR);
                TreeNode parentNode = treeData.getDataModel().getNodeById(parentNodeId);

                if (treeData.getDataModel().isLastChild(currentNodeId)) { // go up the hierarchy if last child
                    treeData.setNodeId(parentNodeId);
                    idStack.pop();
                    nodeStack.pop();
                    return next();
                }

                boolean nextIsNext = false;
                for (TreeNode childNode : (List)parentNode.getChildren()) { // find next child
                    if (nextIsNext) {
                        nextNode = childNode;
                        nextNodeId = parentNodeId + SEPARATOR + nextNode.getIdentifier();
                        break;
                    } else if (childNode.getIdentifier().equals(currentNodeIdentifier)) {
                        nextIsNext = true;
                    }
                }

            }

            treeData.setNodeId(nextNodeId);
            idStack.push(nextNodeId);
            nodeStack.push(nextNode);

            return true;
        }
    }

    public String getRootNodeId() {
        return ((CustomTreeModel)treeData.getDataModel()).getRootTreeNode().getIdentifier();
    }

    public void setTree(UITreeData treeData) {
        this.treeData = treeData;
    }

    public void reset() {
        idStack.empty();
        nodeStack.empty();
        startedWalking = false;
    }

}
CustomTreeWalker implementation uses naming schema based on node identifier, separated by colon(:) so it assumes that node identifier does not include any colon. next() method is the main method which walks through the tree nodes the same way as the default TreeWalkerBase does.

Download

No download available, so just copy the code above.

Copyright?

Please use them for free!
posted by apptaro at 11:58 | Comment(3) | TrackBack(0) | JSF
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。