All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nextapp.echo.webcontainer.OutputProcessor Maven / Gradle / Ivy

/* 
 * This file is part of the Echo Web Application Framework (hereinafter "Echo").
 * Copyright (C) 2002-2009 NextApp, Inc.
 *
 * 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.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 */

package nextapp.echo.webcontainer;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Map.Entry;

import nextapp.echo.app.ApplicationInstance;
import nextapp.echo.app.Command;
import nextapp.echo.app.Component;
import nextapp.echo.app.ContentPane;
import nextapp.echo.app.Style;
import nextapp.echo.app.StyleSheet;
import nextapp.echo.app.Window;
import nextapp.echo.app.reflect.ComponentIntrospector;
import nextapp.echo.app.reflect.IntrospectorFactory;
import nextapp.echo.app.serial.PropertyPeerFactory;
import nextapp.echo.app.serial.SerialException;
import nextapp.echo.app.serial.SerialPropertyPeer;
import nextapp.echo.app.update.ServerComponentUpdate;
import nextapp.echo.app.update.ServerUpdateManager;
import nextapp.echo.app.util.Context;
import nextapp.echo.app.util.DomUtil;
import nextapp.echo.app.util.Log;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

/**
 * Generates an XML ServerMessage describing server-side changes to the
 * state of an application that is returned to the remote client as a response
 * to its synchronization HTTP connection.
 */
class OutputProcessor {
    
    /**
     * Returns the client locale string for the specified Locale object.
     * The client locale contains the language first, and if set, the country code with a preceding hyphen.
     * For example, US English would be rendered as "en-US".
     * 
     * @param locale the locale
     * @return the client-side locale string
     */
    public static String getClientLocaleString(Locale locale) {
        String language = locale.getLanguage();
        String country = locale.getCountry();
        if (country.length() == 0) {
            return language;
        } else {
            return language + "-" + country;
        }
    }
   
    /**
     * Context implementation.
     */
    private class OutputContext extends SynchronizationContext {
        
        /**
         * Creates a new OutputContext.
         */
        public OutputContext() {
            super(conn, document);
        }

        /**
         * @see nextapp.echo.app.util.Context#get(java.lang.Class)
         */
        public Object get(Class specificContextClass) {
            if (specificContextClass == ServerMessage.class) {
                return serverMessage;
            } else {
                return super.get(specificContextClass);
            }
        }
    }
    
    private Connection conn;
    private UserInstance userInstance;
    private ServerMessage serverMessage;
    private ServerUpdateManager serverUpdateManager;
    
    private Context context;
    private PropertyPeerFactory propertyPeerFactory;
    private Document document;
    private int nextPropertyKey = 0;
    private Map propertyValueToKeyMap = null;
    private int nextStyleKey = 0;
    private Map styleValueToKeyMap = null;
    private Element rpElement;
    private Element rsElement;
    private SynchronizationState syncState;
    
    private ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

    /**
     * Creates a new OutputProcessor.
     * 
     * @param syncState the SynchronizationState of the current synchronization
     * @param conn the Connection for which the output is being generated.
     */
    public OutputProcessor(SynchronizationState syncState, Connection conn) {
        super();
        this.syncState = syncState;
        this.conn = conn;
        serverMessage = new ServerMessage();
        document = serverMessage.getDocument();
        context = new OutputContext();
        userInstance = conn.getUserInstance();
        serverUpdateManager = userInstance.getUpdateManager().getServerUpdateManager();
        propertyPeerFactory = PropertySerialPeerFactory.forClassLoader(classLoader);
    }
        
    /**
     * Processes pending output from the application, generating a server message and rendering it
     * to the output PrintWriter of the Connection specified in the constructor.
     */
    public void process() 
    throws IOException {
        serverMessage.setUserInstanceId(userInstance.getId());
        serverMessage.setTransactionId(userInstance.getNextTransactionId());
        if (syncState.isOutOfSync()) {
            serverMessage.setResync();
        }
        
        try {
            // Render output to server message DOM.
            if (serverUpdateManager.isFullRefreshRequired()) {
                renderApplicationFull();
                renderComponentsFull();
            } else {
                renderApplicationIncremental();
                renderComponentsIncremental();
            }
            renderCommands();
            renderFocus();
            renderAsyncState();
        } catch (SerialException ex) {
            throw new SynchronizationException("Cannot serialize server state.", ex);
        }
        
        try {
            // Render DOM to PrintWriter.
            conn.setContentType(ContentType.TEXT_XML);
            DomUtil.save(serverMessage.getDocument(), conn.getWriter(), null);
        } catch (SAXException ex) {
            throw new SynchronizationException("Cannot serialize server state.", ex);
        }
        
        if (WebContainerServlet.DEBUG_PRINT_MESSAGES_TO_CONSOLE) {
            // Print ServerMessage DOM to console. 
            try {
                System.err.println("======== Response: " + userInstance.getCurrentTransactionId() + " ========");
                DomUtil.save(document, System.err, DomUtil.OUTPUT_PROPERTIES_INDENT);
                System.err.println();
            } catch (SAXException ex) {
                // Should not generally occur.
                throw new SynchronizationException("Cannot render XML sync message to console.", ex);
            }
        }
    }
    
