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