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}