    /**
     * Renders asynchronous callback settings to server message.
     */
    private void renderAsyncState() {
        if (userInstance.getApplicationInstance().hasTaskQueues()) {
            serverMessage.setAttribute("async-interval", Integer.toString(userInstance.getCallbackInterval()));
        }
    }
    
    /**
     * Renders full-refresh application-level properties.
     */
    private void renderApplicationFull() 
    throws SerialException {
        // Clear User Instance updated property names.
        userInstance.getUpdatedPropertyNames();
        
        Element localeElement = serverMessage.addDirective(ServerMessage.GROUP_ID_INIT, "AppSync", "locale");
        localeElement.appendChild(document.createTextNode(getClientLocaleString(
                userInstance.getApplicationInstance().getLocale())));
        Element layoutDirElement = serverMessage.addDirective(ServerMessage.GROUP_ID_INIT, "AppSync", "dir");
        layoutDirElement.appendChild(document.createTextNode(
                userInstance.getApplicationInstance().getLayoutDirection().isLeftToRight() ? "ltr" : "rtl"));
        renderClientConfiguration();
    }
    
    /**
     * Renders incrementally updated application instance properties.
     */
    private void renderApplicationIncremental() 
    throws SerialException {
        Iterator it = userInstance.getUpdatedPropertyNames();
        while (it.hasNext()) {
            String propertyName = (String) it.next();
            if (UserInstance.PROPERTY_CLIENT_CONFIGURATION.equals(propertyName)) {
                renderClientConfiguration();
            } else if (ApplicationInstance.STYLE_SHEET_CHANGED_PROPERTY.equals(propertyName)) {
                renderStyleSheet();
            }
        }
    }
    
    /**
     * Renders state of ClientConfiguration object associated with the UserInstance.
     */
    private void renderClientConfiguration() 
    throws SerialException {
        ClientConfiguration config = userInstance.getClientConfiguration();
        if (config == null) {
            return;
        }
        
        Element configElement = serverMessage.addDirective(ServerMessage.GROUP_ID_INIT, "AppSync", "config");
        String[] propertyNames = config.getPropertyNames();
        for (int i = 0; i < propertyNames.length; ++i) {
            Element pElement = document.createElement("p");
            pElement.setAttribute("n", propertyNames[i]);
            Object propertyValue = config.getProperty(propertyNames[i]);
            
            
            if (propertyValue == null) {
                // Set null property value.
                pElement.setAttribute("t", "0");
            } else {
                SerialPropertyPeer propertySyncPeer = propertyPeerFactory.getPeerForProperty(propertyValue.getClass());
                if (propertySyncPeer == null) {
                    // Unsupported property: do nothing.
                    continue;
                }
    
                // Render property value.
                propertySyncPeer.toXml(context, ClientConfiguration.class, pElement, propertyValue);
            }
            configElement.appendChild(pElement);
        }
    }
    
    /**
     * Renders enqueued commands to server message.
     */
    private void renderCommands() 
    throws SerialException {
        Command[] commands = serverUpdateManager.getCommands();
        for (int i = 0; i < commands.length; ++i) {
            CommandSynchronizePeer commandPeer = SynchronizePeerFactory.getPeerForCommand(commands[i].getClass());
            if (commandPeer == null) {
                throw new IllegalStateException("No synchronize peer found for command: " 
                        + commands[i].getClass().getName());
            }
            commandPeer.init(context);
            Element commandExecuteElement = serverMessage.addDirective(ServerMessage.GROUP_ID_UPDATE, "CmdExec", "cmd");
            commandExecuteElement.setAttribute("t", commandPeer.getClientCommandType());
            Iterator propertyNameIt = commandPeer.getPropertyNames(context, commands[i]);
            while (propertyNameIt.hasNext()) {
                String propertyName = (String) propertyNameIt.next();
                if (commandPeer.isPropertyIndexed(context, commands[i], propertyName)) {
                    Iterator propertyIndexIt = commandPeer.getPropertyIndices(context, commands[i], propertyName);
                    while (propertyIndexIt.hasNext()) {
                        int propertyIndex = ((Integer) propertyIndexIt.next()).intValue();
                        renderCommandProperty(commandExecuteElement, commandPeer, commands[i], propertyName, propertyIndex);
                    }
                } else {
                    renderCommandProperty(commandExecuteElement, commandPeer, commands[i], propertyName, -1);
                }
            }
        }
    }
    
