/* * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Max Stepanov * * $Id$ */ package org.nuxeo.ecm.platform.domsync.core; import java.util.ArrayList; import java.util.List; import org.nuxeo.ecm.platform.domsync.core.events.DOMAttrModifiedEvent; import org.nuxeo.ecm.platform.domsync.core.events.DOMCharacterDataModifiedEvent; import org.nuxeo.ecm.platform.domsync.core.events.DOMMutationEvent; import org.nuxeo.ecm.platform.domsync.core.events.DOMNodeInsertedEvent; import org.nuxeo.ecm.platform.domsync.core.events.DOMNodeRemovedEvent; import org.w3c.dom.Attr; import org.w3c.dom.CharacterData; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; import org.w3c.dom.events.Event; import org.w3c.dom.events.EventListener; import org.w3c.dom.events.MutationEvent; /** * @author Max Stepanov */ public class DOMSynchronizer implements EventListener, IDOMMutationListener { private static final String DOM_SUBTREE_MODIFIED = "DOMSubtreeModified"; private static final String DOM_NODE_INSERTED = "DOMNodeInserted"; private static final String DOM_NODE_REMOVED = "DOMNodeRemoved"; private static final String DOM_NODE_REMOVED_FROM_DOCUMENT = "DOMNodeRemovedFromDocument"; private static final String DOM_NODE_INSERTED_INTO_DOCUMENT = "DOMNodeInsertedIntoDocument"; private static final String DOM_ATTR_MODIFIED = "DOMAttrModified"; private static final String DOM_CHARACTER_DATA_MODIFIED = "DOMCharacterDataModified"; private final Document document; private final IDOMSupport domSupport; private int dispatchLevel; private DOMMutationEvent currentEvent; /* for debug/test purposes */ private final List<IDOMMutationListener> listeners = new ArrayList<IDOMMutationListener>(); public DOMSynchronizer(Document document, IDOMSupport domSupport) { this.document = document; this.domSupport = domSupport; } /** * Document event handler */ public void handleEvent(Event evt) { if (!(evt instanceof MutationEvent)) { return; } MutationEvent event = (MutationEvent) evt; String type = event.getType(); if (DOM_CHARACTER_DATA_MODIFIED.equals(type)) { Node target = (Node) event.getTarget(); String newValue = event.getNewValue(); dispatchEvent(new DOMCharacterDataModifiedEvent(DOMUtil.computeNodeXPath(document, target), newValue)); } else if (DOM_NODE_INSERTED.equals(type)) { Node target = event.getRelatedNode(); Node insertedNode = (Node) event.getTarget(); int position = DOMUtil.getNodePosition(insertedNode); List<DOMNodeInsertedEvent> list = new ArrayList<DOMNodeInsertedEvent>(); buildFragmentInsertedEvents(DOMUtil.computeNodeXPath(document, target), insertedNode, position, list); for (DOMNodeInsertedEvent aList : list) { dispatchEvent(aList); } } else if (DOM_NODE_REMOVED.equals(type)) { Node target = (Node) event.getTarget(); dispatchEvent(new DOMNodeRemovedEvent(DOMUtil.computeNodeXPath(document, target))); } else if (DOM_ATTR_MODIFIED.equals(type)) { Node target = (Node) event.getTarget(); dispatchEvent(new DOMAttrModifiedEvent(DOMUtil.computeNodeXPath(document, target), event.getAttrName(), event.getAttrChange(), event.getNewValue())); } else { System.err.println("!Unsupported event type " + type); } } private static void buildFragmentInsertedEvents(String baseXPath, Node node, int position, List<DOMNodeInsertedEvent> list) { if (node instanceof Text) { list.add(new DOMNodeInsertedEvent(baseXPath, "#text" + ((Text) node).getData(), position)); } else if (node instanceof Element) { list.add(new DOMNodeInsertedEvent(baseXPath, DOMUtil.getElementOuterNoChildren((Element) node), position)); if (node.hasChildNodes()) { baseXPath += DOMUtil.computeNodeXPath(node.getParentNode(), node); node = node.getFirstChild(); position = 0; while (node != null) { buildFragmentInsertedEvents(baseXPath, node, position, list); node = node.getNextSibling(); ++position; } } } else { System.err.println("!Unsupported node type"); } } private void dispatchEvent(DOMMutationEvent event) { if (dispatchLevel != 0) { if (!event.equals(currentEvent)) { System.err.println("Events don't match"); System.err.println("original " + currentEvent); System.err.println("generated " + event); } return; } IDOMMutationListener[] list = listeners.toArray(new IDOMMutationListener[listeners.size()]); for (IDOMMutationListener listener : list) { listener.handleEvent(event); } } public void addMutationListener(IDOMMutationListener listener) { if (!listeners.contains(listener)) { listeners.add(listener); } } public void removeMutationListener(IDOMMutationListener listener) { listeners.remove(listener); } /** * External mutation event handler */ public void handleEvent(DOMMutationEvent event) { currentEvent = event; ++dispatchLevel; try { Node target = DOMUtil.findNodeByXPath(document, event.getTarget()); if (target == null) { System.err.println("!Null target for " + event.getTarget()); return; } if (event instanceof DOMNodeInsertedEvent) { if (!(target instanceof Element) && !(target instanceof Document)) { System.err.println("!Unsupported target node type"); return; } int position = ((DOMNodeInsertedEvent) event).getPosition(); String fragment = ((DOMNodeInsertedEvent) event).getFragment(); Node docFragment; if (fragment.startsWith("#text")) { docFragment = document.createTextNode(fragment.substring(5)); } else { docFragment = domSupport.createContextualFragment(target, fragment); } Node nodeBefore = DOMUtil.getNodeAtPosition(target, position); if (nodeBefore != null) { target.insertBefore(docFragment, nodeBefore); } else { target.appendChild(docFragment); } } else if (event instanceof DOMNodeRemovedEvent) { if (!(target instanceof Element) && !(target instanceof CharacterData) && !(target instanceof ProcessingInstruction)) { System.err.println("!Unsupported target node type"); return; } target.getParentNode().removeChild(target); } else if (event instanceof DOMAttrModifiedEvent) { if (!(target instanceof Element)) { System.err.println("!Unsupported target node type"); return; } String attrName = ((DOMAttrModifiedEvent) event).getAttrName(); short attrChange = ((DOMAttrModifiedEvent) event).getAttrChange(); String newValue = ((DOMAttrModifiedEvent) event).getNewValue(); NamedNodeMap attrs = target.getAttributes(); if (attrChange == MutationEvent.REMOVAL) { attrs.removeNamedItem(attrName); } else { Attr attr = (Attr) attrs.getNamedItem(attrName); if (attr != null) { attr.setValue(newValue); } else { attr = document.createAttribute(attrName); attr.setValue(newValue); attrs.setNamedItem(attr); } } } else if (event instanceof DOMCharacterDataModifiedEvent) { String data = ((DOMCharacterDataModifiedEvent) event).getNewValue(); if (target instanceof CharacterData) { ((CharacterData) target).setData(data); } else if (target instanceof ProcessingInstruction) { ((ProcessingInstruction) target).setData(data); } else { System.err.println("!Unsupported target node type"); } } else { System.err.println("!Unsupported event " + event); } } finally { --dispatchLevel; } } }