com.icesoft.faces.context.DOMResponseWriter.java Source code

Java tutorial

Introduction

Here is the source code for com.icesoft.faces.context.DOMResponseWriter.java

Source

/*
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * "The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License.
 *
 * The Original Code is ICEfaces 1.5 open source software code, released
 * November 5, 2006. The Initial Developer of the Original Code is ICEsoft
 * Technologies Canada, Corp. Portions created by ICEsoft are Copyright (C)
 * 2004-2006 ICEsoft Technologies Canada, Corp. All Rights Reserved.
 *
 * Contributor(s): _____________________.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"
 * License), in which case the provisions of the LGPL License are
 * applicable instead of those above. If you wish to allow use of your
 * version of this file only under the terms of the LGPL License and not to
 * allow others to use your version of this file under the MPL, indicate
 * your decision by deleting the provisions above and replace them with
 * the notice and other provisions required by the LGPL License. If you do
 * not delete the provisions above, a recipient may use your version of
 * this file under either the MPL or the LGPL License."
 *
 */

package com.icesoft.faces.context;

import com.icesoft.faces.application.StartupTime;
import com.icesoft.faces.context.effects.JavascriptContext;
import com.icesoft.faces.util.CoreUtils;
import com.icesoft.faces.util.DOMUtils;
import com.icesoft.faces.webapp.http.common.Configuration;
import com.icesoft.faces.webapp.http.common.ConfigurationException;
import com.icesoft.faces.webapp.xmlhttp.PersistentFacesState;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.faces.FacesException;
import javax.faces.application.ViewHandler;
import javax.faces.component.UIComponent;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListResourceBundle;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

/**
 * <p><strong>DOMResponseWriter</strong> is a DOM specific implementation of
 * <code>javax.faces.context.ResponseWriter</code>.
 */
public class DOMResponseWriter extends ResponseWriter {
    private static final Log log = LogFactory.getLog(DOMResponseWriter.class);
    public static final String DOCTYPE_PUBLIC = "com.icesoft.doctype.public";
    public static final String DOCTYPE_SYSTEM = "com.icesoft.doctype.system";
    public static final String DOCTYPE_ROOT = "com.icesoft.doctype.root";
    public static final String DOCTYPE_OUTPUT = "com.icesoft.doctype.output";
    public static final String DOCTYPE_PRETTY_PRINTING = "com.icesoft.doctype.prettyprinting";

    private List markerNodes = new ArrayList();

    private static final BundleResolver bridgeMessageResolver = new FailoverBundleResolver("bridge-messages",
            new ListResourceBundle() {
                protected Object[][] getContents() {
                    return new Object[][] { { "session-expired", "User Session Expired" },
                            { "connection-lost", "Network Connection Interrupted" },
                            { "server-error", "Server Internal Error" },
                            { "description",
                                    "To reconnect click the Reload button on the browser or click the button below" },
                            { "button-text", "Reload" } };
                }
            });
    static DocumentBuilder DOCUMENT_BUILDER;

    static {
        try {
            DOCUMENT_BUILDER = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            log.error("Cannot acquire a DocumentBuilder", e);
        }
    }

    private static boolean isStreamWritingFlag = false;
    private static boolean isDOMChecking = true;
    private final Map domContexts = new HashMap();
    private Document document;