    /**
     * Renders an individual property of a Command.
     * 
     * @param commandExecuteElement the command execute element to which the property should be added
     * @param commandPeer the CommandSynchronizePeer
     * @param command the Command
     * @param propertyName the name of the property
     * @param propertyIndex the property index
     * @throws SerialException
     */
    private void renderCommandProperty(Element commandExecuteElement, CommandSynchronizePeer commandPeer,
            Command command, String propertyName, int propertyIndex) 
    throws SerialException {
        Element pElement = document.createElement("p");
        pElement.setAttribute("n", propertyName);
        if (propertyIndex != -1) {
            // Set property index.
            pElement.setAttribute("x", Integer.toString(propertyIndex));
        }
        Object propertyValue = commandPeer.getProperty(context, command, propertyName, propertyIndex);
        if (propertyValue == null) {
            // Set null property value.
            pElement.setAttribute("t", "0");
        } else {
            SerialPropertyPeer propertySyncPeer = propertyPeerFactory.getPeerForProperty(propertyValue.getClass());
            if (propertySyncPeer == null) {
                // Unsupported property: do nothing.
                return;
            }

            // Render property value.
            propertySyncPeer.toXml(context, command.getClass(), pElement, propertyValue);
        }
        
        // Append to parent element.
        commandExecuteElement.appendChild(pElement);
    }
    
    /**
     * Renders the state of the entire component hierarchy to the server message, i.e.,
     * on initialization or when the client page is reloaded.
     * 
     * @throws SerialException
     */
    private void renderComponentsFull()
    throws SerialException {
        // Special case: clear/full redraw.  Render entire component hierarchy by rendering an
        // add directive to add the Window's child ContentPane to the root.   
        // Render all properties of Window. 
        Window window = userInstance.getApplicationInstance().getDefaultWindow();
        serverMessage.addDirective(ServerMessage.GROUP_ID_INIT, "CSyncIn", "cl");
        serverMessage.setAttribute("root", userInstance.getRootHtmlElementId());
        
        // Render Style Sheet
        renderStyleSheet();
        
        // Render Add ContentPane to Window
        ContentPane content = window.getContent();
        if (content == null) {
            throw new IllegalStateException("No content to render: default window has no content.");
        }
        
        Element upElement = serverMessage.addDirective(ServerMessage.GROUP_ID_UPDATE, "CSyncUp", "up");
        upElement.setAttribute("r", "true"); // Adding to root.
        renderComponentState(upElement, content);

        // Render Window properties
        ComponentSynchronizePeer componentPeer = SynchronizePeerFactory.getPeerForComponent(window.getClass());
        if (componentPeer == null) {
            throw new IllegalStateException("No synchronize peer found for component: " + window.getClass().getName());
        }
        Iterator propertyNameIterator = componentPeer.getOutputPropertyNames(context, window);
        while (propertyNameIterator.hasNext()) {
            String propertyName = (String) propertyNameIterator.next();
            renderComponentProperty(upElement, componentPeer, window, propertyName, false);
        }
    }
    
