nextapp.echo2.webcontainer.ContainerSynchronizeService Maven / Gradle / Ivy
Show all versions of ibis-echo2 Show documentation
/*
* 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.echo2.webcontainer;
import java.util.HashSet;
import java.util.Set;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import nextapp.echo2.app.ApplicationInstance;
import nextapp.echo2.app.Command;
import nextapp.echo2.app.Component;
import nextapp.echo2.app.Window;
import nextapp.echo2.app.update.PropertyUpdate;
import nextapp.echo2.app.update.ServerComponentUpdate;
import nextapp.echo2.app.update.ServerUpdateManager;
import nextapp.echo2.app.update.UpdateManager;
import nextapp.echo2.webcontainer.syncpeer.WindowPeer;
import nextapp.echo2.webrender.Connection;
import nextapp.echo2.webrender.ServerMessage;
import nextapp.echo2.webrender.Service;
import nextapp.echo2.webrender.UserInstance;
import nextapp.echo2.webrender.WebRenderServlet;
import nextapp.echo2.webrender.servermessage.WindowUpdate;
import nextapp.echo2.webrender.service.JavaScriptService;
import nextapp.echo2.webrender.service.SynchronizeService;
import nextapp.echo2.webrender.util.DomUtil;
/**
* A service which synchronizes the state of the client with that of the
* server. Requests made to this service are in the form of "ClientMessage"
* XML documents which describe the users actions since the last
* synchronization, e.g., input typed into text fields and the action taken
* (e.g., a button press) which caused the server interaction.
* The service then communicates these changes to the server-side application,
* and then generates an output "ServerMessage" containing instructions to
* update the client-side state of the application to the updated server-side
* state.
*
* This class is derived from the base class SynchronizeService
* of the web renderer, which handles the lower-level work.
*/
public class ContainerSynchronizeService extends SynchronizeService {
/**
* Service to provide supporting JavaScript library.
*/
public static final Service WEB_CONTAINER_SERVICE = JavaScriptService.forResource("Echo.WebContainer",
"/nextapp/echo2/webcontainer/resource/js/WebContainer.js");
static {
WebRenderServlet.getServiceRegistry().add(WEB_CONTAINER_SERVICE);
}
/**
* A single shared instance of this stateless service.
*/
public static final ContainerSynchronizeService INSTANCE = new ContainerSynchronizeService();
/**
* Determines if any of the Component
object in the provided
* set of "potential" ancestors is in fact an ancestor of
* component
.
*
* @param potentialAncestors a set containing Component
s
* @param component the Component
to evaluate
* @return true if any component in potentialAncestors
is an
* ancestor of component
*/
private static boolean isAncestor(Set potentialAncestors, Component component) {
component = component.getParent();
while (component != null) {
if (potentialAncestors.contains(component)) {
return true;
}
component = component.getParent();
}
return false;
}
/**
* ClientMessagePartProcessor
to process user-interface
* component input message parts.
*/
private ClientMessagePartProcessor propertyUpdateProcessor = new ClientMessagePartProcessor() {
/**
* @see nextapp.echo2.webrender.service.SynchronizeService.ClientMessagePartProcessor#getName()
*/
public String getName() {
return "EchoPropertyUpdate";
}
/**
* @see nextapp.echo2.webrender.service.SynchronizeService.ClientMessagePartProcessor#process(
* nextapp.echo2.webrender.UserInstance, org.w3c.dom.Element)
*/
public void process(UserInstance userInstance, Element messagePartElement) {
ContainerInstance ci = (ContainerInstance) userInstance;
Element[] propertyElements = DomUtil.getChildElementsByTagName(messagePartElement, "property");
for (int i = 0; i < propertyElements.length; ++i) {
String componentId = propertyElements[i].getAttribute("component-id");
Component component = ci.getComponentByElementId(componentId);
if (component == null) {
// Component removed. This should not frequently occur, however in certain cases,
// e.g., dragging a window during an during before, during, after a server pushed update
// can result in the condition where input is received from a component which no longer
// is registered.
continue;
}
ComponentSynchronizePeer syncPeer = SynchronizePeerFactory.getPeerForComponent(component.getClass());
if (!(syncPeer instanceof PropertyUpdateProcessor)) {
throw new IllegalStateException("Target peer is not an PropertyUpdateProcessor.");
}
((PropertyUpdateProcessor) syncPeer).processPropertyUpdate(ci, component, propertyElements[i]);
}
}
};
/**
* ClientMessagePartProcessor
to process user-interface
* component action message parts.
*/
private ClientMessagePartProcessor actionProcessor = new ClientMessagePartProcessor() {
/**
* @see nextapp.echo2.webrender.service.SynchronizeService.ClientMessagePartProcessor#getName()
*/
public String getName() {
return "EchoAction";
}
/**
* @see nextapp.echo2.webrender.service.SynchronizeService.ClientMessagePartProcessor#process(
* nextapp.echo2.webrender.UserInstance, org.w3c.dom.Element)
*/
public void process(UserInstance userInstance, Element messagePartElement) {
ContainerInstance ci = (ContainerInstance) userInstance;
Element actionElement = DomUtil.getChildElementByTagName(messagePartElement, "action");
String componentId = actionElement.getAttribute("component-id");
Component component = ci.getComponentByElementId(componentId);
if (component == null) {
// Component removed. This should not frequently occur, however in certain cases,
// e.g., dragging a window during an during before, during, after a server pushed update
// can result in the condition where input is received from a component which no longer
// is registered.
return;
}
ComponentSynchronizePeer syncPeer = SynchronizePeerFactory.getPeerForComponent(component.getClass());
if (!(syncPeer instanceof ActionProcessor)) {
throw new IllegalStateException("Target peer is not an ActionProcessor.");
}
((ActionProcessor) syncPeer).processAction(ci, component, actionElement);
}
};
/**
* Creates a new ContainerSynchronizeService
.
* Installs "ClientMessage" part processors.
*/
private ContainerSynchronizeService() {
super();
registerClientMessagePartProcessor(propertyUpdateProcessor);
registerClientMessagePartProcessor(actionProcessor);
}
/**
* Performs disposal operations on components which have been removed from
* the hierarchy. Removes any RenderState
objects being
* stored in the ContainerInstance
for the disposed
* components. Invokes ComponentSynchronizePeer.renderDispose()
* such that the peers of the components can dispose of resources on the
* client.
*
* @param rc the relevant RenderContext
* @param componentUpdate the ServerComponentUpdate
causing
* components to be disposed.
* @param disposedComponents the components to dispose
*/
private void disposeComponents(RenderContext rc, ServerComponentUpdate componentUpdate,
Component[] disposedComponents) {
ContainerInstance ci = rc.getContainerInstance();
for (int i = 0; i < disposedComponents.length; ++i) {
ComponentSynchronizePeer disposedSyncPeer = SynchronizePeerFactory.getPeerForComponent(
disposedComponents[i].getClass());
disposedSyncPeer.renderDispose(rc, componentUpdate, disposedComponents[i]);
ci.removeRenderState(disposedComponents[i]);
}
}
/**
* Invokes renderDispose()
on
* ComponentSynchronizePeer
s in a hierarchy of Components that is
* be re-rendered on the client. That is, this hierarchy of components exist on
* the client, are being removed, and will be re-rendered due to a container
* component NOT being capable of rendering a partial update.
* This method is invoked recursively.
*
* @param rc the relevant RenderContext
* @param update the update
* @param parent the Component
whose descendants should be disposed
*/
private void disposeReplacedDescendants(RenderContext rc, ServerComponentUpdate update, Component parent) {
Component[] replacedComponents = parent.getVisibleComponents();
boolean isRoot = parent == update.getParent();
for (int i = 0; i < replacedComponents.length; ++i) {
// Verify that component was not added on this synchronization.
if (isRoot && update.hasAddedChild(replacedComponents[i])) {
// Component was added as a child on this synchronization:
// There is no reason to dispose of it as it does not yet exist on the client.
continue;
}
// Recursively dispose child components.
disposeReplacedDescendants(rc, update, replacedComponents[i]);
// Dispose component.
ComponentSynchronizePeer syncPeer = SynchronizePeerFactory.getPeerForComponent(replacedComponents[i].getClass());
syncPeer.renderDispose(rc, update, replacedComponents[i]);
}
}
/**
* Determines if the specified component
has been rendered to
* the client by determining if it is a descendant of any
* LazyRenderContainer
s and if so querying them to determine
* the hierarchy's render state. This method is recursively invoked.
*
* @param ci the relevant ContainerInstance
* @param component the Component
to analyze
* @return true
if the Component
has been
* rendered to the client
*/
private boolean isRendered(ContainerInstance ci, Component component) {
Component parent = component.getParent();
if (parent == null) {
return true;
}
ComponentSynchronizePeer syncPeer = SynchronizePeerFactory.getPeerForComponent(parent.getClass());
if (syncPeer instanceof LazyRenderContainer) {
boolean rendered = ((LazyRenderContainer) syncPeer).isRendered(ci, parent, component);
if (!rendered) {
return false;
}
}
return isRendered(ci, parent);
}
/**
* Retrieves information about the current focused component on the client,
* if provided, and in such case notifies the
* ApplicationInstance
of the focus.
*
* @param rc the relevant RenderContext
* @param clientMessageDocument the ClientMessage Document
to
* retrieve focus information from
*/
private void processClientFocusedComponent(RenderContext rc, Document clientMessageDocument) {
if (clientMessageDocument.getDocumentElement().hasAttribute("focus")) {
String focusedComponentId = clientMessageDocument.getDocumentElement().getAttribute("focus");
Component component = null;
if (focusedComponentId.length() > 2) {
// Valid component id.
component = rc.getContainerInstance().getComponentByElementId(focusedComponentId);
}
ApplicationInstance applicationInstance = rc.getContainerInstance().getApplicationInstance();
applicationInstance.getUpdateManager().getClientUpdateManager().setApplicationProperty(
ApplicationInstance.FOCUSED_COMPONENT_CHANGED_PROPERTY, component);
}
}
/**
* Handles an invalid transaction id scenario, reinitializing the entire
* state of the client.
*
* @param rc the relevant RenderContex
*/
private void processInvalidTransaction(RenderContext rc) {
WindowUpdate.renderReload(rc.getServerMessage());
}
/**
* Executes queued Command
s.
*
* @param rc the relevant RenderContext
*/
private void processQueuedCommands(RenderContext rc) {
ServerUpdateManager serverUpdateManager = rc.getContainerInstance().getUpdateManager().getServerUpdateManager();
Command[] commands = serverUpdateManager.getCommands();
for (int i = 0; i < commands.length; i++) {
CommandSynchronizePeer peer = SynchronizePeerFactory.getPeerForCommand(commands[i].getClass());
peer.render(rc, commands[i]);
}
}
/**
* Processes updates from the application, generating an outgoing
* ServerMessage
.
*
* @param rc the relevant RenderContext
*/
private void processServerUpdates(RenderContext rc) {
ContainerInstance ci = rc.getContainerInstance();
UpdateManager updateManager = ci.getUpdateManager();
ServerUpdateManager serverUpdateManager = updateManager.getServerUpdateManager();
ServerComponentUpdate[] componentUpdates = updateManager.getServerUpdateManager().getComponentUpdates();
if (serverUpdateManager.isFullRefreshRequired()) {
Window window = rc.getContainerInstance().getApplicationInstance().getDefaultWindow();
ServerComponentUpdate fullRefreshUpdate = componentUpdates[0];
// Dispose of removed descendants.
Component[] removedDescendants = fullRefreshUpdate.getRemovedDescendants();
disposeComponents(rc, fullRefreshUpdate, removedDescendants);
// Perform full refresh.
RootSynchronizePeer rootSyncPeer
= (RootSynchronizePeer) SynchronizePeerFactory.getPeerForComponent(window.getClass());
rootSyncPeer.renderRefresh(rc, fullRefreshUpdate, window);
setRootLayoutDirection(rc);
} else {
// Remove any updates whose updates are descendants of components which have not been rendered to the
// client yet due to lazy-loading containers.
for (int i = 0; i < componentUpdates.length; ++i) {
if (!isRendered(ci, componentUpdates[i].getParent())) {
componentUpdates[i] = null;
}
}
// Set of Components whose HTML was entirely re-rendered, negating the need
// for updates of their children to be processed.
Set fullyReplacedHierarchies = new HashSet();
for (int i = 0; i < componentUpdates.length; ++i) {
if (componentUpdates[i] == null) {
// Update removed, do nothing.
continue;
}
// Dispose of removed children.
Component[] removedChildren = componentUpdates[i].getRemovedChildren();
disposeComponents(rc, componentUpdates[i], removedChildren);
// Dispose of removed descendants.
Component[] removedDescendants = componentUpdates[i].getRemovedDescendants();
disposeComponents(rc, componentUpdates[i], removedDescendants);
// Perform update.
Component parentComponent = componentUpdates[i].getParent();
if (!isAncestor(fullyReplacedHierarchies, parentComponent)) {
// Only perform update if ancestor of updated component is NOT contained in
// the set of components whose descendants were fully replaced.
ComponentSynchronizePeer syncPeer = SynchronizePeerFactory.getPeerForComponent(parentComponent.getClass());
String targetId;
if (parentComponent.getParent() == null) {
targetId = null;
} else {
ComponentSynchronizePeer parentSyncPeer
= SynchronizePeerFactory.getPeerForComponent(parentComponent.getParent().getClass());
targetId = parentSyncPeer.getContainerId(parentComponent);
}
boolean fullReplacement = syncPeer.renderUpdate(rc, componentUpdates[i], targetId);
if (fullReplacement) {
// Invoke renderDispose() on hierarchy of components destroyed by
// the complete replacement.
disposeReplacedDescendants(rc, componentUpdates[i], parentComponent);
fullyReplacedHierarchies.add(parentComponent);
}
}
}
}
}
/**
* @see nextapp.echo2.webrender.service.SynchronizeService#renderInit(nextapp.echo2.webrender.Connection,
* org.w3c.dom.Document)
*/
protected ServerMessage renderInit(Connection conn, Document clientMessageDocument) {
ServerMessage serverMessage = new ServerMessage();
RenderContext rc = new RenderContextImpl(conn, serverMessage);
ContainerInstance containerInstance = rc.getContainerInstance();
try {
serverMessage.addLibrary(WEB_CONTAINER_SERVICE.getId());
processClientMessage(conn, clientMessageDocument);
if (!containerInstance.isInitialized()) {
containerInstance.init(conn);
}
ApplicationInstance applicationInstance = rc.getContainerInstance().getApplicationInstance();
ApplicationInstance.setActive(applicationInstance);
Window window = applicationInstance.getDefaultWindow();
ServerComponentUpdate componentUpdate = new ServerComponentUpdate(window);
ComponentSynchronizePeer syncPeer = SynchronizePeerFactory.getPeerForComponent(window.getClass());
((WindowPeer) syncPeer).renderRefresh(rc, componentUpdate, window);
setAsynchronousMonitorInterval(rc);
setFocus(rc, true);
setModalContextRootId(rc);
setRootLayoutDirection(rc);
processQueuedCommands(rc);
applicationInstance.getUpdateManager().purge();
return serverMessage;
} finally {
ApplicationInstance.setActive(null);
}
}
/**
* @see nextapp.echo2.webrender.service.SynchronizeService#renderUpdate(nextapp.echo2.webrender.Connection,
* org.w3c.dom.Document)
*/
protected ServerMessage renderUpdate(Connection conn, Document clientMessageDocument) {
ServerMessage serverMessage = new ServerMessage();
RenderContext rc = new RenderContextImpl(conn, serverMessage);
ContainerInstance ci = rc.getContainerInstance();
ApplicationInstance applicationInstance = ci.getApplicationInstance();
try {
if (!validateTransactionId(ci, clientMessageDocument)) {
processInvalidTransaction(rc);
return serverMessage;
}
// Mark instance as active.
ApplicationInstance.setActive(applicationInstance);
UpdateManager updateManager = applicationInstance.getUpdateManager();
processClientFocusedComponent(rc, clientMessageDocument);
// Process updates from client.
processClientMessage(conn, clientMessageDocument);
updateManager.processClientUpdates();
// Process updates from server.
processServerUpdates(rc);
setAsynchronousMonitorInterval(rc);
setFocus(rc, false);
setModalContextRootId(rc);
processQueuedCommands(rc);
updateManager.purge();
return serverMessage;
} finally {
// Mark instance as inactive.
ApplicationInstance.setActive(null);
}
}
/**
* Sets the interval between asynchronous monitor requests.
*
* @param rc the relevant RenderContext
.
*/
private void setAsynchronousMonitorInterval(RenderContext rc) {
boolean hasTaskQueues = rc.getContainerInstance().getApplicationInstance().hasTaskQueues();
if (hasTaskQueues) {
int interval = rc.getContainerInstance().getCallbackInterval();
rc.getServerMessage().setAsynchronousMonitorInterval(interval);
} else {
rc.getServerMessage().setAsynchronousMonitorInterval(-1);
}
}
/**
* Update the ServerMessage
to set the focused component if
* required.
*
* @param rc the relevant RenderContext
* @param initial a flag indicating whether the initial synchronization is
* being performed, i.e., whether this method is being invoked from
* renderInit()
*/
private void setFocus(RenderContext rc, boolean initial) {
ApplicationInstance applicationInstance = rc.getContainerInstance().getApplicationInstance();
Component focusedComponent = null;
if (initial) {
focusedComponent = applicationInstance.getFocusedComponent();
} else {
ServerUpdateManager serverUpdateManager = applicationInstance.getUpdateManager().getServerUpdateManager();
PropertyUpdate focusUpdate =
serverUpdateManager.getApplicationPropertyUpdate(ApplicationInstance.FOCUSED_COMPONENT_CHANGED_PROPERTY);
if (focusUpdate != null) {
focusedComponent = (Component) focusUpdate.getNewValue();
}
}
if (focusedComponent != null) {
ComponentSynchronizePeer componentSyncPeer
= SynchronizePeerFactory.getPeerForComponent(focusedComponent.getClass());
if (componentSyncPeer instanceof FocusSupport) {
((FocusSupport) componentSyncPeer).renderSetFocus(rc, focusedComponent);
}
}
}
/**
* Update the ServerMessage
to describe the current root
* element of the modal context.
*
* @param rc the relevant RenderContext
*/
private void setModalContextRootId(RenderContext rc) {
ApplicationInstance applicationInstance = rc.getContainerInstance().getApplicationInstance();
Component modalContextRoot = applicationInstance.getModalContextRoot();
if (modalContextRoot == null) {
rc.getServerMessage().setModalContextRootId(null);
} else {
rc.getServerMessage().setModalContextRootId(ContainerInstance.getElementId(modalContextRoot));
}
}
/**
* Update the ServerMessage
to describe the current root
* layout direction
*
* @param rc the relevant RenderContext
*/
private void setRootLayoutDirection(RenderContext rc) {
ApplicationInstance applicationInstance = rc.getContainerInstance().getApplicationInstance();
rc.getServerMessage().setRootLayoutDirection(applicationInstance.getLayoutDirection().isLeftToRight()
? ServerMessage.LEFT_TO_RIGHT : ServerMessage.RIGHT_TO_LEFT);
}
/**
* Determines if transaction id retrieved from client matches current transaction id.
*
* @param containerInstance the relevant ContainerInstance
* @param clientMessageDocument the incoming client message
* @return true if the transaction id is valid
*/
private boolean validateTransactionId(ContainerInstance containerInstance, Document clientMessageDocument) {
try {
long clientTransactionId = Long.parseLong(clientMessageDocument.getDocumentElement().getAttribute("trans-id"));
return containerInstance.getCurrentTransactionId() == clientTransactionId;
} catch (NumberFormatException ex) {
// Client has not provided a transaction id at all, return true.
// This should not occur.
return true;
}
}
}