2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* The Original Software is NetBeans. The Initial Developer of the Original
27
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28
* Microsystems, Inc. All Rights Reserved.
30
* If you wish your version of this file to be governed by only the CDDL
31
* or only the GPL Version 2, indicate your decision by adding
32
* "[Contributor] elects to include this software in this distribution
33
* under the [CDDL or GPL Version 2] license." If you do not indicate a
34
* single choice of license, a recipient has the option to distribute
35
* your version of this file under either the CDDL, the GPL Version 2 or
36
* to extend the choice of license to its licensees as provided above.
37
* However, if you add GPL Version 2 code and therefore, elected the GPL
38
* Version 2 license, then the option applies only if the new code is
39
* made subject to such option by the copyright holder.
41
package org.netbeans.modules.xml.xdm;
43
import java.beans.PropertyChangeListener;
44
import java.beans.PropertyChangeSupport;
45
import java.io.IOException;
46
import java.util.ArrayList;
47
import java.util.Collection;
48
import java.util.List;
50
import java.util.logging.Level;
51
import java.util.logging.Logger;
52
import javax.swing.event.UndoableEditEvent;
53
import javax.swing.event.UndoableEditListener;
54
import javax.swing.text.BadLocationException;
55
import javax.swing.undo.CompoundEdit;
56
import javax.swing.undo.UndoableEdit;
57
import javax.swing.undo.UndoableEditSupport;
58
import javax.xml.XMLConstants;
59
import javax.xml.namespace.QName;
60
import org.netbeans.editor.BaseDocument;
61
import org.netbeans.modules.xml.xam.ModelSource;
62
import org.netbeans.modules.xml.xam.dom.ElementIdentity;
63
import org.netbeans.modules.xml.xdm.diff.DiffFinder;
64
import org.netbeans.modules.xml.xdm.diff.XDMTreeDiff;
65
import org.netbeans.modules.xml.xdm.diff.Difference;
66
import org.netbeans.modules.xml.xdm.diff.Add;
67
import org.netbeans.modules.xml.xdm.diff.Change;
68
import org.netbeans.modules.xml.xdm.diff.Delete;
69
import org.netbeans.modules.xml.xdm.diff.DefaultElementIdentity;
70
import org.netbeans.modules.xml.xdm.diff.MergeDiff;
71
import org.netbeans.modules.xml.xdm.diff.NodeIdDiffFinder;
72
import org.netbeans.modules.xml.xdm.diff.NodeInfo;
73
import org.netbeans.modules.xml.xdm.diff.SyncPreparation;
74
import org.netbeans.modules.xml.xdm.nodes.Attribute;
75
import org.netbeans.modules.xml.xdm.nodes.Document;
76
import org.netbeans.modules.xml.xdm.nodes.Element;
77
import org.netbeans.modules.xml.xdm.nodes.Node;
78
import org.netbeans.modules.xml.xdm.nodes.NodeImpl;
79
import org.netbeans.modules.xml.xdm.nodes.Text;
80
import org.netbeans.modules.xml.xdm.nodes.Token;
81
import org.netbeans.modules.xml.xdm.nodes.XMLSyntaxParser;
82
import org.netbeans.modules.xml.xdm.visitor.FindVisitor;
83
import org.netbeans.modules.xml.xdm.visitor.FlushVisitor;
84
import org.netbeans.modules.xml.xdm.visitor.NamespaceRefactorVisitor;
85
import org.netbeans.modules.xml.xdm.visitor.PathFromRootVisitor;
86
import org.netbeans.modules.xml.xdm.visitor.Utils;
87
import org.w3c.dom.NamedNodeMap;
88
import org.w3c.dom.NodeList;
92
public class XDMModel {
95
* @param ms requires an instance of org.netbeans.editor.BaseDocument to be
96
* available in the lookup;
98
public XDMModel(ModelSource ms) {
100
assert getSwingDocument() != null;
101
ues = new UndoableEditSupport(this);
102
pcs = new PropertyChangeSupport(this);
103
parser = new XMLSyntaxParser();
104
setStatus(Status.UNPARSED);
106
//establish a default element identification mechanism
107
//domain models should override this by invoking "setElementIdentity"
108
ElementIdentity eID = createElementIdentity();
109
setElementIdentity(eID);
112
public String getIndentation() {
113
return currentIndent;
116
public void setIndentation(String indent) {
117
currentIndent = indent;
118
indentInitialized = true;
121
private void setDefaultIndentation() {
122
currentIndent = DEFAULT_INDENT;
126
* override this method if domain model wants to identify elements
127
* using a different mechanism than this default one.
130
private ElementIdentity createElementIdentity() {
131
//Establish DOM element identities
132
ElementIdentity eID = new DefaultElementIdentity();
133
//Following values are suitable for Schema and WSDL documents
134
//these default values can be reset by eID.reset() call
135
eID.addIdentifier( "id" );
136
eID.addIdentifier( "name" );
137
eID.addIdentifier( "ref" );
142
* This api flushes the changes made to the model to the underlying document.
144
public synchronized void flush() {
145
flushDocument(getDocument());
149
* This api syncs the model with the underlying swing document.
150
* If its the first time sync is called, swing document is parsed and model
151
* is initialized. Otherwise the changes made to swing document are applied
152
* to the model using DiffMerger.
154
public synchronized void sync() throws IOException {
155
if (preparation == null) {
161
public synchronized void prepareSync() {
162
Status oldStat = getStatus();
164
setStatus(Status.PARSING); // to access in case old broken tree
165
Document newDoc = parser.parse(getSwingDocument());
166
Document oldDoc = getCurrentDocument();
167
if (oldDoc == null) {
168
preparation = new SyncPreparation(newDoc);
170
newDoc.assignNodeIdRecursively();
171
XDMTreeDiff treeDiff = new XDMTreeDiff(eID);
172
List<Difference> preparedDiffs = treeDiff.performDiff( this, newDoc );
173
preparation = new SyncPreparation(oldDoc, preparedDiffs);
175
} catch (BadLocationException ble) {
176
preparation = new SyncPreparation(ble);
177
} catch (IllegalArgumentException iae) {
178
preparation = new SyncPreparation(iae);
179
} catch (IOException ioe) {
180
preparation = new SyncPreparation(ioe);
182
setStatus(oldStat); // we are not mutating yet, so alway restore
186
private SyncPreparation preparation = null;
188
private void finishSync() throws IOException {
189
if (preparation == null) {
190
return; // unprepared or other thread has stealth the sync
193
if (preparation.hasErrors()) {
194
IOException error = preparation.getError();
196
setStatus(Status.BROKEN);
200
Status savedStatus = getStatus();
201
setStatus(Status.PARSING);
202
Document oldDoc = getCurrentDocument();
204
if (preparation.getNewDocument() != null) {
205
Document newDoc = preparation.getNewDocument();
206
newDoc.addedToTree(this);
209
assert preparation.getOldDocument() != null : "Invalid preparation oldDoc is null";
210
if (oldDoc != preparation.getOldDocument()) {
211
// other thread has completed the sync before me
212
setStatus(savedStatus);
215
List<Difference> diffs = preparation.getDifferences();
217
//diffs = DiffFinder.filterWhitespace(diffs);
218
fireDiffEvents(diffs);
219
if (getCurrentDocument() != oldDoc) {
220
fireUndoableEditEvent(getCurrentDocument(), oldDoc);
223
setStatus(Status.STABLE);
224
} catch (IllegalArgumentException iae) {
225
if (getStatus() != Status.STABLE) {
226
IOException ioe = new IOException();
230
// XAM will review the mutation and veto by it wrapped IOException
231
if (iae.getCause() instanceof IOException) {
232
setStatus(Status.BROKEN);
233
throw (IOException) iae.getCause();
239
if(getStatus() != Status.STABLE) {
240
setStatus(Status.BROKEN);
247
public void mergeDiff(List<Difference> diffs) {
248
setStatus(Status.PARSING);
249
new MergeDiff().merge(this, diffs);
250
// exception in event firing should not put tree out-of-sync with buffer
251
setStatus(Status.STABLE);
254
private void fireDiffEvents(final List<Difference> deList) {
255
for ( Difference de:deList ) {
256
NodeInfo.NodeType nodeType = de.getNodeType();
257
// if ( nodeType == NodeInfo.NodeType.WHITE_SPACE ) continue;//skip if WS
258
if ( de instanceof Add ) {
259
NodeInfo newNodeInfo = ((Add)de).getNewNodeInfo();
260
assert newNodeInfo != null;
261
pcs.firePropertyChange( PROP_ADDED, null, newNodeInfo );
262
} else if ( de instanceof Delete ) {
263
NodeInfo OldNodeInfo = ((Delete)de).getOldNodeInfo();
264
assert OldNodeInfo != null;
265
pcs.firePropertyChange( PROP_DELETED, OldNodeInfo, null );
266
} else if ( de instanceof Change ) {
267
NodeInfo oldNodeInfo = ((Change)de).getOldNodeInfo();
268
assert oldNodeInfo != null;
270
NodeInfo newNodeInfo = ((Change)de).getNewNodeInfo();
271
assert newNodeInfo != null;
273
//fire delete and add events for position change of element/text
274
if ( ((Change)de).isPositionChanged() ) {
275
pcs.firePropertyChange( PROP_DELETED, oldNodeInfo, null );
276
pcs.firePropertyChange( PROP_ADDED, null, newNodeInfo );
277
} else if ( de.getNodeType() == NodeInfo.NodeType.TEXT ) { //text change only
278
pcs.firePropertyChange( PROP_MODIFIED, oldNodeInfo, newNodeInfo );
279
} else if ( de.getNodeType() == NodeInfo.NodeType.ELEMENT ) {
280
List<Node> path1 = new ArrayList<Node>(oldNodeInfo.getOriginalAncestors());
281
path1.add(0, oldNodeInfo.getNode());
282
List<Node> path2 = new ArrayList<Node>(newNodeInfo.getNewAncestors());
283
path2.add(0, newNodeInfo.getNode());
284
//fire attribute change events
285
List<Change.AttributeDiff> attrChanges = ((Change)de).getAttrChanges();
286
for (Change.AttributeDiff attrDiff:attrChanges) {
287
Node oldAttr = attrDiff.getOldAttribute();
288
Node newAttr = attrDiff.getNewAttribute();
289
if(attrDiff instanceof Change.AttributeAdd) {
290
assert newAttr != null;
291
pcs.firePropertyChange( PROP_ADDED, null,
292
new NodeInfo(newAttr, -1, path1, path2));
293
} else if(attrDiff instanceof Change.AttributeDelete) {
294
assert oldAttr != null;
295
pcs.firePropertyChange(
296
PROP_DELETED, new NodeInfo(oldAttr, -1, path1, path2), null );
297
} else if(attrDiff instanceof Change.AttributeChange) {
298
assert oldAttr != null;
299
assert newAttr != null;
300
NodeInfo old = new NodeInfo(oldAttr, -1, path1, path2);
301
NodeInfo now = new NodeInfo(newAttr, -1, path1, path2);
302
pcs.firePropertyChange( PROP_MODIFIED, old, now);
310
private interface Updater {
311
void update(Node parent, Node oldNode, Node newNode);
314
private List<Node> getPathToRoot(Node node, Document root) {
315
PathFromRootVisitor pathVisitor = new PathFromRootVisitor();
316
List<Node> path = pathVisitor.findPath(root, node);
317
if (path==null || path.isEmpty()) {
318
throw new IllegalArgumentException("old node must be in the tree");
323
private static String classifyMutationType(Node oldNode, Node newNode) {
324
if (newNode == null && oldNode == null ||
325
newNode != null && oldNode != null) {
326
return PROP_MODIFIED;
327
} else if (newNode != null) {
334
private enum MutationType { CHILDREN, ATTRIBUTE, BOTH }
336
private List<Node> mutate(Node parent, Node oldNode, Node newNode, Updater updater) {
337
return mutate(parent, oldNode, newNode, updater, null);
340
private List<Node> mutate(Node parent, Node oldNode, Node newNode, Updater updater, MutationType type) {
341
checkStableOrParsingState();
342
if (newNode != null) checkNodeInTree(newNode);
344
Document currentDocument = getDocument();
345
List<Node> ancestors;
346
if (parent == null) {
347
assert(oldNode != null);
348
ancestors = getPathToRoot(oldNode, currentDocument);
349
oldNode = ancestors.remove(0);
351
if (oldNode != null) {
352
assert parent.getIndexOfChild(oldNode) > -1;
353
ancestors = getPathToRoot(oldNode, currentDocument);
354
assert oldNode.getId() == ancestors.get(0).getId();
355
oldNode = ancestors.remove(0);
356
assert parent.getId() == ancestors.get(0).getId();
358
ancestors = getPathToRoot(parent, currentDocument);
362
final Node oldParent = ancestors.remove(0);
365
if (oldNode instanceof Attribute || newNode instanceof Attribute ||
366
(oldNode == null && newNode == null)) {
367
type = MutationType.ATTRIBUTE;
368
} else if (ancestors.size() == 1) {
369
assert (oldParent instanceof Element);
370
//might have to add namespace declaration to root
371
type = MutationType.BOTH;
373
type = MutationType.CHILDREN;
378
newParent = (Node)oldParent.clone(true,true,false);
381
newParent = (Node)oldParent.clone(true,false,true);
384
newParent = (Node)oldParent.clone(true,true,true);
387
if (oldNode != null && oldNode.getNodeType() != Node.TEXT_NODE && newNode == null) { // pure remove
388
undoPrettyPrint(newParent, oldNode, oldParent);
390
updater.update(newParent, oldNode, newNode);
391
if (oldNode == null && newNode != null && newNode.getNodeType() != Node.TEXT_NODE ) { // pure add
392
doPrettyPrint(newParent, newNode, oldParent);
395
List<Node> newAncestors = updateAncestors(ancestors, newParent, oldParent);
396
if(getStatus() != Status.PARSING && newNode instanceof Element) {
397
consolidateNamespaces(newAncestors, newParent, (Element)newNode);
399
Document d = (Document) (!newAncestors.isEmpty() ?
400
newAncestors.get(newAncestors.size()-1) : newParent);
403
ancestors.add(0, oldParent);
404
newAncestors.add(0, newParent);
405
if(getStatus() != Status.PARSING) { // not merging
406
fireUndoableEditEvent(d, currentDocument);
407
String mutationType = classifyMutationType(oldNode, newNode);
408
//TODO seems missing delete/change; also, who are listening to these xdm mutation events
409
NodeInfo newNodeInfo = new NodeInfo( newNode, -1, ancestors, newAncestors );
410
pcs.firePropertyChange(mutationType, null, newNodeInfo);
415
private void consolidateNamespaces(List<Node> ancestors, Node parent, Element newNode) {
416
if (parent instanceof Document) return; // no actions if newNode is root itself
417
assert ancestors.size() > 0;
418
Element root = (Element) (ancestors.size() == 1 ?
419
parent : ancestors.get(ancestors.size()-2));
420
List<Node> parentAndAncestors = new ArrayList(ancestors);
421
parentAndAncestors.add(0, parent);
422
consolidateAttributePrefix(parentAndAncestors, newNode);
423
NamedNodeMap nnm = newNode.getAttributes();
424
for (int i=0; i<nnm.getLength(); i++) {
425
if (nnm.item(i) instanceof Attribute) {
426
Attribute attr = (Attribute) nnm.item(i);
427
consolidateNamespace(root, parentAndAncestors, newNode, attr);
431
// use parent node prefix
432
String parentPrefix = parent.getPrefix();
433
String parentNS = NodeImpl.lookupNamespace(parent, ancestors);
434
if (parentNS != null && ! parentNS.equals(XMLConstants.NULL_NS_URI)) {
435
new NamespaceRefactorVisitor(this).refactor(
436
newNode, parentNS, parentPrefix, parentAndAncestors);
440
private void consolidateAttributePrefix(List<Node> parentAndAncestors, Element newNode) {
441
NamedNodeMap nnm = newNode.getAttributes();
442
for (int i=0; i<nnm.getLength(); i++) {
443
if (! (nnm.item(i) instanceof Attribute)) continue;
444
Attribute attr = (Attribute) nnm.item(i);
445
String prefix = attr.getPrefix();
446
if (prefix != null && ! attr.isXmlnsAttribute()) {
447
String namespace = newNode.lookupNamespaceURI(prefix);
448
if (namespace == null) continue;
449
prefix = NodeImpl.lookupPrefix(namespace, parentAndAncestors);
450
if (prefix != null) {
451
attr.setPrefix(prefix);
458
* Consolidate new node top-leveled namespaces with parent's.
459
* Note: this assume #consolidateAttributePrefix has been called
461
private void consolidateNamespace(Element root, List<Node> parentAndAncestors,
462
Element newNode, Attribute attr) {
463
if (attr.isXmlnsAttribute()) {
464
String prefix = attr.getLocalName();
465
if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
466
prefix = XMLConstants.DEFAULT_NS_PREFIX;
468
String namespace = attr.getValue();
469
assert (namespace != null);
471
Node parent = parentAndAncestors.get(0);
472
String parentNS = NodeImpl.lookupNamespace(parent, parentAndAncestors);
474
String existingNS = NodeImpl.lookupNamespace(prefix, parentAndAncestors);
475
String existingPrefix = NodeImpl.lookupPrefix(namespace, parentAndAncestors);
477
// 1. prefix is free (existingNS == null) and namespace is never declared (existingPrefix == null)
478
// 2. prefix is used and for the same namespace
479
// 3. namespace is declared by different prefix
480
// 4. prefix is used and for different namespace
482
if (existingNS == null && existingPrefix == null) { // case 1.
483
newNode.removeAttributeNode(attr);
484
root.appendAttribute(attr);
485
} else if (namespace.equals(existingNS) && prefix.equals(existingPrefix)) { // case 2
486
assert prefix.equals(existingPrefix) : "prefix='"+prefix+"' existingPrefix='"+existingPrefix+"'";
487
newNode.removeAttributeNode(attr);
488
} else if (existingPrefix != null) { // case 3.
489
// skip attribute redeclaring namespace of parent element
490
// we will refactor to parent prefix after processing all xmlns-attributes
491
if (! namespace.equals(parentNS)) {
492
new NamespaceRefactorVisitor(this).refactor(
493
newNode, namespace, existingPrefix, parentAndAncestors);
495
} else { // existingNS != null && existingPrefix == null
496
// case 4 just leave prefix as overriding with different namespace
502
* This api replaces given old node with given new node.
503
* The old node passed must be in tree, the new node must not be in tree,
504
* and new node must be clone of old node.
505
* @param oldValue The old node to be replaced.
506
* @param newValue The new node.
507
* @return The new parent
509
public synchronized List<Node> modify(Node oldValue, Node newValue) {
510
if (oldValue.getId() != newValue.getId()) {
511
throw new IllegalArgumentException("newValue must be a clone of oldValue");
514
if (oldValue instanceof Document) {
515
assert newValue instanceof Document;
516
Document oldDoc = (Document) oldValue;
517
Document newDoc = (Document) newValue;
518
newDoc.addedToTree(this);
520
if (getStatus() != Status.PARSING) { // not merging
521
fireUndoableEditEvent(oldDoc, currentDocument);
522
String mutationType = classifyMutationType(oldDoc, newDoc);
523
ArrayList<Node> ancestors = new ArrayList<Node>();
524
NodeInfo oldNodeInfo = new NodeInfo( oldDoc, -1, ancestors, ancestors );
525
NodeInfo newNodeInfo = new NodeInfo( newDoc, -1, ancestors, ancestors );
526
pcs.firePropertyChange(mutationType, oldNodeInfo, newNodeInfo);
528
return new ArrayList<Node>();
531
Updater modifier = new Updater() {
532
public void update(Node newParent, Node oldNode, Node newNode) {
533
if (oldNode instanceof Attribute) {
534
((Element)newParent).replaceAttribute((Attribute)newNode,(Attribute)oldNode);
536
newParent.replaceChild(newNode, oldNode);
540
return mutate(null, oldValue, newValue, modifier);
544
* This api adds given node to given parent at given index.
545
* The added node will be part of childnodes of the parent,
546
* and its index will be the given index. If the given index
547
* is out of the parents childnodes range, the node will be
549
* @param parent The parent node to which the node is to be added.
550
* @param node The node which is to be added.
551
* @param offset The index at which the node is to be added.
552
* @return The parent node resulted by addition of this node
554
public synchronized List<Node> add(Node parent, Node node, final int offset) {
555
if (offset<0) throw new IndexOutOfBoundsException();
556
Updater adder = new Updater() {
557
public void update(Node newParent, Node oldNode, Node newNode) {
558
if (newParent instanceof Element && newNode instanceof Attribute) {
559
Element newElement = (Element)newParent;
560
if (offset>newElement.getAttributes().getLength())
561
throw new IndexOutOfBoundsException();
562
newElement.addAttribute((Attribute)newNode,offset);
564
//reset id, since adding root element
565
if(newParent instanceof Document && newNode instanceof Element)
568
if (offset>newParent.getChildNodes().getLength())
569
throw new IndexOutOfBoundsException();
570
if(offset<newParent.getChildNodes().getLength()) {
571
Node refChild = (Node)newParent.getChildNodes().item(offset);
572
newParent.insertBefore(newNode,refChild);
574
newParent.appendChild(newNode);
579
return mutate(parent, null, node, adder);
583
* This api adds given node to given parent before given ref node.
584
* The inserted node will be part of childnodes of the parent,
585
* and will appear before ref node.
586
* @param parent The parent node to which the node is to be added.
587
* @param node The node which is to be added.
588
* @param refChild The ref node (child) of parent node,
589
* before which the node is to be added.
590
* @return The parent node resulted by inserion of this node.
592
public synchronized List<Node> insertBefore(Node parent, Node node, Node refChild) {
593
final Node ref = refChild;
594
Updater updater = new Updater() {
595
public void update(Node newParent, Node oldNode, Node newNode) {
596
newParent.insertBefore(newNode, ref);
600
return mutate(parent, null, node, updater);
604
* This api adds given node to given parent at the end.
605
* The added node will be part of childnodes of the parent,
606
* and it will be the last node.
607
* @param parent The parent node to which the node is to be appended.
608
* @param node The node which is to be appended.
609
* @return The parent node resulted by addition of this node
611
public synchronized List<Node> append(Node parent, Node node) {
612
Updater appender = new Updater() {
613
public void update(Node parent, Node oldNode, Node newNode) {
614
//reset id, since adding root element
615
if(parent instanceof Document && newNode instanceof Element)
617
parent.appendChild(newNode);
620
return mutate(parent, null, node, appender);
624
* This api deletes given node from a tree.
625
* @param node The node to be deleted.
626
* @return The parent node resulted by deletion of this node.
628
public synchronized List<Node> delete(Node n) {
629
Updater remover = new Updater() {
630
public void update(Node newParent, Node oldNode, Node newNode) {
631
newParent.removeChild(oldNode);
634
return mutate(null, n, null, remover);
638
* This api changes index of the given node.
639
* @param nodes The nodes to be moved.
640
* @param indexes the new indexes of the nodes.
641
* @return The parent node resulted by deletion of this node.
643
public synchronized List<Node> reorder(Node parent, Node n, final int index) {
644
if (index < 0) throw new IndexOutOfBoundsException("index="+index);
645
Updater u = new Updater() {
646
public void update(Node newParent, Node oldNode, Node newNode) {
647
if (newParent instanceof Element && newNode instanceof Attribute) {
648
Element parent = (Element) newParent;
650
if (index > parent.getAttributes().getLength()) {
651
i = parent.getAttributes().getLength();
653
parent.reorderAttribute((Attribute) oldNode, i);
656
if (index > newParent.getChildNodes().getLength()) {
657
i = newParent.getChildNodes().getLength();
659
((NodeImpl)newParent).reorderChild(oldNode, i);
663
return mutate(parent, n, null, u);
667
* This api changes indexes of the given node children.
668
* @param nodes The nodes to be moved.
669
* @param indexes the new indexes of the nodes.
670
* @return The parent node resulted by deletion of this node.
672
public synchronized List<Node> reorderChildren(Node parent, final int[] permutation) {
673
Updater u = new Updater() {
674
public void update(Node newParent, Node oldNode, Node newNode) {
675
((NodeImpl)newParent).reorderChildren(permutation);
678
return mutate(parent, null, null, u, MutationType.CHILDREN);
682
* This api deletes given node from a given parent node.
683
* @param parent The parent node from which the node is to be deleted.
684
* @param child The node to be deleted.
685
* @return The parent node resulted by deletion of this node.
687
public synchronized List<Node> remove(final Node parent, Node child) {
688
Updater remover = new Updater() {
689
public void update(Node newParent, Node oldNode, Node newNode) {
690
assert parent.isEquivalentNode(newParent);
691
newParent.removeChild(oldNode);
694
return mutate(parent, child, null, remover);
698
* This api deletes given node from a given parent node.
699
* @param parent The parent node from which the node is to be deleted.
700
* @param toRemove collection of node to be deleted.
701
* @return The parent node resulted by deletion of this node.
703
public synchronized List<Node> removeChildNodes(final Node parent, final Collection<Node> toRemove) {
704
Updater remover = new Updater() {
705
public void update(Node newParent, Node oldNode, Node newNode) {
706
assert parent.isEquivalentNode(newParent);
707
for (Node n : toRemove) {
708
newParent.removeChild(n);
712
return mutate(parent, null, null, remover, MutationType.CHILDREN);
715
public synchronized List<Node> replaceChild(final Node parent, Node child, Node newChild) {
716
Updater updater = new Updater() {
717
public void update(Node newParent, Node oldNode, Node newNode) {
718
assert newParent.isEquivalentNode(parent);
719
newParent.replaceChild(newNode, oldNode);
722
return mutate(null, child, newChild, updater);
726
* This api sets an attribute given name and value of a given element node.
727
* If an attribute with given name already present in element, it will only
728
* set the value. Otherwise a new attribute node, with given name and value,
729
* will be appended to the attibute list of the element node.
730
* @param element The element of which the attribute to be set.
731
* @param name The name of the attribute to be set.
732
* @param value The value of the attribute to be set.
733
* @return The element resulted by setting of attribute.
735
public synchronized List<Node> setAttribute(Element element, final String name, final String value) {
736
Updater updater = new Updater() {
737
public void update(Node newParent, Node oldNode, Node newNode) {
738
((Element)newParent).setAttribute(name,value);
741
return mutate(element, null, null, updater);
745
* This api removes an attribute given name and value of a given element node.
746
* @param element The element of which the attribute to be removed.
747
* @param name The name of the attribute to be removed.
748
* @return The element resulted by removed of attribute.
750
public synchronized List<Node> removeAttribute(Element element, final String name) {
751
Updater updater = new Updater() {
752
public void update(Node newParent, Node oldNode, Node newNode) {
753
((Element)newParent).removeAttribute(name);
756
return mutate(element, null, null, updater);
759
private interface CheckIOExceptionUpdater extends Updater {
760
public IOException getError();
763
public synchronized List<Node> setXmlFragmentText(Element node, final String value) throws IOException {
764
CheckIOExceptionUpdater updater = new CheckIOExceptionUpdater() {
765
public void update(Node newParent, Node oldNode, Node newNode) {
767
((Element)newParent).setXmlFragmentText(value);
768
} catch(IOException ioe) {
772
public IOException getError() {
775
private IOException error;
777
List<Node> retPath = mutate(node, null, null, updater, MutationType.CHILDREN);
778
if (updater.getError() != null) {
779
throw updater.getError();
785
public synchronized List<Node> setTextValue(Node node, String value) {
786
Node text = (Node) currentDocument.createTextNode(value);
787
Updater updater = new Updater() {
788
public void update(Node newParent, Node oldNode, Node newNode) {
789
while(newParent.hasChildNodes()) {
790
newParent.removeChild(newParent.getLastChild());
792
newParent.appendChild(newNode);
795
return mutate(node, null, text, updater);
799
* This is utility method which updates all the ancestors in the given
800
* ancestor list of given originalNode. The list returned represents
801
* the ancestors of given modified node.
802
* @param ancestors the list of ancestors starting from parent
803
* @param modifiedNode The modified node for which the new list is to be created
804
* @param originalNode The original node which ancestors are given
805
* @return The list of new ancestors starting parent for the modified node
807
private List<Node> updateAncestors(List<Node> ancestors, Node modifiedNode, Node originalNode) {
808
assert ancestors != null && modifiedNode != null && originalNode != null;
809
List<Node> newAncestors = new ArrayList<Node>(ancestors.size());
810
Node currentModifiedNode = modifiedNode;
811
Node currentOrigNode = originalNode;
812
for(Node parentNode: ancestors) {
813
Node newParentNode = (Node)parentNode.clone(false,true,true);
814
newParentNode.replaceChild(currentModifiedNode, currentOrigNode);
815
newAncestors.add(newParentNode);
816
currentOrigNode = parentNode;
817
currentModifiedNode = newParentNode;
823
* This api returns the latest stable document in the model.
824
* @return The latest stable document in the model.
826
public synchronized Document getDocument() {
827
checkStableOrParsingState();
828
return currentDocument;
832
* This api returns the current document in the model, regardless of the state.
833
* @return The latest stable document in the model.
835
public synchronized Document getCurrentDocument() {
836
return currentDocument;
840
* Reset document to provided known version and cause events to be fired.
841
* Note caller are responsible to handle exception and decide which version
842
* to keep after exception and do proper cleanup.
844
synchronized void resetDocument(Document newDoc) {
846
fireUndoEvents = false;
847
List<Difference> diffs = new NodeIdDiffFinder().findDiff(getCurrentDocument(), newDoc);
848
List<Difference> filtered = DiffFinder.filterWhitespace(diffs);
849
//flushDocument(newDoc);
851
if ( filtered != null && !filtered.isEmpty() ) {
852
fireDiffEvents(filtered);
855
fireUndoEvents = true;
859
private void flushDocument(Document newDoc) {
861
UndoableEditListener uel = null;
862
BaseDocument d = getSwingDocument();
863
final CompoundEdit ce = new CompoundEdit();
865
FlushVisitor flushvisitor = new FlushVisitor();
866
String newXMLText = flushvisitor.flushModel(newDoc);
867
uel = new UndoableEditListener() {
868
public void undoableEditHappened(UndoableEditEvent e) {
869
ce.addEdit(e.getEdit());
872
d.addUndoableEditListener(uel);
873
Utils.replaceDocument(d, newXMLText);
874
} catch (BadLocationException ble) {
875
throw new IllegalStateException("It is possible that model source file is locked", ble);
878
d.removeUndoableEditListener(uel);
881
for (UndoableEditListener l : ues.getUndoableEditListeners()) {
882
l.undoableEditHappened(new UndoableEditEvent(this,ce));
887
public synchronized String getCurrentDocumentText() {
888
return new FlushVisitor().flushModel(getCurrentDocument());
891
private BaseDocument getSwingDocument() {
892
BaseDocument bd = (BaseDocument)
893
source.getLookup().lookup(BaseDocument.class);
897
public synchronized void setDocument(Document newDoc) {
898
currentDocument = newDoc;
902
* This returns the statuc of the model.
903
* @return the status.
906
public synchronized Status getStatus() {
911
* This api adds an undoable edit listener.
912
* @param l The undoable edit listener to be added.
914
public synchronized void addUndoableEditListener(UndoableEditListener l) {
915
ues.addUndoableEditListener(l);
919
* This api removes an undoable edit listener.
920
* @param l The undoable edit listener to be removed.
922
public synchronized void removeUndoableEditListener(UndoableEditListener l) {
923
ues.addUndoableEditListener(l);
927
* This api adds a property change listener.
928
* @param pcl The property change listener to be added.
930
public synchronized void addPropertyChangeListener(PropertyChangeListener pcl) {
931
pcs.addPropertyChangeListener(pcl);
935
* This api removes a property change listener.
936
* @param pcl The property change listener to be removed.
938
public synchronized void removePropertyChangeListener(PropertyChangeListener pcl) {
939
pcs.removePropertyChangeListener(pcl);
943
* Find the node with same id in the current tree.
945
private synchronized Node findNode(int id) {
946
FindVisitor fv = new FindVisitor();
947
return fv.find(getDocument(), id);
951
* This represents the status of the XDM Model.
952
* Status STABLE means the latest attempt to parse was successful
953
* Status BROKEN means that the latest attempt to parse was unsuccessful.
954
* Status UNPARSED means the document has not been parsed yet.
955
* Status PARSING means the document is being parsed.
957
//TODO Last Parsed status
958
public enum Status {BROKEN, STABLE, UNPARSED, PARSING;}
960
private void fireUndoableEditEvent(Document newDoc, Document oldDoc) {
961
if (fireUndoEvents) {
962
assert newDoc != oldDoc;
963
UndoableEdit ee = new XDMModelUndoableEdit(oldDoc, newDoc, this);
964
UndoableEditEvent ue = new UndoableEditEvent(this, ee);
965
for (UndoableEditListener l:ues.getUndoableEditListeners()) {
966
l.undoableEditHappened(ue);
971
private void checkNodeInTree(Node n) {
973
throw new IllegalArgumentException("newValue must not have been added to model"); // NOI18N
977
private void checkStableState() {
978
if (getStatus() != Status.STABLE ) {
979
throw new IllegalStateException("flush can only be called from STABLE STATE"); //NOI18N
983
private void checkStableOrParsingState() {
984
if (getStatus() != Status.STABLE && getStatus() != Status.PARSING) {
985
throw new IllegalStateException("The model is not initialized or is broken."); //NOI18N
989
private void setStatus(Status s) {
994
* This api keeps track of the nodes created in this model.
995
* @return the id of the next node to be created.
997
public int getNextNodeId() {
998
int nodeId = nodeCount;
1006
private void resetIdMeter() {
1010
private boolean isPretty() {
1014
public void setPretty(boolean print) {
1018
private void doPrettyPrint(Node newParent, Node newNode, Node oldParent) {
1019
if ((getStatus() != Status.PARSING) && isPretty()) {
1020
if(isSimpleContent(newParent)) {//skip if simpleContent
1022
* <test name="test1">A new text node</test>
1026
if(!indentInitialized)
1027
initializeIndent(oldParent);
1028
String parentIndent = calculateNodeIndent(oldParent);
1029
if(!isPretty(newParent, newNode)) {//skip if already pretty
1031
if(oldParent.getChildNodes().getLength() == 0) {//old parent did not have prettyprint before
1035
* <test name="test1"></test>
1039
* <test name="test1">
1043
newParent.insertBefore(createPrettyText(
1044
parentIndent+getIndentation()), newNode);
1047
int index = ((NodeImpl)newParent).getIndexOfChild(newNode);
1049
Node oldText = (Node)newParent.getChildNodes().item(index-1);
1050
if(checkPrettyText(oldText)) {
1054
* <test name="test1">
1062
* <test name="test1">
1068
Text newText = createPrettyText(
1069
parentIndent+getIndentation());
1070
newParent.replaceChild(newText, oldText);
1075
* <test name="test1">
1077
* <b name="b1"><c name="c1">
1082
* <test name="test1">
1088
newParent.insertBefore(createPrettyText(
1089
parentIndent+getIndentation()), newNode);
1094
if((index+offset) < newParent.getChildNodes().getLength())
1095
ref = (Node)newParent.getChildNodes().item((index+offset));
1097
if(!checkPrettyText(ref)) {
1101
* <test name="test1">
1104
* <c name="c1"><d name="d1">
1109
* <test name="test1">
1116
newParent.insertBefore(createPrettyText(
1117
parentIndent+getIndentation()), ref);
1123
* <test name="test1">
1126
* <c name="c1"></test>
1130
* <test name="test1">
1136
newParent.appendChild(createPrettyText(parentIndent));
1140
//recurse pretty print
1141
doPrettyPrintRecursive(newNode, parentIndent, newParent);//for children of node
1146
* initialized only once
1149
private void initializeIndent(final Node n) {
1150
String parentIndent = calculateNodeIndent(n);
1151
List<Node> pathToRoot = new PathFromRootVisitor().findPath(getDocument(), n);
1152
if(parentIndent.length() > 0 && pathToRoot.size()-2 > 0) {
1153
//exclude Document and the root from path for indent step calculation
1154
double step = Math.floor(parentIndent.length() / (double) (pathToRoot.size()-2));
1155
StringBuffer sb = new StringBuffer();
1156
for(int i=0;i<step;i++)
1158
String indentString = sb.toString();
1159
if(indentString.length() > 0)
1160
setIndentation(indentString);
1162
setDefaultIndentation();
1164
setDefaultIndentation();
1167
private String calculateNodeIndent(final Node n) {
1169
Node parent = (Node) n.getParentNode();
1170
if(parent != null) {
1171
int index = parent.getIndexOfChild(n);
1173
Node txt = (Node) parent.getChildNodes().item(index-1);
1174
if(checkPrettyText(txt)) {
1175
String wsValue = ((NodeImpl)txt).getTokens().get(0).getValue();
1176
int ndx = wsValue.lastIndexOf("\n");
1177
if(ndx != -1 && (ndx+1) < wsValue.length())
1178
indent = wsValue.substring(ndx+1);
1185
private void doPrettyPrintRecursive(Node n, String indent, Node parent) {
1186
if ((getStatus() != Status.PARSING) && isPretty()) {
1187
if(isSimpleContent(n))
1188
return; //skip if simpleContent
1189
else if(n instanceof Element && isPretty(n)) {//adjust for pretty length difference
1190
fixPrettyForCopiedNode(n, indent, parent);
1192
List<Node> childList = new ArrayList<Node>();
1193
List<Node> visitList = new ArrayList<Node>();
1194
NodeList childs = n.getChildNodes();
1195
for(int i=0;i<childs.getLength();i++) {
1196
childList.add((Node)childs.item(i));
1197
if(childs.item(i) instanceof Element)
1198
visitList.add((Node)childs.item(i));
1200
String parentIndent = indent+getIndentation();
1201
if(childList.size() > 0)
1202
n.appendChild(createPrettyText(parentIndent));
1203
String childIndent = parentIndent+getIndentation();
1204
for(int i=childList.size()-1;i>=0;i--) {
1205
Node ref = (Node)childList.get(i);
1206
Text postText = createPrettyText(childIndent);
1207
n.insertBefore(postText, ref);
1209
childList.clear(); //no need to keep it beyond here
1210
for(int i=0;i<visitList.size();i++) {
1211
doPrettyPrintRecursive((Node)visitList.get((i)), parentIndent, n);
1213
visitList.clear(); //no need to keep it beyond here
1219
* This function will fix the pretty text of nodes that are cut or copied and
1220
* pasted to xdm tree
1222
private void fixPrettyForCopiedNode(Node n, String indent, Node parent) {
1223
NodeList childs = n.getChildNodes();
1224
if(childs.getLength() == 0)
1226
Text nlastChild = (Text)childs.item(childs.getLength()-1);
1227
String lc = ((NodeImpl)nlastChild).getTokens().get(0).getValue();
1228
NodeImpl pfirstChild = (NodeImpl)parent.getChildNodes().item(0);
1229
String fc = pfirstChild.getTokens().get(0).getValue();
1231
if(fc.length() == lc.length()) {//return if already pretty
1234
String parentIndent = indent+getIndentation();
1235
String childIndent = parentIndent+getIndentation();
1236
List<Node> childList = new ArrayList<Node>();
1237
for(int i=0;i<childs.getLength();i++) {
1238
childList.add((Node)childs.item(i));
1240
for(int i=0;i<childList.size();i++) {
1241
Node txt = (Node)n.getChildNodes().item(i);
1242
if(checkPrettyText(txt)) {
1243
String newIndent = childIndent+getIndentation();
1245
newIndent = childIndent;
1246
} else if(i == childList.size()-1) {
1247
newIndent = parentIndent;
1249
n.replaceChild(createPrettyText(newIndent), txt);
1252
for(int i=0;i<childList.size();i++) {
1253
fixPrettyForCopiedNode((Node)n.getChildNodes().item(i),
1256
childList.clear(); //no need to keep it beyond here
1260
private Text createPrettyText(String indent) {
1261
String textChars = "\n"+indent;
1262
Text txt = (Text)this.getDocument().createTextNode(textChars);
1266
private void undoPrettyPrint(Node newParent, Node oldNode, Node oldParent) {
1267
if ((getStatus() != Status.PARSING) && isPretty()) {
1268
String parentIndent = calculateNodeIndent(oldParent);
1269
int piLength = parentIndent != null ? parentIndent.length() : 0;
1270
int index = ((NodeImpl)oldParent).getIndexOfChild(oldNode);
1271
Node txtBefore = null;
1272
if(index > 0) {//remove pre pretty print node
1273
txtBefore = (Node)oldParent.getChildNodes().item(index-1);
1274
if(checkPrettyText(txtBefore) &&
1275
piLength <= getLength((Text) txtBefore)) {
1279
* <test name="test1">
1287
* <test name="test1">
1289
* <b name="b1"><c name="c1">
1292
newParent.removeChild(txtBefore);
1295
if(newParent.getChildNodes().getLength() == 2 &&
1296
index+1 < oldParent.getChildNodes().getLength()) {//remove post pretty print node
1297
Node txtAfter = (Node)oldParent.getChildNodes().item(index+1);
1298
if(checkPrettyText(txtAfter) &&
1299
piLength <= getLength((Text) txtAfter)) {
1303
* <test name="test1">
1305
* <b name="b1"><c name="c1">
1310
* <test name="test1">
1312
* <b name="b1"><c name="c1"></test>
1314
newParent.removeChild(txtAfter);
1320
private int getLength(Text n) {
1322
for(Token token:((NodeImpl)n).getTokens())
1323
len += token.getValue().length();
1327
private boolean checkPrettyText(Node txt) {
1328
if (txt instanceof Text) {
1329
if ((((NodeImpl)txt).getTokens().size() == 1) &&
1330
isWhitespaceOnly(((NodeImpl)txt).getTokens().get(0).getValue())) {
1337
private boolean isPossibleWhiteSpace(String text) {
1338
return text.length() > 0 &&
1339
Character.isWhitespace(text.charAt(0)) &&
1340
Character.isWhitespace(text.charAt(text.length()-1));
1343
private boolean isWhitespaceOnly(String tn) {
1344
return isPossibleWhiteSpace(tn) &&
1345
tn.trim().length() == 0;
1348
public ElementIdentity getElementIdentity() {
1354
public void setElementIdentity(ElementIdentity eID) {
1358
private boolean isSimpleContent(Node newParent) {
1359
NodeList childs = newParent.getChildNodes();
1360
for(int i=0;i<childs.getLength();i++)
1361
if(!(childs.item(i) instanceof Text))
1366
private boolean isPretty(Node newParent) {
1367
return isPretty(newParent, null);
1370
private boolean isPretty(Node newParent, Node newNode) {
1371
boolean parentPretty = false;
1372
NodeList childs = newParent.getChildNodes();
1373
int len = childs.getLength();
1376
* <test name="test1"></test> parentPretty = true
1378
* <test name="test1"> parentPretty = true
1381
* <test name="test1"> parentPretty = true
1385
* <test name="test1"><c name="c1"> parentPretty = false
1389
parentPretty = true;
1390
else if(len == 1 && childs.item(0) instanceof Text)
1391
parentPretty = true;
1393
checkPrettyText((Node) childs.item(0)) && checkPrettyText((Node) childs.item(len-1)))
1394
parentPretty = true;
1399
if(newNode != null) {
1400
//now check newNode pretty
1401
Node preText = null;
1402
Node postText = null;
1403
int index = ((NodeImpl)newParent).getIndexOfChild(newNode);
1405
preText = (Node)newParent.getChildNodes().item(index-1);
1406
if((index+1) < newParent.getChildNodes().getLength())
1407
postText = (Node)newParent.getChildNodes().item((index+1));
1410
* <test name="test1">
1413
* <c name="c1"/> 'c' pretty = true
1416
* <test name="test1">
1418
* <b name="b1"/><c name="c1"/> 'c' pretty = false
1422
if(checkPrettyText(preText) && checkPrettyText(postText))
1425
return parentPretty;
1431
* Set/get mapping of QName-valued attributes by element.
1432
* Key of the mapping is QName of the element.
1433
* Value of the mapping is list QName's of the attributes.
1434
* If the mapping is set, it will be used to identify the
1435
* attribute values affected by namespace prefix refactoring
1436
* during namespace consolidation. If not set, namespace consolidation
1437
* would skip prefix rename refactoring case.
1439
public void setQNameValuedAttributes(Map<QName,List<QName>> attrsByElement) {
1440
qnameValuedAttributesByElementMap = attrsByElement;
1442
public Map<QName,List<QName>> getQNameValuedAttributes() {
1443
return qnameValuedAttributesByElementMap;
1447
* The xml syntax parser
1449
private XMLSyntaxParser parser;
1452
* The current stable document represented by the model
1454
private Document currentDocument;
1457
* Property change support
1459
private PropertyChangeSupport pcs;
1462
* The underlying model source
1464
private ModelSource source;
1467
* Current status of the model
1469
private Status status;
1471
private boolean pretty = false;
1474
* Undoable edit support
1476
private UndoableEditSupport ues;
1479
* whether to fire undo events
1481
private boolean fireUndoEvents = true;
1484
* The names of property change events fired
1487
* Indicates node modified
1489
public static final String PROP_MODIFIED = "modified";
1491
* Indicates node deleted
1493
public static final String PROP_DELETED = "deleted";
1495
* Indicates node added
1497
public static final String PROP_ADDED = "added";
1499
public static final String DEFAULT_INDENT = " ";
1502
* current node count
1504
private int nodeCount = 0;
1506
private ElementIdentity eID;
1508
private String currentIndent = "";
1510
private boolean indentInitialized = false;
1512
private Map<QName,List<QName>> qnameValuedAttributesByElementMap;