    /**
     * Renders an incremental update to the state of the client component hierarchy.
     * 
     * @throws SerialException
     */
    private void renderComponentsIncremental() 
    throws SerialException {
        ServerComponentUpdate[] componentUpdates = serverUpdateManager.getComponentUpdates();
        
        // Render Component Synchronization Removes
        for (int i = 0; i < componentUpdates.length; ++i) {
            if (componentUpdates[i] == null || !componentUpdates[i].hasRemovedChildren()) {
                // Update removed, or update has no removed children: do nothing.
                continue;
            }

            Element rmElement = serverMessage.addDirective(ServerMessage.GROUP_ID_UPDATE, "CSyncRm", "rm");

            Component parentComponent = componentUpdates[i].getParent();
            setComponentId(rmElement, parentComponent);
                
            Component[] removedChildren = componentUpdates[i].getRemovedChildren();
            Set removedIdSet = new HashSet(); // Set containing removed ids, to avoid removing same id multiple times.
            StringBuffer out = new StringBuffer();
            for (int j = 0; j < removedChildren.length; ++j) {
                String renderId = userInstance.getClientRenderId(removedChildren[j]);
                if (removedIdSet.contains(renderId)) {
                    continue;
                }
                if (j > 0) {
                    out.append(",");
                }
                out.append(renderId);
                removedIdSet.add(renderId);
            }
            rmElement.setAttribute("rm", out.toString());
        }

        // Render Component Synchronization Updates
        for (int i = 0; i < componentUpdates.length; ++i) {
            if (componentUpdates[i] == null) {
                // Update removed, do nothing.
                continue;
            }
            
            // Process added/removed children and updated properties of update's parent component.
            if (componentUpdates[i].hasAddedChildren() || componentUpdates[i].hasUpdatedProperties()) {
                Component parentComponent = componentUpdates[i].getParent();
                Element upElement = serverMessage.addDirective(ServerMessage.GROUP_ID_UPDATE, "CSyncUp", "up");
                setComponentId(upElement, parentComponent);
            
                // Added children.
                Component[] addedChildren = componentUpdates[i].getAddedChildren();
                if (addedChildren.length > 0) {
                    // sort components by their index
                    SortedMap indexedComponents = new TreeMap();
                    for (int j = 0; j < addedChildren.length; ++j) {
                        Component addedChild = addedChildren[j];
                        indexedComponents.put(new Integer((parentComponent.visibleIndexOf(addedChild))), addedChild);
                    }
                    Iterator indexedComponentsIter = indexedComponents.entrySet().iterator();
                    int lastIndex = Integer.MIN_VALUE;
                    while (indexedComponentsIter.hasNext()) {
                        Entry entry = (Entry)indexedComponentsIter.next();
                        Element cElement = renderComponentState(upElement, (Component) entry.getValue());
                        int index = ((Integer) entry.getKey()).intValue();
                        if (index != lastIndex + 1) {
                            cElement.setAttribute("x", Integer.toString(index));
                        }
                        lastIndex = index;
                    }
                }
                
                // Updated properties.
                renderComponentUpdatedProperties(upElement, parentComponent, componentUpdates[i]);
            }
            
            // Process updated layout data on immediate children of update's parent component.
            if (componentUpdates[i].hasUpdatedLayoutDataChildren()) {
                Component[] updatedLayoutDataChildren = componentUpdates[i].getUpdatedLayoutDataChildren();
                for (int j = 0; j < updatedLayoutDataChildren.length; ++j) {
                    Component component = updatedLayoutDataChildren[j];
                    ComponentSynchronizePeer componentPeer = SynchronizePeerFactory.getPeerForComponent(component.getClass());
                    if (componentPeer == null) {
                        throw new IllegalStateException("No synchronize peer found for component: " 
                                + component.getClass().getName());
                    }
                    Element upElement = serverMessage.addDirective(ServerMessage.GROUP_ID_UPDATE, "CSyncUp", "up");
                    setComponentId(upElement, component);
                    renderComponentProperty(upElement, componentPeer, component, Component.PROPERTY_LAYOUT_DATA, true); 
                }
            }
        }
    }
    
    /**
     * Renders a single property of a component.
     * 
     * @param parentElement the component element ("c") or update element ("up") on which the property should be added. 
     * @param componentPeer the peer of the component
     * @param c the component
     * @param propertyName the name of the property
     * @param renderNulls flag indicating whether null values should be rendered (i.e., when updating an existing) 
     *        or ignored (i.e., when rendering a complete component)
     * @throws SerialException
     */
    private void renderComponentProperty(Element parentElement, ComponentSynchronizePeer componentPeer, 
            Component c, String propertyName, boolean renderNulls) 
    throws SerialException {
        boolean indexedProperty = componentPeer.isOutputPropertyIndexed(context, c, propertyName);
        if (indexedProperty) {
            Iterator indicesIt = componentPeer.getOutputPropertyIndices(context, c, propertyName);
            if (indicesIt == null) {
                // Abort immediately if no indices can be determined.
                return;
            }
            while (indicesIt.hasNext()) {
                int index = ((Integer) indicesIt.next()).intValue();
                renderComponentPropertyImpl(parentElement, componentPeer, c, propertyName, index, renderNulls);
            }
        } else {
            renderComponentPropertyImpl(parentElement, componentPeer, c, propertyName, -1, renderNulls);
        }
    }
    
