001/* 002 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Max Stepanov 018 * 019 * $Id$ 020 */ 021 022package org.nuxeo.ecm.platform.domsync.core; 023 024import java.util.ArrayList; 025import java.util.List; 026 027import org.nuxeo.ecm.platform.domsync.core.events.DOMAttrModifiedEvent; 028import org.nuxeo.ecm.platform.domsync.core.events.DOMCharacterDataModifiedEvent; 029import org.nuxeo.ecm.platform.domsync.core.events.DOMMutationEvent; 030import org.nuxeo.ecm.platform.domsync.core.events.DOMNodeInsertedEvent; 031import org.nuxeo.ecm.platform.domsync.core.events.DOMNodeRemovedEvent; 032import org.w3c.dom.Attr; 033import org.w3c.dom.CharacterData; 034import org.w3c.dom.Document; 035import org.w3c.dom.Element; 036import org.w3c.dom.NamedNodeMap; 037import org.w3c.dom.Node; 038import org.w3c.dom.ProcessingInstruction; 039import org.w3c.dom.Text; 040import org.w3c.dom.events.Event; 041import org.w3c.dom.events.EventListener; 042import org.w3c.dom.events.MutationEvent; 043 044/** 045 * @author Max Stepanov 046 */ 047public class DOMSynchronizer implements EventListener, IDOMMutationListener { 048 049 private static final String DOM_SUBTREE_MODIFIED = "DOMSubtreeModified"; 050 051 private static final String DOM_NODE_INSERTED = "DOMNodeInserted"; 052 053 private static final String DOM_NODE_REMOVED = "DOMNodeRemoved"; 054 055 private static final String DOM_NODE_REMOVED_FROM_DOCUMENT = "DOMNodeRemovedFromDocument"; 056 057 private static final String DOM_NODE_INSERTED_INTO_DOCUMENT = "DOMNodeInsertedIntoDocument"; 058 059 private static final String DOM_ATTR_MODIFIED = "DOMAttrModified"; 060 061 private static final String DOM_CHARACTER_DATA_MODIFIED = "DOMCharacterDataModified"; 062 063 private final Document document; 064 065 private final IDOMSupport domSupport; 066 067 private int dispatchLevel; 068 069 private DOMMutationEvent currentEvent; /* for debug/test purposes */ 070 071 private final List<IDOMMutationListener> listeners = new ArrayList<IDOMMutationListener>(); 072 073 public DOMSynchronizer(Document document, IDOMSupport domSupport) { 074 this.document = document; 075 this.domSupport = domSupport; 076 } 077 078 /** 079 * Document event handler 080 */ 081 public void handleEvent(Event evt) { 082 if (!(evt instanceof MutationEvent)) { 083 return; 084 } 085 MutationEvent event = (MutationEvent) evt; 086 String type = event.getType(); 087 088 if (DOM_CHARACTER_DATA_MODIFIED.equals(type)) { 089 Node target = (Node) event.getTarget(); 090 String newValue = event.getNewValue(); 091 dispatchEvent(new DOMCharacterDataModifiedEvent(DOMUtil.computeNodeXPath(document, target), newValue)); 092 093 } else if (DOM_NODE_INSERTED.equals(type)) { 094 Node target = event.getRelatedNode(); 095 Node insertedNode = (Node) event.getTarget(); 096 int position = DOMUtil.getNodePosition(insertedNode); 097 List<DOMNodeInsertedEvent> list = new ArrayList<DOMNodeInsertedEvent>(); 098 buildFragmentInsertedEvents(DOMUtil.computeNodeXPath(document, target), insertedNode, position, list); 099 for (DOMNodeInsertedEvent aList : list) { 100 dispatchEvent(aList); 101 } 102 103 } else if (DOM_NODE_REMOVED.equals(type)) { 104 Node target = (Node) event.getTarget(); 105 dispatchEvent(new DOMNodeRemovedEvent(DOMUtil.computeNodeXPath(document, target))); 106 107 } else if (DOM_ATTR_MODIFIED.equals(type)) { 108 Node target = (Node) event.getTarget(); 109 dispatchEvent(new DOMAttrModifiedEvent(DOMUtil.computeNodeXPath(document, target), event.getAttrName(), 110 event.getAttrChange(), event.getNewValue())); 111 112 } else { 113 System.err.println("!Unsupported event type " + type); 114 } 115 } 116 117 private static void buildFragmentInsertedEvents(String baseXPath, Node node, int position, 118 List<DOMNodeInsertedEvent> list) { 119 if (node instanceof Text) { 120 list.add(new DOMNodeInsertedEvent(baseXPath, "#text" + ((Text) node).getData(), position)); 121 } else if (node instanceof Element) { 122 list.add(new DOMNodeInsertedEvent(baseXPath, DOMUtil.getElementOuterNoChildren((Element) node), position)); 123 if (node.hasChildNodes()) { 124 baseXPath += DOMUtil.computeNodeXPath(node.getParentNode(), node); 125 node = node.getFirstChild(); 126 position = 0; 127 while (node != null) { 128 buildFragmentInsertedEvents(baseXPath, node, position, list); 129 node = node.getNextSibling(); 130 ++position; 131 } 132 } 133 } else { 134 System.err.println("!Unsupported node type"); 135 } 136 } 137 138 private void dispatchEvent(DOMMutationEvent event) { 139 if (dispatchLevel != 0) { 140 if (!event.equals(currentEvent)) { 141 System.err.println("Events don't match"); 142 System.err.println("original " + currentEvent); 143 System.err.println("generated " + event); 144 } 145 return; 146 } 147 IDOMMutationListener[] list = listeners.toArray(new IDOMMutationListener[listeners.size()]); 148 for (IDOMMutationListener listener : list) { 149 listener.handleEvent(event); 150 } 151 } 152 153 public void addMutationListener(IDOMMutationListener listener) { 154 if (!listeners.contains(listener)) { 155 listeners.add(listener); 156 } 157 } 158 159 public void removeMutationListener(IDOMMutationListener listener) { 160 listeners.remove(listener); 161 } 162 163 /** 164 * External mutation event handler 165 */ 166 public void handleEvent(DOMMutationEvent event) { 167 currentEvent = event; 168 ++dispatchLevel; 169 try { 170 Node target = DOMUtil.findNodeByXPath(document, event.getTarget()); 171 if (target == null) { 172 System.err.println("!Null target for " + event.getTarget()); 173 return; 174 } 175 176 if (event instanceof DOMNodeInsertedEvent) { 177 if (!(target instanceof Element) && !(target instanceof Document)) { 178 System.err.println("!Unsupported target node type"); 179 return; 180 } 181 int position = ((DOMNodeInsertedEvent) event).getPosition(); 182 String fragment = ((DOMNodeInsertedEvent) event).getFragment(); 183 Node docFragment; 184 if (fragment.startsWith("#text")) { 185 docFragment = document.createTextNode(fragment.substring(5)); 186 } else { 187 docFragment = domSupport.createContextualFragment(target, fragment); 188 } 189 Node nodeBefore = DOMUtil.getNodeAtPosition(target, position); 190 if (nodeBefore != null) { 191 target.insertBefore(docFragment, nodeBefore); 192 } else { 193 target.appendChild(docFragment); 194 } 195 196 } else if (event instanceof DOMNodeRemovedEvent) { 197 if (!(target instanceof Element) && !(target instanceof CharacterData) 198 && !(target instanceof ProcessingInstruction)) { 199 System.err.println("!Unsupported target node type"); 200 return; 201 } 202 target.getParentNode().removeChild(target); 203 204 } else if (event instanceof DOMAttrModifiedEvent) { 205 if (!(target instanceof Element)) { 206 System.err.println("!Unsupported target node type"); 207 return; 208 } 209 String attrName = ((DOMAttrModifiedEvent) event).getAttrName(); 210 short attrChange = ((DOMAttrModifiedEvent) event).getAttrChange(); 211 String newValue = ((DOMAttrModifiedEvent) event).getNewValue(); 212 NamedNodeMap attrs = target.getAttributes(); 213 if (attrChange == MutationEvent.REMOVAL) { 214 attrs.removeNamedItem(attrName); 215 } else { 216 Attr attr = (Attr) attrs.getNamedItem(attrName); 217 if (attr != null) { 218 attr.setValue(newValue); 219 } else { 220 attr = document.createAttribute(attrName); 221 attr.setValue(newValue); 222 attrs.setNamedItem(attr); 223 } 224 } 225 226 } else if (event instanceof DOMCharacterDataModifiedEvent) { 227 String data = ((DOMCharacterDataModifiedEvent) event).getNewValue(); 228 if (target instanceof CharacterData) { 229 ((CharacterData) target).setData(data); 230 } else if (target instanceof ProcessingInstruction) { 231 ((ProcessingInstruction) target).setData(data); 232 } else { 233 System.err.println("!Unsupported target node type"); 234 } 235 236 } else { 237 System.err.println("!Unsupported event " + event); 238 } 239 } finally { 240 --dispatchLevel; 241 } 242 } 243 244}