    {
        document = DOCUMENT_BUILDER.newDocument();
        if (!isDOMChecking) {
            Method setErrorCheckingMethod = null;
            try {
                setErrorCheckingMethod = document.getClass().getMethod("setErrorChecking",
                        new Class[] { boolean.class });
                setErrorCheckingMethod.invoke(document, new Object[] { Boolean.FALSE });
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug("DOM error checking not disabled ", e);
                }
            }
        }
    }

    private final BridgeFacesContext context;
    private final DOMSerializer serializer;
    private final Configuration configuration;
    private final Collection jsCode;
    private final Collection cssCode;
    private String blockingRequestHandlerContext;
    private Node cursor = document;
    private Node savedJSFStateCursor;
    private boolean checkJavaScript;

    public DOMResponseWriter(final FacesContext context, final DOMSerializer serializer,
            final Configuration configuration, final Collection jsCode, final Collection cssCode,
            final String blockingRequestHandlerContext) {
        this.serializer = serializer;
        this.configuration = configuration;
        this.jsCode = jsCode;
        this.cssCode = cssCode;
        this.blockingRequestHandlerContext = blockingRequestHandlerContext;
        this.checkJavaScript = configuration.getAttributeAsBoolean("checkJavaScript", true);
        try {
            this.context = (BridgeFacesContext) context;
        } catch (ClassCastException e) {
            throw new IllegalStateException("ICEfaces requires the PersistentFacesServlet. "
                    + "Please check your web.xml servlet mappings");
        }
    }

    Map getDomContexts() {
        return domContexts;
    }

    public Node getCursorParent() {
        return cursor;
    }

    public Document getDocument() {
        return document;
    }

    public static void setDOMErrorChecking(boolean flag) {
        isDOMChecking = flag;
    }

    public String getContentType() {
        return "text/html; charset=UTF-8";
    }

    public String getCharacterEncoding() {
        return "UTF-8";
    }

    public void startDocument() throws IOException {
    }

    public void endDocument() throws IOException {
        domContexts.clear();
        if (!isStreamWriting()) {
            enhanceAndFixDocument();
            serializer.serialize(document);
        }
        document = null;
        cursor = null;
        savedJSFStateCursor = null;
    }

    public void flush() throws IOException {
    }

    public void startElement(String name, UIComponent componentForElement) throws IOException {
        moveCursorOn(appendToCursor(document.createElement(name)));
    }

    public void endElement(String name) throws IOException {
        moveCursorOn(cursor.getParentNode());
    }

    public void writeAttribute(String name, Object value, String componentPropertyName) throws IOException {
        //name.trim() because cardemo had a leading space in an attribute name
        //which made the DOM processor choke
        Attr attribute = document.createAttribute(name.trim());
        attribute.setValue(String.valueOf(value));
        appendToCursor(attribute);
    }

    public void writeURIAttribute(String name, Object value, String componentPropertyName) throws IOException {
        String stringValue = String.valueOf(value);
        if (stringValue.startsWith("javascript:")) {
            writeAttribute(name, stringValue, componentPropertyName);
        } else {
            writeAttribute(name, stringValue.replace(' ', '+'), componentPropertyName);
        }
    }

    public void writeComment(Object comment) throws IOException {
        appendToCursor(document.createComment(String.valueOf(comment)));
    }

    public void writeText(Object text, String componentPropertyName) throws IOException {
        if ("".equals(text)) {
            return;
        }
        appendToCursor(document.createTextNode(String.valueOf(text)));
    }

    public void writeText(char text[], int off, int len) throws IOException {
        if (0 == len) {
            return;
        }
        appendToCursor(document.createTextNode(new String(text, off, len)));
    }

    public ResponseWriter cloneWithWriter(Writer writer) {
        //FIXME: This is a hack for DOM rendering but JSF currently clones the writer
        //just as the components are complete
        if (null != document) {
            try {
                endDocument();
            } catch (IOException e) {
                throw new IllegalStateException(e.toString());
            }
        }
        try {
            return new DOMResponseWriter(context, serializer, configuration, jsCode, cssCode,
                    blockingRequestHandlerContext);
        } catch (FacesException e) {
            throw new IllegalStateException();
        }
    }

    public void close() throws IOException {
    }

    public void write(char[] cbuf, int off, int len) throws IOException {
        if (0 == len) {
            return;
        }
        appendToCursor(document.createTextNode(new String(cbuf, off, len)));
    }

    public void write(int c) throws IOException {
        appendToCursor(document.createTextNode(String.valueOf((char) c)));
    }

    public void write(String str) throws IOException {
        if ("".equals(str)) {
            return;
        }
        appendToCursor(document.createTextNode(str));
    }

    public void write(String str, int off, int len) throws IOException {
        if (0 == len) {
            return;
        }
        appendToCursor(document.createTextNode(str.substring(off, len)));
    }

    public Element getHtmlElement() {
        Element html = document.getDocumentElement();
        if (html == null) {
            html = document.createElement("html");
            document.appendChild(html);
        }
        if (html.getTagName().equals("html"))
            return html;
        return fixHtml();
    }

    public Element getHeadElement() {
        Element head = (Element) document.getElementsByTagName("head").item(0);
        if (head == null)
            head = fixHead();
        return head;
    }

    public Element getBodyElement() {
        Element body = (Element) document.getElementsByTagName("body").item(0);
        if (body == null)
            body = fixBody();
        return body;
    }

    private void enhanceAndFixDocument() {
        Element html = (Element) document.getDocumentElement();
        enhanceHtml(html = "html".equals(html.getTagName()) ? html : fixHtml());

        Element head = (Element) document.getElementsByTagName("head").item(0);
        enhanceHead(head == null ? fixHead() : head);

        Element body = (Element) document.getElementsByTagName("body").item(0);
        enhanceBody(body == null ? fixBody() : body);
    }

    private void enhanceHtml(Element html) {
        //add lang attribute
        Locale locale = context.getViewRoot().getLocale();
        //id required for forwarded (server-side) redirects
        html.setAttribute("id", "document:html");
        html.setAttribute("lang", locale.getLanguage());
    }

    private void enhanceBody(Element body) {
        //id required for forwarded (server-side) redirects
        body.setAttribute("id", "document:body");

        // TODO This is only meant to be a transitional focus retention(management) solution.
        String focusId = context.getFocusId();
        if (focusId != null && !focusId.equals("null")) {
            JavascriptContext.focus(context, focusId);
        }
        ExternalContext externalContext = context.getExternalContext();
        ViewHandler handler = context.getApplication().getViewHandler();
        String sessionIdentifier = context.getIceFacesId();
        String viewIdentifier = context.getViewNumber();
        String prefix = sessionIdentifier + ':' + viewIdentifier + ':';

        Element script = (Element) body.appendChild(document.createElement("script"));
        script.setAttribute("id", prefix + "dynamic-code");
        script.setAttribute("type", "text/javascript");
        String calls = JavascriptContext.getJavascriptCalls(context);
        script.appendChild(document.createTextNode(calls));

        String contextPath = handler.getResourceURL(context, "/");
        if (blockingRequestHandlerContext == null) {
            //ICE-3784
            //Only need to strip leading and trailing slashes. Removing all slashes
            //will cause problems with contexts that actually contain slashes e.g /myapp/mysubapp.
            String normalizedPath = contextPath;
            if (normalizedPath.length() > 0) {
                if (normalizedPath.charAt(0) == '/') {
                    normalizedPath = normalizedPath.substring(1);
                }
                if (normalizedPath.length() > 0) {
                    if (normalizedPath.charAt(normalizedPath.length() - 1) == '/') {
                        normalizedPath = normalizedPath.substring(0, normalizedPath.length() - 1);
                    }
                }
            }
            blockingRequestHandlerContext = URI.create("/").resolve(normalizedPath + "/").toString();
        }
        String connectionLostRedirectURI;
        try {
            String uri = configuration.getAttribute("connectionLostRedirectURI");
            connectionLostRedirectURI = "'" + handler.getResourceURL(context, uri.replaceAll("'", "")) + "'";
        } catch (ConfigurationException e) {
            connectionLostRedirectURI = "null";
        }
        String sessionExpiredRedirectURI;
        try {
            String uri = configuration.getAttribute("sessionExpiredRedirectURI");
            sessionExpiredRedirectURI = "'" + handler.getResourceURL(context, uri.replaceAll("'", "")) + "'";
        } catch (ConfigurationException e) {
            sessionExpiredRedirectURI = "null";
        }
        String configurationID = prefix + "configuration-script";
        //add viewIdentifier property to the container element ("body" for servlet env., any element for the portlet env.)
        ResourceBundle localizedBundle = bridgeMessageResolver.bundleFor(context.getViewRoot().getLocale());
        //todo: build startup script only once on aplication startup
        boolean synchronousMode = configuration.getAttributeAsBoolean("synchronousUpdate", false);
        String startupScript = "window.disposeViewsURI = '" + blockingRequestHandlerContext
                + "block/dispose-views';\n" + "var container = '" + configurationID + "'.asElement().parentNode;\n"
                + "container.bridge = new Ice.Community.Application({" + "blockUI: "
                + configuration.getAttribute("blockUIOnSubmit", "false") + "," + "session: '" + sessionIdentifier
                + "'," + "view: " + viewIdentifier + "," + "synchronous: " + synchronousMode + ","
                + "connectionLostRedirectURI: " + connectionLostRedirectURI + "," + "sessionExpiredRedirectURI: "
                + sessionExpiredRedirectURI + "," + "serverErrorRetryTimeouts: [" + configuration
                        .getAttribute("serverErrorRetryTimeouts", "1000 2000 4000").trim().replaceAll("\\s+", ",")
                + "], " + "connection: {" + "context: '" + contextPath + "', " + (synchronousMode ?
                //encode path for URL rewrite session tracking mode
                        ("sendReceiveUpdatesURI: '"
                                + externalContext.encodeResourceURL(contextPath + "block/send-receive-updates")
                                + "',")
                        : ("sendReceiveUpdatesURI: '" + contextPath + "block/send-receive-updates" + "',"
                                + "pingURI: '" + contextPath + "block/ping" + "'," + "receiveUpdatesURI: '"
                                + contextPath + "block/receive-updates" + "'," + "receiveUpdatedViewsURI: '"
                                + blockingRequestHandlerContext + "block/receive-updated-views" + "',")
                                + "heartbeat: {" + "interval: "
                                + configuration.getAttributeAsLong("heartbeatInterval", 50000) + "," + "timeout: "
                                + configuration.getAttributeAsLong("heartbeatTimeout", 30000) + "," + "retries: "
                                + configuration.getAttributeAsLong("heartbeatRetries", 3) + "},")
                + "timeout: " + configuration.getAttributeAsLong("connectionTimeout", 60000) + "}," + "messages: {"
                + "sessionExpired: '" + localizedBundle.getString("session-expired") + "'," + "connectionLost: '"
                + localizedBundle.getString("connection-lost") + "'," + "serverError: '"
                + localizedBundle.getString("server-error") + "'," + "description: '"
                + localizedBundle.getString("description") + "'," + "buttonText: '"
                + localizedBundle.getString("button-text") + "'" + "}" + "}, container);";

        Element configurationElement = (Element) body.appendChild(document.createElement("script"));
        configurationElement.setAttribute("id", configurationID);
        configurationElement.setAttribute("type", "text/javascript");
        configurationElement.appendChild(document.createTextNode(startupScript));
        body.insertBefore(configurationElement, body.getFirstChild());

        Element iframe = document.createElement("iframe");
        body.insertBefore(iframe, body.getFirstChild());
        String iframeID = "history-frame:" + sessionIdentifier + ":" + viewIdentifier;
        iframe.setAttribute("id", iframeID);
        iframe.setAttribute("name", iframeID);
        Object request = externalContext.getRequest();

        final String frameURI;
        //another "workaround" to resolve the iframe URI
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            if (httpRequest.getRequestURI() == null) {
                frameURI = "about:blank";
            } else {
                frameURI = CoreUtils.resolveResourceURL(FacesContext.getCurrentInstance(), "/xmlhttp/blank");
            }
        } else {
            frameURI = CoreUtils.resolveResourceURL(FacesContext.getCurrentInstance(), "/xmlhttp/blank"); // ICE-2553
        }
        iframe.setAttribute("title", "Icefaces Redirect");
        iframe.setAttribute("src", frameURI);
        iframe.setAttribute("frameborder", "0");
        iframe.setAttribute("style",
                "z-index: 10000; visibility: hidden; width: 0; height: 0; position: absolute; opacity: 0.22; filter: alpha(opacity=22);");

        if (checkJavaScript) {
            Element noscript = (Element) body.appendChild(document.createElement("noscript"));
            Element noscriptMeta = (Element) noscript.appendChild(document.createElement("meta"));
            noscriptMeta.setAttribute("http-equiv", "refresh");
            noscriptMeta.setAttribute("content",
                    "0;url=" + handler.getResourceURL(context, "/xmlhttp/javascript-blocked"));
        }

        if (context.isContentIncluded()) {
            Element element = (Element) body.insertBefore(document.createElement("div"), configurationElement);
            element.setAttribute("style", "display: none;");
            //id added so the conditional rendering of the components under the portlet, 
            //won't send the body level update
            element.setAttribute("id", "cntIncDiv");
            appendContentReferences(element);
        }
    }

    private void enhanceHead(Element head) {
        Element meta = (Element) head.appendChild(document.createElement("meta"));
        meta.setAttribute("name", "icefaces");
        meta.setAttribute("content", "Rendered by ICEFaces D2D");

        //avoid reloading the head when only document's title is changed
        Element title = (Element) head.getElementsByTagName("title").item(0);
        if (title != null && !title.hasAttribute("id")) {
            title.setAttribute("id", "document:title");
        }

        if (!context.isContentIncluded()) {
            appendContentReferences(head);
        }
    }

    private void appendContentReferences(Element container) {
        //load libraries
        Collection libs = new ArrayList();
        if (configuration.getAttributeAsBoolean("openAjaxHub", false)) {
            libs.add("/xmlhttp/openajax.js");
        }
        libs.add("/xmlhttp" + StartupTime.getStartupInc() + "icefaces-d2d.js");
        //todo: refactor how external libraries are loaded into the bridge; always include extra libraries for now
        libs.add("/xmlhttp" + StartupTime.getStartupInc() + "ice-extras.js");

        String[] componentLibs = JavascriptContext.getIncludedLibs(context);
        for (int i = 0; i < componentLibs.length; i++) {
            String componentLib = componentLibs[i];
            if (!libs.contains(componentLib)) {
                libs.add(componentLib);
            }
        }

        libs.addAll(jsCode);

        ViewHandler handler = context.getApplication().getViewHandler();
        Iterator libIterator = libs.iterator();
        while (libIterator.hasNext()) {
            String lib = (String) libIterator.next();
            Element script = (Element) container.appendChild(document.createElement("script"));
            script.setAttribute("type", "text/javascript");
            script.setAttribute("src", handler.getResourceURL(context, lib));
        }

        Iterator cssIterator = cssCode.iterator();
        while (cssIterator.hasNext()) {
            String css = (String) cssIterator.next();
            Element link = (Element) container.appendChild(document.createElement("link"));
            link.setAttribute("rel", "stylesheet");
            link.setAttribute("type", "text/css");
            link.setAttribute("href", handler.getResourceURL(context, css));
        }

        //fix IE image caching bug -- see: http://www.mister-pixel.com/index.php?Content__state=whats_the_problem
        Element link = (Element) container.appendChild(document.createElement("script"));
        link.setAttribute("type", "text/javascript");
        link.appendChild(document
                .createTextNode("try { document.execCommand('BackgroundImageCache', false, true); } catch(e) {}"));
    }

    private Element fixHtml() {
        Element root = document.getDocumentElement();
        Element html = document.createElement("html");
        document.replaceChild(html, root);
        html.appendChild(root);

        return html;
    }

    private Element fixBody() {
        Element html = document.getDocumentElement();
        Element body = document.createElement("body");
        NodeList children = html.getChildNodes();
        int length = children.getLength();
        Node[] nodes = new Node[length];
        //copy the children first, since NodeList is live
        for (int i = 0; i < nodes.length; i++)
            nodes[i] = children.item(i);
        for (int i = 0; i < nodes.length; i++) {
            Node node = nodes[i];
            if (!(node instanceof Element && "head".equals(((Element) node).getTagName())))
                body.appendChild(node);
        }
        html.appendChild(body);

        return body;
    }

    private Element fixHead() {
        Element html = document.getDocumentElement();
        Element head = document.createElement("head");
        html.insertBefore(head, html.getFirstChild());

        return head;
    }

    /**
     * This method sets the write cursor for DOM modifications.  Subsequent DOM
     * modifications will take place below the cursor element.
     *
     * @param cursorParent parent node for subsequent modifications to the DOM
     */
    protected void setCursorParent(Node cursorParent) {
        this.cursor = cursorParent;
    }

    public static boolean isStreamWriting() {
        return isStreamWritingFlag;
    }

    private void moveCursorOn(Node node) {
        if (log.isTraceEnabled()) {
            log.trace("moving cursor on " + DOMUtils.toDebugString(node));
        }
        cursor = node;
    }

    private Node appendToCursor(Node node) {
        try {
            if (log.isTraceEnabled()) {
                log.trace("Appending " + DOMUtils.toDebugString(node) + " into " + DOMUtils.toDebugString(cursor));
            }
            return cursor.appendChild(node);

        } catch (DOMException e) {
            String message = "Failed to append " + DOMUtils.toDebugString(node) + " into "
                    + DOMUtils.toDebugString(cursor);
            log.error(message);
            throw new RuntimeException(message, e);
        }
    }

    private Node appendToCursor(Attr node) {
        try {
            if (log.isTraceEnabled()) {
                log.trace("Appending " + DOMUtils.toDebugString(node) + " into " + DOMUtils.toDebugString(cursor));
            }
            return ((Element) cursor).setAttributeNode(node);
        } catch (DOMException e) {
            String message = "Failed to append " + DOMUtils.toDebugString(node) + " into "
                    + DOMUtils.toDebugString(cursor);
            log.error(message);
            throw new RuntimeException(message, e);
        } catch (ClassCastException e) {
            String message = "The cursor is not an element: " + DOMUtils.toDebugString(cursor);
            log.error(message);
            throw new RuntimeException(message, e);
        }
    }

    /**
     * When JSF writes the state as a result of state saving, we can set aside
     * a <div> parent that containas all the nodes generated by that action. This should
     *
     * @param saveNextNodes true if the DOMResponseWriter is to capture the next
     *                      nodes written to a saved area, false if it is to turn this functionality off.
     */
    public void setSaveNextNode(boolean saveNextNodes) {

        // if we're turning off state saving, restore the original cursor
        if (saveNextNodes) {

            savedJSFStateCursor = cursor;

            // We have to store the state saving token nodes in a div (or other id'able nodes)
            // because you can't assign id's to #text nodes, and without an ID, the DOM diffing
            // breaks because it ascends the hierarchy until it finds a node that does have an Id
            // which is usually the containing form. This results in all the Forms children being
            // serialized.

            cursor = document.createElement("div");
            ((Element) cursor).setAttribute("id", "stateSavingDiv");

        } else {
            // swap cursor with savedNode
            Node n = cursor;
            cursor = savedJSFStateCursor;
            savedJSFStateCursor = n;
        }
    }

    /**
     * Retrieve the parent Node of the JSF state saving set of nodes. May be null
     * if the state saving method has not been turned on (and state written) in the
     * event that full JSF state saving is not configured.
     *
     * @return <div> node parent containing JSF state saving children
     */
    public Node getSavedNode() {
        return savedJSFStateCursor;
    }

    /**
     * Copy the one generated stateSaving branch into all the marker (one per form)
     * node areas. <p>
     * This method shouldn't be called if state saving is not enabled
     */
    public void copyStateNodesToMarkers() {

        Iterator i = markerNodes.iterator();
        while (i.hasNext()) {

            Node n = (Node) i.next();
            if ((n != null) && (savedJSFStateCursor != null)) {

                String nodeValue;

                // The View state consists of 4 sibling text nodes. We need to find the one
                // with the actual id, and preserve that ID in the PersistentFacesState object
                // so that server push operations can restore state as well.

                for (Node child = savedJSFStateCursor.getFirstChild(); child != null; child = child
                        .getNextSibling()) {

                    nodeValue = child.getNodeValue();
                    if (nodeValue != null && nodeValue.indexOf("j_id") > -1) {
                        PersistentFacesState.getInstance().setStateRestorationId(nodeValue);
                        if (log.isDebugEnabled()) {
                            log.debug("State id for server push state saving: " + nodeValue);
                        }
                    }
                    n.appendChild(child.cloneNode(true));
                }
            }
            //avoids unnecessary DOM diff due to normalization during
            //FastInfoset compression
            n.normalize();
        }
        markerNodes.clear();
    }

    /**
     * Keep the marker nodes (1 per form) in a structure so that at the end
     * of the lifecycle we can copy the state saving information into each
     * one.
     *
     * @param n Placeholding node inside the form to hold state saving info
     */
    public void trackMarkerNode(Node n) {
        markerNodes.add(n);
    }

    private void displayParent(Node n) {
        System.out.println("Node: " + n);
        Node parent = n.getParentNode();
        if (parent != null) {
            displayParent(parent);
        } else {
            System.out.println("-- done");
        }

    }
}