    /**
     * Implementation method for renderComponentProperty().
     * This method is invoked by renderComponentProperty() to render a non-indexed property or to render individual
     * indices of an indexed property
     * 
     * @param parentElement the component element ("c") or update element ("up") on which the property should be added. 
     * @param componentPeer the peer of the component
     * @param c the component
     * @param propertyName the name of the property
     * @param propertyIndex the index of the property (-1 for a non-indexed property)
     * @param renderNulls flag indicating whether null values should be rendered (i.e., when updating an existing) 
     *        or ignored (i.e., when rendering a complete component)
     * @throws SerialException
     */
    private void renderComponentPropertyImpl(Element parentElement, ComponentSynchronizePeer componentPeer, 
            Component c, String propertyName, int propertyIndex, boolean renderNulls) 
    throws SerialException {
        Object propertyValue = componentPeer.getOutputProperty(context, c, propertyName, propertyIndex);
        if (propertyValue == null && !renderNulls) {
            // Abort immediately if rendering of nulls is not desired.
            return;
        }
        
        // Create property element.
        Element pElement = document.createElement("p");
        
        String propertyKey = null;
        Element propertyDataElement;
        if (propertyValue != null && componentPeer.isOutputPropertyReferenced(context, c, propertyName)) {
            if (rpElement == null) {
                // Create "reference property" container element ("rp").
                rpElement = serverMessage.addDirective(ServerMessage.GROUP_ID_INIT, "CSyncUp", "rp");
            }
            
            if (propertyValueToKeyMap == null) {
                propertyValueToKeyMap = new HashMap();
            } else {
                propertyKey = (String) propertyValueToKeyMap.get(propertyValue);
            }
            
            if (propertyKey == null) {
                propertyKey = Integer.toString(nextPropertyKey++);
                propertyValueToKeyMap.put(propertyValue, propertyKey);

                propertyDataElement = document.createElement("p");
                propertyDataElement.setAttribute("i", propertyKey);
                
                rpElement.appendChild(propertyDataElement);
            } else {
                propertyDataElement = null;
            }

            pElement.setAttribute("r", propertyKey);
        } else {
            propertyDataElement = pElement;
        }
        
        String methodName = componentPeer.getOutputPropertyMethodName(context, c, propertyName);
        if (methodName != null) {
            // Set method name.
            pElement.setAttribute("m", methodName);
        } else {
            // Set property name.
            pElement.setAttribute("n", propertyName);
        }
        
        if (propertyIndex != -1) {
            // Set property index.
            pElement.setAttribute("x", Integer.toString(propertyIndex));
        }
        
        if (propertyValue == null) {
            // Set null property value.
            pElement.setAttribute("t", "0");
        } else if (propertyDataElement != null) {
            // Set non-null property value (if necessary, i.e., if propertyDataElement is set).
            // Obtain appropriate peer.
            SerialPropertyPeer propertySyncPeer = propertyPeerFactory.getPeerForProperty(propertyValue.getClass());
            if (propertySyncPeer == null) {
                // Unsupported property: do nothing.
                return;
            }

            // Render property value.
            propertySyncPeer.toXml(context, c.getClass(), propertyDataElement, propertyValue);
        }
        
        // Append to parent element.
        parentElement.appendChild(pElement);
    }
    
    /**
     * Renders the full state of a specific component.
     * 
     * @param parentElement the element to append the component element to
     * @param c the rendering component
     */
    private Element renderComponentState(Element parentElement, Component c)
    throws SerialException {
        Document document = parentElement.getOwnerDocument();
        ComponentSynchronizePeer componentPeer = SynchronizePeerFactory.getPeerForComponent(c.getClass());
        if (componentPeer == null) {
            throw new IllegalStateException("No synchronize peer found for component: " + c.getClass().getName());
        }
        
        Element cElement = document.createElement("c");
        cElement.setAttribute("i", userInstance.getClientRenderId(c));

        cElement.setAttribute("t", componentPeer.getClientComponentType(true));
        
        componentPeer.init(context, c);

        renderComponentStyleName(cElement, c, false);
        renderComponentStyle(cElement, c, false);
        
        // Render focus traversal information.
        if (c.getFocusNextId() != null || c.getFocusPreviousId() != null) {
            Element fElement = document.createElement("f");
            if (c.getFocusNextId() != null) {
                Component focusComponent = c.getApplicationInstance().getComponentByRenderId(c.getFocusNextId());
                if (focusComponent != null) {
                    fElement.setAttribute("n", userInstance.getClientRenderId(focusComponent));
                }
            }
            if (c.getFocusPreviousId() != null) {
                Component focusComponent = c.getApplicationInstance().getComponentByRenderId(c.getFocusPreviousId());
                if (focusComponent != null) {
                    fElement.setAttribute("p", userInstance.getClientRenderId(focusComponent));
                }
            }
            cElement.appendChild(fElement);
        }
        
        // Render enabled state.
        if (!c.isEnabled()) {
            Element enElement = document.createElement("en");
            enElement.appendChild(document.createTextNode("false"));
            cElement.appendChild(enElement);
        }
        
        // Render locale.
        if (c.getLocale() != null) {
            Element localeElement = document.createElement("locale");
            localeElement.appendChild(document.createTextNode(getClientLocaleString(c.getLocale())));
            cElement.appendChild(localeElement);
        }
        
        // Render layout direction.
        if (c.getLayoutDirection() != null) {
            Element dirElement = document.createElement("dir");
            dirElement.appendChild(document.createTextNode(c.getLayoutDirection().isLeftToRight() ? "ltr" : "rtl"));
            cElement.appendChild(dirElement);
        }
        
        // Render component properties.
        Iterator propertyNameIterator = componentPeer.getOutputPropertyNames(context, c);
        while (propertyNameIterator.hasNext()) {
            String propertyName = (String) propertyNameIterator.next();
            renderComponentProperty(cElement, componentPeer, c, propertyName, false);
        }
        
        // Render immediate event flags.
        Iterator eventTypeIterator = componentPeer.getEventTypes(context, c);
        while (eventTypeIterator.hasNext()) {
            String eventType = (String) eventTypeIterator.next();
            if (!componentPeer.hasListeners(context, c, eventType)) {
                continue;
            }
            Element eElement = document.createElement("e");
            eElement.setAttribute("t", eventType);
            cElement.appendChild(eElement);
        }
        
        // Render child components.
        Component[] children = c.getVisibleComponents();
        for (int i = 0; i < children.length; ++i) {
            renderComponentState(cElement, children[i]);
        }
        
        // Append component element to parent.
        parentElement.appendChild(cElement);
        
        return cElement;
    }
    
    /**
     * Sets the directly referenced style of a component.
     * If the style has not been rendered in the current synchronization message,
     * it will be added to it.
     */
    private void renderComponentStyle(Element element, Component c, boolean required) 
    throws SerialException {
        Style style = c.getStyle();
        if (style == null) {
            if (required) {
                element.appendChild(document.createElement("sr"));
            }
            return;
        }
        
        if (rsElement == null) {
            rsElement = serverMessage.addDirective(ServerMessage.GROUP_ID_INIT, "CSyncUp", "rs");
        }

        String styleKey = null;
        if (styleValueToKeyMap == null) {
            styleValueToKeyMap = new HashMap();
        } else {
            styleKey = (String) styleValueToKeyMap.get(style);
        }
        
        if (styleKey == null) {
            styleKey = Integer.toString(nextStyleKey++);
            styleValueToKeyMap.put(style, styleKey);

            Element sElement = document.createElement("s");
            sElement.setAttribute("i", styleKey);
            renderStyle(c.getClass(), sElement, style);
            rsElement.appendChild(sElement);
        }
        
        Element srElement = document.createElement("sr");
        srElement.appendChild(document.createTextNode(styleKey));
        element.appendChild(srElement);
    }

    /**
     * Sets the style name attribute on a component (c) element.
     * 
     * @param element the element to append the style attributes to
     * @param c the rendering component
     * @param required require attribute to be rendered, even if style is null
     *        (used when updating style)
     */ 
    private void renderComponentStyleName(Element element, Component c, boolean required) 
    throws SerialException {
        StyleSheet styleSheet = c.getApplicationInstance().getStyleSheet();
        String styleName = c.getStyleName();
        
        if (styleSheet == null || (!required && styleName == null)) { 
            return;
        }
        
        // Determine the class of the style that will be used to render the component.
        // This may be the component's class, or one of its ancestor classes.
        Class styleClass = c.getClass();
        Style style = styleSheet.getStyle(styleName, styleClass, false);
        while (style == null && styleClass != Component.class) {
            styleClass = styleClass.getSuperclass();
            style = styleSheet.getStyle(styleName, styleClass, false);
        }
        
        // Retrieve the component peer for the style class.
        ComponentSynchronizePeer componentPeer = SynchronizePeerFactory.getPeerForComponent(styleClass, false);
        
        if (componentPeer == null) {
            // A synchronize peer DOES NOT exist for the style class, the style name will be rendered as:
            // styleName:styleClass.
            componentPeer = SynchronizePeerFactory.getPeerForComponent(styleClass, true);
            if (componentPeer == null) {
                // Should not occur.
                throw new SerialException("No peer available for component: " + styleClass.getName(), null);
            }
            Element sElement = document.createElement("s");
            sElement.appendChild(document.createTextNode((styleName == null ? "" : styleName) + ":" + styleClass.getName()));
            element.appendChild(sElement);
        } else {
            // A synchronize peer exists for the style class, simply render the style name.
            Element sElement = document.createElement("s");
            sElement.appendChild(document.createTextNode(styleName == null ? "" : styleName));
            element.appendChild(sElement);
        }
    }
    
    /**
     * Renders updated properties of a component.
     * 
     * @param upElement the "up" update element
     * @param c the updating component
     * @param update the ServerComponentUpdate
     * @throws SerialException
     */
    private void renderComponentUpdatedProperties(Element upElement, Component c, ServerComponentUpdate update) 
    throws SerialException {
        ComponentSynchronizePeer componentPeer = SynchronizePeerFactory.getPeerForComponent(c.getClass());
        if (componentPeer == null) {
            throw new IllegalStateException("No synchronize peer found for component: " + c.getClass().getName());
        }

        Iterator propertyNameIt = componentPeer.getUpdatedOutputPropertyNames(context, c, update);
        while (propertyNameIt.hasNext()) {
            String propertyName = (String) propertyNameIt.next();
            renderComponentProperty(upElement, componentPeer, c, propertyName, true);
        }
        
        if (update.hasUpdatedProperties()) {
            if (update.hasUpdatedProperty(Component.STYLE_NAME_CHANGED_PROPERTY)) {
                renderComponentStyleName(upElement, c, true);
            }
            
            if (update.hasUpdatedProperty(Component.STYLE_CHANGED_PROPERTY)) {
                renderComponentStyle(upElement, c, true);
            }
            
            // Render enabled state update.
            if (update.hasUpdatedProperty(Component.ENABLED_CHANGED_PROPERTY)) {
                Element enElement = document.createElement("en");
                enElement.appendChild(document.createTextNode(update.getParent().isEnabled() ? "true" : "false"));
                upElement.appendChild(enElement);
            }
            
            // Render locale update.
            if (update.hasUpdatedProperty(Component.LOCALE_CHANGED_PROPERTY)) {
                Element localeElement = document.createElement("locale");
                if (c.getLocale() != null) {
                    localeElement.appendChild(document.createTextNode(getClientLocaleString(c.getLocale())));
                }
                upElement.appendChild(localeElement);
            }

            // Render layout direction update.
            if (update.hasUpdatedProperty(Component.LAYOUT_DIRECTION_CHANGED_PROPERTY)) {
                Element dirElement = document.createElement("dir");
                if (c.getLayoutDirection() != null) {
                    dirElement.appendChild(document.createTextNode(c.getLayoutDirection().isLeftToRight() ? "ltr" : "rtl"));
                }
                upElement.appendChild(dirElement);
            }
        }
        
        // Render immediate event flags.
        Iterator eventTypeIterator = componentPeer.getEventTypes(context, c);
        while (eventTypeIterator.hasNext()) {
            String eventType = (String) eventTypeIterator.next();
            if (!componentPeer.hasUpdatedListeners(context, c, update, eventType)) {
                continue;
            }
            Element eElement = document.createElement("e");
            eElement.setAttribute("t", eventType);
            eElement.setAttribute("v", componentPeer.hasListeners(context, c, eventType) ? "true" : "false");
            upElement.appendChild(eElement);
        }
    }
    
    /**
     * Renders the focus state of the application, if necessary.
     */
    private void renderFocus() {
        Component focusedComponent = userInstance.getApplicationInstance().getFocusedComponent();
        if (focusedComponent != null) {
            Element focusElement = serverMessage.addDirective(ServerMessage.GROUP_ID_UPDATE, "CFocus", "focus");
            focusElement.setAttribute("i", userInstance.getClientRenderId(focusedComponent));
        }
    }
    
    /**
     * Renders an individual style.
     * 
     * @param componentClass the component class
     * @param sElement the style ("s") element into which the style should be rendered
     * @param style the style
     * @throws SerialException
     */
    private void renderStyle(Class componentClass, Element sElement, Style style)
    throws SerialException {
        Document document = sElement.getOwnerDocument();
        
        ComponentIntrospector ci;
        try {
            ci = (ComponentIntrospector) IntrospectorFactory.get(componentClass.getName(),
                    componentClass.getClassLoader());
        } catch (ClassNotFoundException ex) {
            // Should never occur.
            throw new RuntimeException("Internal error.", ex);
        }
        
        Iterator it = style.getPropertyNames();
        while (it.hasNext()) {
            String propertyName = (String) it.next();

            if (ci.isIndexedProperty(propertyName)) {
                Iterator indicesIt = style.getPropertyIndices(propertyName);
                while (indicesIt.hasNext()) {
                    int index = ((Integer) indicesIt.next()).intValue();
                    Object propertyValue = style.getIndex(propertyName, index);
                    if (propertyValue == null) {
                        continue;
                    }
                    SerialPropertyPeer propertySyncPeer = propertyPeerFactory.getPeerForProperty(propertyValue.getClass());
                    if (propertySyncPeer == null) {
                        // Log and ignore property value that cannot be serialized. 
                        Log.log("No peer found for property class: " + propertyValue.getClass());
                        continue;
                    }
                    Element pElement = document.createElement("p");
                    pElement.setAttribute("n", propertyName);
                    // Set property index.
                    pElement.setAttribute("x", Integer.toString(index));
                    propertySyncPeer.toXml(context, componentClass, pElement, propertyValue);
                    sElement.appendChild(pElement);
                }
            } else {
                Object propertyValue = style.get(propertyName);
                if (propertyValue == null) {
                    continue;
                }
                SerialPropertyPeer propertySyncPeer = propertyPeerFactory.getPeerForProperty(propertyValue.getClass());
                if (propertySyncPeer == null) {
                    Log.log("No peer found for property class: " + propertyValue.getClass());
                    // Log and ignore property value that cannot be serialized.
                    continue;
                }
                
                Element pElement = document.createElement("p");
                pElement.setAttribute("n", propertyName);
                propertySyncPeer.toXml(context, componentClass, pElement, propertyValue);
                sElement.appendChild(pElement);
            }
        }
    }
    
    /**
     * Renders the complete style sheet of an application to the ServerMessage.
     * 
     * @throws SerialException
     */
    private void renderStyleSheet() 
    throws SerialException {
        Element ssElement = serverMessage.addDirective(ServerMessage.GROUP_ID_UPDATE, "CSyncUp", "ss");
        
        StyleSheet styleSheet = userInstance.getApplicationInstance().getStyleSheet();
        if (styleSheet == null) {
            return;
        }
        
        Document document = ssElement.getOwnerDocument();
        Iterator styleNameIterator = styleSheet.getStyleNames();
        while (styleNameIterator.hasNext()) {
            String styleName = (String) styleNameIterator.next();
            Iterator componentTypeIterator = styleSheet.getComponentTypes(styleName);
            while (componentTypeIterator.hasNext()) {
                Class componentClass = (Class) componentTypeIterator.next();
                Element sElement = document.createElement("s");
                
                // Retrieve component synchronize peer for style's SPECIFIC component class (not searching superclasses).
                ComponentSynchronizePeer componentPeer = SynchronizePeerFactory.getPeerForComponent(componentClass, false);
                if (componentPeer == null) {
                    // No synchronize peer exists for style's specific component class, find synchronize peer for
                    // a superclass.
                    componentPeer = SynchronizePeerFactory.getPeerForComponent(componentClass, true);
                    if (componentPeer == null) {
                        // No synchronize peer for any superclass.
                        throw new SerialException("No peer available for component: " + componentClass.getName(), null);
                    }
                    
                    // Render style name as styleName:styleClass.
                    sElement.setAttribute("n", (styleName == null ? "" : styleName) + ":" + componentClass.getName());
                } else {
                    // Synchronize peer does exist for style's specific component class, render style name unmodified.
                    if (styleName != null) {
                        sElement.setAttribute("n", styleName);
                    }
                }

                sElement.setAttribute("t", componentPeer.getClientComponentType(false));
                
                Style style = styleSheet.getStyle(styleName, componentClass, false);
                renderStyle(componentClass, sElement, style);
                
                ssElement.appendChild(sElement);
            }
        }
    }

    /**
     * Utility method to identify a component in an add/update directive.
     * Adds an 'r="true"' attribute if the updating component is the root.
     * Adds an 'i="xxx"' attribute if the updating component is not root
     * 
     * @param element the element to add the component identifier to
     * @param component the component
     */
    private void setComponentId(Element element, Component component) {
        if (component.getParent() == null) {
            element.setAttribute("r", "true");
        } else {
            element.setAttribute("i", userInstance.getClientRenderId(component));
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy