0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041: package org.netbeans.modules.xml.xdm;
0042:
0043: import java.beans.PropertyChangeListener;
0044: import java.beans.PropertyChangeSupport;
0045: import java.io.IOException;
0046: import java.util.ArrayList;
0047: import java.util.Collection;
0048: import java.util.List;
0049: import java.util.Map;
0050: import javax.swing.event.UndoableEditEvent;
0051: import javax.swing.event.UndoableEditListener;
0052: import javax.swing.text.BadLocationException;
0053: import javax.swing.undo.CompoundEdit;
0054: import javax.swing.undo.UndoableEdit;
0055: import javax.swing.undo.UndoableEditSupport;
0056: import javax.xml.XMLConstants;
0057: import javax.xml.namespace.QName;
0058: import org.netbeans.api.lexer.Language;
0059: import org.netbeans.api.xml.lexer.XMLTokenId;
0060: import org.netbeans.editor.BaseDocument;
0061: import org.netbeans.modules.xml.xam.ModelSource;
0062: import org.netbeans.modules.xml.xam.dom.ElementIdentity;
0063: import org.netbeans.modules.xml.xdm.diff.DiffFinder;
0064: import org.netbeans.modules.xml.xdm.diff.XDMTreeDiff;
0065: import org.netbeans.modules.xml.xdm.diff.Difference;
0066: import org.netbeans.modules.xml.xdm.diff.Add;
0067: import org.netbeans.modules.xml.xdm.diff.Change;
0068: import org.netbeans.modules.xml.xdm.diff.Delete;
0069: import org.netbeans.modules.xml.xdm.diff.DefaultElementIdentity;
0070: import org.netbeans.modules.xml.xdm.diff.MergeDiff;
0071: import org.netbeans.modules.xml.xdm.diff.NodeIdDiffFinder;
0072: import org.netbeans.modules.xml.xdm.diff.NodeInfo;
0073: import org.netbeans.modules.xml.xdm.diff.SyncPreparation;
0074: import org.netbeans.modules.xml.xdm.nodes.Attribute;
0075: import org.netbeans.modules.xml.xdm.nodes.Document;
0076: import org.netbeans.modules.xml.xdm.nodes.Element;
0077: import org.netbeans.modules.xml.xdm.nodes.Node;
0078: import org.netbeans.modules.xml.xdm.nodes.NodeImpl;
0079: import org.netbeans.modules.xml.xdm.nodes.Text;
0080: import org.netbeans.modules.xml.xdm.nodes.Token;
0081: import org.netbeans.modules.xml.xdm.nodes.XMLSyntaxParser;
0082: import org.netbeans.modules.xml.xdm.visitor.FindVisitor;
0083: import org.netbeans.modules.xml.xdm.visitor.FlushVisitor;
0084: import org.netbeans.modules.xml.xdm.visitor.NamespaceRefactorVisitor;
0085: import org.netbeans.modules.xml.xdm.visitor.PathFromRootVisitor;
0086: import org.netbeans.modules.xml.xdm.visitor.Utils;
0087: import org.w3c.dom.NamedNodeMap;
0088: import org.w3c.dom.NodeList;
0089:
0090: /**
0091: */
0092: public class XDMModel {
0093:
0094: /**
0095: * @param ms requires an instance of org.netbeans.editor.BaseDocument to be
0096: * available in the lookup;
0097: */
0098: public XDMModel(ModelSource ms) {
0099: source = ms;
0100: assert getSwingDocument() != null;
0101: ues = new UndoableEditSupport(this );
0102: pcs = new PropertyChangeSupport(this );
0103: parser = new XMLSyntaxParser();
0104: setStatus(Status.UNPARSED);
0105:
0106: //establish a default element identification mechanism
0107: //domain models should override this by invoking "setElementIdentity"
0108: ElementIdentity eID = createElementIdentity();
0109: setElementIdentity(eID);
0110: }
0111:
0112: public String getIndentation() {
0113: return currentIndent;
0114: }
0115:
0116: public void setIndentation(String indent) {
0117: currentIndent = indent;
0118: indentInitialized = true;
0119: }
0120:
0121: private void setDefaultIndentation() {
0122: currentIndent = DEFAULT_INDENT;
0123: }
0124:
0125: /*
0126: * override this method if domain model wants to identify elements
0127: * using a different mechanism than this default one.
0128: *
0129: */
0130: private ElementIdentity createElementIdentity() {
0131: //Establish DOM element identities
0132: ElementIdentity eID = new DefaultElementIdentity();
0133: //Following values are suitable for Schema and WSDL documents
0134: //these default values can be reset by eID.reset() call
0135: eID.addIdentifier("id");
0136: eID.addIdentifier("name");
0137: eID.addIdentifier("ref");
0138: return eID;
0139: }
0140:
0141: /**
0142: * This api flushes the changes made to the model to the underlying document.
0143: */
0144: public synchronized void flush() {
0145: flushDocument(getDocument());
0146: }
0147:
0148: /**
0149: * This api syncs the model with the underlying swing document.
0150: * If its the first time sync is called, swing document is parsed and model
0151: * is initialized. Otherwise the changes made to swing document are applied
0152: * to the model using DiffMerger.
0153: */
0154: public synchronized void sync() throws IOException {
0155: if (preparation == null) {
0156: prepareSync();
0157: }
0158: finishSync();
0159: }
0160:
0161: public synchronized void prepareSync() {
0162: Status oldStat = getStatus();
0163: try {
0164: setStatus(Status.PARSING); // to access in case old broken tree
0165: //must set the language for XML lexer to work.
0166: getSwingDocument().putProperty(Language.class,
0167: XMLTokenId.language());
0168: Document newDoc = parser.parse(getSwingDocument());
0169: Document oldDoc = getCurrentDocument();
0170: if (oldDoc == null) {
0171: preparation = new SyncPreparation(newDoc);
0172: } else {
0173: newDoc.assignNodeIdRecursively();
0174: XDMTreeDiff treeDiff = new XDMTreeDiff(eID);
0175: List<Difference> preparedDiffs = treeDiff.performDiff(
0176: this , newDoc);
0177: preparation = new SyncPreparation(oldDoc, preparedDiffs);
0178: }
0179: } catch (BadLocationException ble) {
0180: preparation = new SyncPreparation(ble);
0181: } catch (IllegalArgumentException iae) {
0182: preparation = new SyncPreparation(iae);
0183: } catch (IOException ioe) {
0184: preparation = new SyncPreparation(ioe);
0185: } finally {
0186: setStatus(oldStat); // we are not mutating yet, so alway restore
0187: }
0188: }
0189:
0190: private SyncPreparation preparation = null;
0191:
0192: private void finishSync() throws IOException {
0193: if (preparation == null) {
0194: return; // unprepared or other thread has stealth the sync
0195: }
0196:
0197: if (preparation.hasErrors()) {
0198: IOException error = preparation.getError();
0199: preparation = null;
0200: setStatus(Status.BROKEN);
0201: throw error;
0202: }
0203:
0204: Status savedStatus = getStatus();
0205: setStatus(Status.PARSING);
0206: Document oldDoc = getCurrentDocument();
0207: try {
0208: if (preparation.getNewDocument() != null) {
0209: Document newDoc = preparation.getNewDocument();
0210: newDoc.addedToTree(this );
0211: setDocument(newDoc);
0212: } else {
0213: assert preparation.getOldDocument() != null : "Invalid preparation oldDoc is null";
0214: if (oldDoc != preparation.getOldDocument()) {
0215: // other thread has completed the sync before me
0216: setStatus(savedStatus);
0217: return;
0218: }
0219: List<Difference> diffs = preparation.getDifferences();
0220: mergeDiff(diffs);
0221: //diffs = DiffFinder.filterWhitespace(diffs);
0222: fireDiffEvents(diffs);
0223: if (getCurrentDocument() != oldDoc) {
0224: fireUndoableEditEvent(getCurrentDocument(), oldDoc);
0225: }
0226: }
0227: setStatus(Status.STABLE);
0228: } catch (IllegalArgumentException iae) {
0229: if (getStatus() != Status.STABLE) {
0230: IOException ioe = new IOException();
0231: ioe.initCause(iae);
0232: throw ioe;
0233: } else {
0234: // XAM will review the mutation and veto by it wrapped IOException
0235: if (iae.getCause() instanceof IOException) {
0236: setStatus(Status.BROKEN);
0237: throw (IOException) iae.getCause();
0238: } else {
0239: throw iae;
0240: }
0241: }
0242: } finally {
0243: if (getStatus() != Status.STABLE) {
0244: setStatus(Status.BROKEN);
0245: setDocument(oldDoc);
0246: }
0247: preparation = null;
0248: }
0249: }
0250:
0251: public void mergeDiff(List<Difference> diffs) {
0252: setStatus(Status.PARSING);
0253: new MergeDiff().merge(this , diffs);
0254: // exception in event firing should not put tree out-of-sync with buffer
0255: setStatus(Status.STABLE);
0256: }
0257:
0258: private void fireDiffEvents(final List<Difference> deList) {
0259: for (Difference de : deList) {
0260: NodeInfo.NodeType nodeType = de.getNodeType();
0261: // if ( nodeType == NodeInfo.NodeType.WHITE_SPACE ) continue;//skip if WS
0262: if (de instanceof Add) {
0263: NodeInfo newNodeInfo = ((Add) de).getNewNodeInfo();
0264: assert newNodeInfo != null;
0265: pcs.firePropertyChange(PROP_ADDED, null, newNodeInfo);
0266: } else if (de instanceof Delete) {
0267: NodeInfo OldNodeInfo = ((Delete) de).getOldNodeInfo();
0268: assert OldNodeInfo != null;
0269: pcs.firePropertyChange(PROP_DELETED, OldNodeInfo, null);
0270: } else if (de instanceof Change) {
0271: NodeInfo oldNodeInfo = ((Change) de).getOldNodeInfo();
0272: assert oldNodeInfo != null;
0273:
0274: NodeInfo newNodeInfo = ((Change) de).getNewNodeInfo();
0275: assert newNodeInfo != null;
0276:
0277: //fire delete and add events for position change of element/text
0278: if (((Change) de).isPositionChanged()) {
0279: pcs.firePropertyChange(PROP_DELETED, oldNodeInfo,
0280: null);
0281: pcs.firePropertyChange(PROP_ADDED, null,
0282: newNodeInfo);
0283: } else if (de.getNodeType() == NodeInfo.NodeType.TEXT) { //text change only
0284: pcs.firePropertyChange(PROP_MODIFIED, oldNodeInfo,
0285: newNodeInfo);
0286: } else if (de.getNodeType() == NodeInfo.NodeType.ELEMENT) {
0287: List<Node> path1 = new ArrayList<Node>(oldNodeInfo
0288: .getOriginalAncestors());
0289: path1.add(0, oldNodeInfo.getNode());
0290: List<Node> path2 = new ArrayList<Node>(newNodeInfo
0291: .getNewAncestors());
0292: path2.add(0, newNodeInfo.getNode());
0293: //fire attribute change events
0294: List<Change.AttributeDiff> attrChanges = ((Change) de)
0295: .getAttrChanges();
0296: for (Change.AttributeDiff attrDiff : attrChanges) {
0297: Node oldAttr = attrDiff.getOldAttribute();
0298: Node newAttr = attrDiff.getNewAttribute();
0299: if (attrDiff instanceof Change.AttributeAdd) {
0300: assert newAttr != null;
0301: pcs.firePropertyChange(PROP_ADDED, null,
0302: new NodeInfo(newAttr, -1, path1,
0303: path2));
0304: } else if (attrDiff instanceof Change.AttributeDelete) {
0305: assert oldAttr != null;
0306: pcs.firePropertyChange(PROP_DELETED,
0307: new NodeInfo(oldAttr, -1, path1,
0308: path2), null);
0309: } else if (attrDiff instanceof Change.AttributeChange) {
0310: assert oldAttr != null;
0311: assert newAttr != null;
0312: NodeInfo old = new NodeInfo(oldAttr, -1,
0313: path1, path2);
0314: NodeInfo now = new NodeInfo(newAttr, -1,
0315: path1, path2);
0316: pcs.firePropertyChange(PROP_MODIFIED, old,
0317: now);
0318: }
0319: }
0320: }
0321: }
0322: }
0323: }
0324:
0325: private interface Updater {
0326: void update(Node parent, Node oldNode, Node newNode);
0327: }
0328:
0329: private List<Node> getPathToRoot(Node node, Document root) {
0330: PathFromRootVisitor pathVisitor = new PathFromRootVisitor();
0331: List<Node> path = pathVisitor.findPath(root, node);
0332: if (path == null || path.isEmpty()) {
0333: throw new IllegalArgumentException(
0334: "old node must be in the tree");
0335: }
0336: return path;
0337: }
0338:
0339: private static String classifyMutationType(Node oldNode,
0340: Node newNode) {
0341: if (newNode == null && oldNode == null || newNode != null
0342: && oldNode != null) {
0343: return PROP_MODIFIED;
0344: } else if (newNode != null) {
0345: return PROP_ADDED;
0346: } else {
0347: return PROP_DELETED;
0348: }
0349: }
0350:
0351: private enum MutationType {
0352: CHILDREN, ATTRIBUTE, BOTH
0353: }
0354:
0355: private List<Node> mutate(Node parent, Node oldNode, Node newNode,
0356: Updater updater) {
0357: return mutate(parent, oldNode, newNode, updater, null);
0358: }
0359:
0360: private List<Node> mutate(Node parent, Node oldNode, Node newNode,
0361: Updater updater, MutationType type) {
0362: checkStableOrParsingState();
0363: if (newNode != null)
0364: checkNodeInTree(newNode);
0365:
0366: Document currentDocument = getDocument();
0367: List<Node> ancestors;
0368: if (parent == null) {
0369: assert (oldNode != null);
0370: ancestors = getPathToRoot(oldNode, currentDocument);
0371: oldNode = ancestors.remove(0);
0372: } else {
0373: if (oldNode != null) {
0374: assert parent.getIndexOfChild(oldNode) > -1;
0375: ancestors = getPathToRoot(oldNode, currentDocument);
0376: assert oldNode.getId() == ancestors.get(0).getId();
0377: oldNode = ancestors.remove(0);
0378: assert parent.getId() == ancestors.get(0).getId();
0379: } else {
0380: ancestors = getPathToRoot(parent, currentDocument);
0381: }
0382: }
0383:
0384: final Node oldParent = ancestors.remove(0);
0385: Node newParent;
0386: if (type == null) {
0387: if (oldNode instanceof Attribute
0388: || newNode instanceof Attribute
0389: || (oldNode == null && newNode == null)) {
0390: type = MutationType.ATTRIBUTE;
0391: } else if (ancestors.size() == 1) {
0392: assert (oldParent instanceof Element);
0393: //might have to add namespace declaration to root
0394: type = MutationType.BOTH;
0395: } else {
0396: type = MutationType.CHILDREN;
0397: }
0398: }
0399: switch (type) {
0400: case ATTRIBUTE:
0401: newParent = (Node) oldParent.clone(true, true, false);
0402: break;
0403: case CHILDREN:
0404: newParent = (Node) oldParent.clone(true, false, true);
0405: break;
0406: default:
0407: newParent = (Node) oldParent.clone(true, true, true);
0408: }
0409:
0410: if (oldNode != null && oldNode.getNodeType() != Node.TEXT_NODE
0411: && newNode == null) { // pure remove
0412: undoPrettyPrint(newParent, oldNode, oldParent);
0413: }
0414: updater.update(newParent, oldNode, newNode);
0415: if (oldNode == null && newNode != null
0416: && newNode.getNodeType() != Node.TEXT_NODE) { // pure add
0417: doPrettyPrint(newParent, newNode, oldParent);
0418: }
0419:
0420: List<Node> newAncestors = updateAncestors(ancestors, newParent,
0421: oldParent);
0422: if (getStatus() != Status.PARSING && newNode instanceof Element) {
0423: consolidateNamespaces(newAncestors, newParent,
0424: (Element) newNode);
0425: }
0426: Document d = (Document) (!newAncestors.isEmpty() ? newAncestors
0427: .get(newAncestors.size() - 1) : newParent);
0428: d.addedToTree(this );
0429: setDocument(d);
0430: ancestors.add(0, oldParent);
0431: newAncestors.add(0, newParent);
0432: if (getStatus() != Status.PARSING) { // not merging
0433: fireUndoableEditEvent(d, currentDocument);
0434: String mutationType = classifyMutationType(oldNode, newNode);
0435: //TODO seems missing delete/change; also, who are listening to these xdm mutation events
0436: NodeInfo newNodeInfo = new NodeInfo(newNode, -1, ancestors,
0437: newAncestors);
0438: pcs.firePropertyChange(mutationType, null, newNodeInfo);
0439: }
0440: return newAncestors;
0441: }
0442:
0443: private void consolidateNamespaces(List<Node> ancestors,
0444: Node parent, Element newNode) {
0445: if (parent instanceof Document)
0446: return; // no actions if newNode is root itself
0447: assert ancestors.size() > 0;
0448: Element root = (Element) (ancestors.size() == 1 ? parent
0449: : ancestors.get(ancestors.size() - 2));
0450: List<Node> parentAndAncestors = new ArrayList(ancestors);
0451: parentAndAncestors.add(0, parent);
0452: consolidateAttributePrefix(parentAndAncestors, newNode);
0453: NamedNodeMap nnm = newNode.getAttributes();
0454: for (int i = 0; i < nnm.getLength(); i++) {
0455: if (nnm.item(i) instanceof Attribute) {
0456: Attribute attr = (Attribute) nnm.item(i);
0457: consolidateNamespace(root, parentAndAncestors, newNode,
0458: attr);
0459: }
0460: }
0461:
0462: // use parent node prefix
0463: String parentPrefix = parent.getPrefix();
0464: String parentNS = NodeImpl.lookupNamespace(parent, ancestors);
0465: if (parentNS != null
0466: && !parentNS.equals(XMLConstants.NULL_NS_URI)) {
0467: new NamespaceRefactorVisitor(this ).refactor(newNode,
0468: parentNS, parentPrefix, parentAndAncestors);
0469: }
0470: }
0471:
0472: private void consolidateAttributePrefix(
0473: List<Node> parentAndAncestors, Element newNode) {
0474: NamedNodeMap nnm = newNode.getAttributes();
0475: for (int i = 0; i < nnm.getLength(); i++) {
0476: if (!(nnm.item(i) instanceof Attribute))
0477: continue;
0478: Attribute attr = (Attribute) nnm.item(i);
0479: String prefix = attr.getPrefix();
0480: if (prefix != null && !attr.isXmlnsAttribute()) {
0481: String namespace = newNode.lookupNamespaceURI(prefix);
0482: if (namespace == null)
0483: continue;
0484: prefix = NodeImpl.lookupPrefix(namespace,
0485: parentAndAncestors);
0486: if (prefix != null) {
0487: attr.setPrefix(prefix);
0488: }
0489: }
0490: }
0491: }
0492:
0493: /**
0494: * Consolidate new node top-leveled namespaces with parent's.
0495: * Note: this assume #consolidateAttributePrefix has been called
0496: */
0497: private void consolidateNamespace(Element root,
0498: List<Node> parentAndAncestors, Element newNode,
0499: Attribute attr) {
0500: if (attr.isXmlnsAttribute()) {
0501: String prefix = attr.getLocalName();
0502: if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
0503: prefix = XMLConstants.DEFAULT_NS_PREFIX;
0504: }
0505: String namespace = attr.getValue();
0506: assert (namespace != null);
0507:
0508: Node parent = parentAndAncestors.get(0);
0509: String parentNS = NodeImpl.lookupNamespace(parent,
0510: parentAndAncestors);
0511:
0512: String existingNS = NodeImpl.lookupNamespace(prefix,
0513: parentAndAncestors);
0514: String existingPrefix = NodeImpl.lookupPrefix(namespace,
0515: parentAndAncestors);
0516:
0517: // 1. prefix is free (existingNS == null) and namespace is never declared (existingPrefix == null)
0518: // 2. prefix is used and for the same namespace
0519: // 3. namespace is declared by different prefix
0520: // 4. prefix is used and for different namespace
0521:
0522: if (existingNS == null && existingPrefix == null) { // case 1.
0523: newNode.removeAttributeNode(attr);
0524: root.appendAttribute(attr);
0525: } else if (namespace.equals(existingNS)
0526: && prefix.equals(existingPrefix)) { // case 2
0527: assert prefix.equals(existingPrefix) : "prefix='"
0528: + prefix + "' existingPrefix='"
0529: + existingPrefix + "'";
0530: newNode.removeAttributeNode(attr);
0531: } else if (existingPrefix != null) { // case 3.
0532: // skip attribute redeclaring namespace of parent element
0533: // we will refactor to parent prefix after processing all xmlns-attributes
0534: if (!namespace.equals(parentNS)) {
0535: new NamespaceRefactorVisitor(this ).refactor(
0536: newNode, namespace, existingPrefix,
0537: parentAndAncestors);
0538: }
0539: } else { // existingNS != null && existingPrefix == null
0540: // case 4 just leave prefix as overriding with different namespace
0541: }
0542: }
0543: }
0544:
0545: /**
0546: * This api replaces given old node with given new node.
0547: * The old node passed must be in tree, the new node must not be in tree,
0548: * and new node must be clone of old node.
0549: * @param oldValue The old node to be replaced.
0550: * @param newValue The new node.
0551: * @return The new parent
0552: */
0553: public synchronized List<Node> modify(Node oldValue, Node newValue) {
0554: if (oldValue.getId() != newValue.getId()) {
0555: throw new IllegalArgumentException(
0556: "newValue must be a clone of oldValue");
0557: }
0558:
0559: if (oldValue instanceof Document) {
0560: assert newValue instanceof Document;
0561: Document oldDoc = (Document) oldValue;
0562: Document newDoc = (Document) newValue;
0563: newDoc.addedToTree(this );
0564: setDocument(newDoc);
0565: if (getStatus() != Status.PARSING) { // not merging
0566: fireUndoableEditEvent(oldDoc, currentDocument);
0567: String mutationType = classifyMutationType(oldDoc,
0568: newDoc);
0569: ArrayList<Node> ancestors = new ArrayList<Node>();
0570: NodeInfo oldNodeInfo = new NodeInfo(oldDoc, -1,
0571: ancestors, ancestors);
0572: NodeInfo newNodeInfo = new NodeInfo(newDoc, -1,
0573: ancestors, ancestors);
0574: pcs.firePropertyChange(mutationType, oldNodeInfo,
0575: newNodeInfo);
0576: }
0577: return new ArrayList<Node>();
0578: }
0579:
0580: Updater modifier = new Updater() {
0581: public void update(Node newParent, Node oldNode,
0582: Node newNode) {
0583: if (oldNode instanceof Attribute) {
0584: ((Element) newParent).replaceAttribute(
0585: (Attribute) newNode, (Attribute) oldNode);
0586: } else {
0587: newParent.replaceChild(newNode, oldNode);
0588: }
0589: }
0590: };
0591: return mutate(null, oldValue, newValue, modifier);
0592: }
0593:
0594: /**
0595: * This api adds given node to given parent at given index.
0596: * The added node will be part of childnodes of the parent,
0597: * and its index will be the given index. If the given index
0598: * is out of the parents childnodes range, the node will be
0599: * appended.
0600: * @param parent The parent node to which the node is to be added.
0601: * @param node The node which is to be added.
0602: * @param offset The index at which the node is to be added.
0603: * @return The parent node resulted by addition of this node
0604: */
0605: public synchronized List<Node> add(Node parent, Node node,
0606: final int offset) {
0607: if (offset < 0)
0608: throw new IndexOutOfBoundsException();
0609: Updater adder = new Updater() {
0610: public void update(Node newParent, Node oldNode,
0611: Node newNode) {
0612: if (newParent instanceof Element
0613: && newNode instanceof Attribute) {
0614: Element newElement = (Element) newParent;
0615: if (offset > newElement.getAttributes().getLength())
0616: throw new IndexOutOfBoundsException();
0617: newElement
0618: .addAttribute((Attribute) newNode, offset);
0619: } else {
0620: //reset id, since adding root element
0621: if (newParent instanceof Document
0622: && newNode instanceof Element)
0623: resetIdMeter();
0624:
0625: if (offset > newParent.getChildNodes().getLength())
0626: throw new IndexOutOfBoundsException();
0627: if (offset < newParent.getChildNodes().getLength()) {
0628: Node refChild = (Node) newParent
0629: .getChildNodes().item(offset);
0630: newParent.insertBefore(newNode, refChild);
0631: } else {
0632: newParent.appendChild(newNode);
0633: }
0634: }
0635: }
0636: };
0637: return mutate(parent, null, node, adder);
0638: }
0639:
0640: /**
0641: * This api adds given node to given parent before given ref node.
0642: * The inserted node will be part of childnodes of the parent,
0643: * and will appear before ref node.
0644: * @param parent The parent node to which the node is to be added.
0645: * @param node The node which is to be added.
0646: * @param refChild The ref node (child) of parent node,
0647: * before which the node is to be added.
0648: * @return The parent node resulted by inserion of this node.
0649: */
0650: public synchronized List<Node> insertBefore(Node parent, Node node,
0651: Node refChild) {
0652: final Node ref = refChild;
0653: Updater updater = new Updater() {
0654: public void update(Node newParent, Node oldNode,
0655: Node newNode) {
0656: newParent.insertBefore(newNode, ref);
0657: }
0658: };
0659:
0660: return mutate(parent, null, node, updater);
0661: }
0662:
0663: /**
0664: * This api adds given node to given parent at the end.
0665: * The added node will be part of childnodes of the parent,
0666: * and it will be the last node.
0667: * @param parent The parent node to which the node is to be appended.
0668: * @param node The node which is to be appended.
0669: * @return The parent node resulted by addition of this node
0670: */
0671: public synchronized List<Node> append(Node parent, Node node) {
0672: Updater appender = new Updater() {
0673: public void update(Node parent, Node oldNode, Node newNode) {
0674: //reset id, since adding root element
0675: if (parent instanceof Document
0676: && newNode instanceof Element)
0677: resetIdMeter();
0678: parent.appendChild(newNode);
0679: }
0680: };
0681: return mutate(parent, null, node, appender);
0682: }
0683:
0684: /**
0685: * This api deletes given node from a tree.
0686: * @param node The node to be deleted.
0687: * @return The parent node resulted by deletion of this node.
0688: */
0689: public synchronized List<Node> delete(Node n) {
0690: Updater remover = new Updater() {
0691: public void update(Node newParent, Node oldNode,
0692: Node newNode) {
0693: newParent.removeChild(oldNode);
0694: }
0695: };
0696: return mutate(null, n, null, remover);
0697: }
0698:
0699: /**
0700: * This api changes index of the given node.
0701: * @param nodes The nodes to be moved.
0702: * @param indexes the new indexes of the nodes.
0703: * @return The parent node resulted by deletion of this node.
0704: */
0705: public synchronized List<Node> reorder(Node parent, Node n,
0706: final int index) {
0707: if (index < 0)
0708: throw new IndexOutOfBoundsException("index=" + index);
0709: Updater u = new Updater() {
0710: public void update(Node newParent, Node oldNode,
0711: Node newNode) {
0712: if (newParent instanceof Element
0713: && newNode instanceof Attribute) {
0714: Element parent = (Element) newParent;
0715: int i = index;
0716: if (index > parent.getAttributes().getLength()) {
0717: i = parent.getAttributes().getLength();
0718: }
0719: parent.reorderAttribute((Attribute) oldNode, i);
0720: } else {
0721: int i = index;
0722: if (index > newParent.getChildNodes().getLength()) {
0723: i = newParent.getChildNodes().getLength();
0724: }
0725: ((NodeImpl) newParent).reorderChild(oldNode, i);
0726: }
0727: }
0728: };
0729: return mutate(parent, n, null, u);
0730: }
0731:
0732: /**
0733: * This api changes indexes of the given node children.
0734: * @param nodes The nodes to be moved.
0735: * @param indexes the new indexes of the nodes.
0736: * @return The parent node resulted by deletion of this node.
0737: */
0738: public synchronized List<Node> reorderChildren(Node parent,
0739: final int[] permutation) {
0740: Updater u = new Updater() {
0741: public void update(Node newParent, Node oldNode,
0742: Node newNode) {
0743: ((NodeImpl) newParent).reorderChildren(permutation);
0744: }
0745: };
0746: return mutate(parent, null, null, u, MutationType.CHILDREN);
0747: }
0748:
0749: /**
0750: * This api deletes given node from a given parent node.
0751: * @param parent The parent node from which the node is to be deleted.
0752: * @param child The node to be deleted.
0753: * @return The parent node resulted by deletion of this node.
0754: */
0755: public synchronized List<Node> remove(final Node parent, Node child) {
0756: Updater remover = new Updater() {
0757: public void update(Node newParent, Node oldNode,
0758: Node newNode) {
0759: assert parent.isEquivalentNode(newParent);
0760: newParent.removeChild(oldNode);
0761: }
0762: };
0763: return mutate(parent, child, null, remover);
0764: }
0765:
0766: /**
0767: * This api deletes given node from a given parent node.
0768: * @param parent The parent node from which the node is to be deleted.
0769: * @param toRemove collection of node to be deleted.
0770: * @return The parent node resulted by deletion of this node.
0771: */
0772: public synchronized List<Node> removeChildNodes(final Node parent,
0773: final Collection<Node> toRemove) {
0774: Updater remover = new Updater() {
0775: public void update(Node newParent, Node oldNode,
0776: Node newNode) {
0777: assert parent.isEquivalentNode(newParent);
0778: for (Node n : toRemove) {
0779: newParent.removeChild(n);
0780: }
0781: }
0782: };
0783: return mutate(parent, null, null, remover,
0784: MutationType.CHILDREN);
0785: }
0786:
0787: public synchronized List<Node> replaceChild(final Node parent,
0788: Node child, Node newChild) {
0789: Updater updater = new Updater() {
0790: public void update(Node newParent, Node oldNode,
0791: Node newNode) {
0792: assert newParent.isEquivalentNode(parent);
0793: newParent.replaceChild(newNode, oldNode);
0794: }
0795: };
0796: return mutate(null, child, newChild, updater);
0797: }
0798:
0799: /**
0800: * This api sets an attribute given name and value of a given element node.
0801: * If an attribute with given name already present in element, it will only
0802: * set the value. Otherwise a new attribute node, with given name and value,
0803: * will be appended to the attibute list of the element node.
0804: * @param element The element of which the attribute to be set.
0805: * @param name The name of the attribute to be set.
0806: * @param value The value of the attribute to be set.
0807: * @return The element resulted by setting of attribute.
0808: */
0809: public synchronized List<Node> setAttribute(Element element,
0810: final String name, final String value) {
0811: Updater updater = new Updater() {
0812: public void update(Node newParent, Node oldNode,
0813: Node newNode) {
0814: ((Element) newParent).setAttribute(name, value);
0815: }
0816: };
0817: return mutate(element, null, null, updater);
0818: }
0819:
0820: /**
0821: * This api removes an attribute given name and value of a given element node.
0822: * @param element The element of which the attribute to be removed.
0823: * @param name The name of the attribute to be removed.
0824: * @return The element resulted by removed of attribute.
0825: */
0826: public synchronized List<Node> removeAttribute(Element element,
0827: final String name) {
0828: Updater updater = new Updater() {
0829: public void update(Node newParent, Node oldNode,
0830: Node newNode) {
0831: ((Element) newParent).removeAttribute(name);
0832: }
0833: };
0834: return mutate(element, null, null, updater);
0835: }
0836:
0837: private interface CheckIOExceptionUpdater extends Updater {
0838: public IOException getError();
0839: }
0840:
0841: public synchronized List<Node> setXmlFragmentText(Element node,
0842: final String value) throws IOException {
0843: CheckIOExceptionUpdater updater = new CheckIOExceptionUpdater() {
0844: public void update(Node newParent, Node oldNode,
0845: Node newNode) {
0846: try {
0847: ((Element) newParent).setXmlFragmentText(value);
0848: } catch (IOException ioe) {
0849: error = ioe;
0850: }
0851: }
0852:
0853: public IOException getError() {
0854: return error;
0855: }
0856:
0857: private IOException error;
0858: };
0859: List<Node> retPath = mutate(node, null, null, updater,
0860: MutationType.CHILDREN);
0861: if (updater.getError() != null) {
0862: throw updater.getError();
0863: } else {
0864: return retPath;
0865: }
0866: }
0867:
0868: public synchronized List<Node> setTextValue(Node node, String value) {
0869: Node text = (Node) currentDocument.createTextNode(value);
0870: Updater updater = new Updater() {
0871: public void update(Node newParent, Node oldNode,
0872: Node newNode) {
0873: while (newParent.hasChildNodes()) {
0874: newParent.removeChild(newParent.getLastChild());
0875: }
0876: newParent.appendChild(newNode);
0877: }
0878: };
0879: return mutate(node, null, text, updater);
0880: }
0881:
0882: /**
0883: * This is utility method which updates all the ancestors in the given
0884: * ancestor list of given originalNode. The list returned represents
0885: * the ancestors of given modified node.
0886: * @param ancestors the list of ancestors starting from parent
0887: * @param modifiedNode The modified node for which the new list is to be created
0888: * @param originalNode The original node which ancestors are given
0889: * @return The list of new ancestors starting parent for the modified node
0890: */
0891: private List<Node> updateAncestors(List<Node> ancestors,
0892: Node modifiedNode, Node originalNode) {
0893: assert ancestors != null && modifiedNode != null
0894: && originalNode != null;
0895: List<Node> newAncestors = new ArrayList<Node>(ancestors.size());
0896: Node currentModifiedNode = modifiedNode;
0897: Node currentOrigNode = originalNode;
0898: for (Node parentNode : ancestors) {
0899: Node newParentNode = (Node) parentNode.clone(false, true,
0900: true);
0901: newParentNode.replaceChild(currentModifiedNode,
0902: currentOrigNode);
0903: newAncestors.add(newParentNode);
0904: currentOrigNode = parentNode;
0905: currentModifiedNode = newParentNode;
0906: }
0907: return newAncestors;
0908: }
0909:
0910: /**
0911: * This api returns the latest stable document in the model.
0912: * @return The latest stable document in the model.
0913: */
0914: public synchronized Document getDocument() {
0915: checkStableOrParsingState();
0916: return currentDocument;
0917: }
0918:
0919: /**
0920: * This api returns the current document in the model, regardless of the state.
0921: * @return The latest stable document in the model.
0922: */
0923: public synchronized Document getCurrentDocument() {
0924: return currentDocument;
0925: }
0926:
0927: /**
0928: * Reset document to provided known version and cause events to be fired.
0929: * Note caller are responsible to handle exception and decide which version
0930: * to keep after exception and do proper cleanup.
0931: */
0932: synchronized void resetDocument(Document newDoc) {
0933: try {
0934: fireUndoEvents = false;
0935: List<Difference> diffs = new NodeIdDiffFinder().findDiff(
0936: getCurrentDocument(), newDoc);
0937: List<Difference> filtered = DiffFinder
0938: .filterWhitespace(diffs);
0939: //flushDocument(newDoc);
0940: setDocument(newDoc);
0941: if (filtered != null && !filtered.isEmpty()) {
0942: fireDiffEvents(filtered);
0943: }
0944: } finally {
0945: fireUndoEvents = true;
0946: }
0947: }
0948:
0949: private void flushDocument(Document newDoc) {
0950: checkStableState();
0951: UndoableEditListener uel = null;
0952: BaseDocument d = getSwingDocument();
0953: final CompoundEdit ce = new CompoundEdit();
0954: try {
0955: FlushVisitor flushvisitor = new FlushVisitor();
0956: String newXMLText = flushvisitor.flushModel(newDoc);
0957: uel = new UndoableEditListener() {
0958: public void undoableEditHappened(UndoableEditEvent e) {
0959: ce.addEdit(e.getEdit());
0960: }
0961: };
0962: d.addUndoableEditListener(uel);
0963: Utils.replaceDocument(d, newXMLText);
0964: } catch (BadLocationException ble) {
0965: throw new IllegalStateException(
0966: "It is possible that model source file is locked",
0967: ble);
0968: } finally {
0969: if (uel != null) {
0970: d.removeUndoableEditListener(uel);
0971: }
0972: ce.end();
0973: for (UndoableEditListener l : ues
0974: .getUndoableEditListeners()) {
0975: l.undoableEditHappened(new UndoableEditEvent(this , ce));
0976: }
0977: }
0978: }
0979:
0980: public synchronized String getCurrentDocumentText() {
0981: return new FlushVisitor().flushModel(getCurrentDocument());
0982: }
0983:
0984: private BaseDocument getSwingDocument() {
0985: BaseDocument bd = (BaseDocument) source.getLookup().lookup(
0986: BaseDocument.class);
0987: return bd;
0988: }
0989:
0990: public synchronized void setDocument(Document newDoc) {
0991: currentDocument = newDoc;
0992: }
0993:
0994: /**
0995: * This returns the statuc of the model.
0996: * @return the status.
0997: * @see #Status
0998: */
0999: public synchronized Status getStatus() {
1000: return status;
1001: }
1002:
1003: /**
1004: * This api adds an undoable edit listener.
1005: * @param l The undoable edit listener to be added.
1006: */
1007: public synchronized void addUndoableEditListener(
1008: UndoableEditListener l) {
1009: ues.addUndoableEditListener(l);
1010: }
1011:
1012: /**
1013: * This api removes an undoable edit listener.
1014: * @param l The undoable edit listener to be removed.
1015: */
1016: public synchronized void removeUndoableEditListener(
1017: UndoableEditListener l) {
1018: ues.addUndoableEditListener(l);
1019: }
1020:
1021: /**
1022: * This api adds a property change listener.
1023: * @param pcl The property change listener to be added.
1024: */
1025: public synchronized void addPropertyChangeListener(
1026: PropertyChangeListener pcl) {
1027: pcs.addPropertyChangeListener(pcl);
1028: }
1029:
1030: /**
1031: * This api removes a property change listener.
1032: * @param pcl The property change listener to be removed.
1033: */
1034: public synchronized void removePropertyChangeListener(
1035: PropertyChangeListener pcl) {
1036: pcs.removePropertyChangeListener(pcl);
1037: }
1038:
1039: /**
1040: * Find the node with same id in the current tree.
1041: */
1042: private synchronized Node findNode(int id) {
1043: FindVisitor fv = new FindVisitor();
1044: return fv.find(getDocument(), id);
1045: }
1046:
1047: /**
1048: * This represents the status of the XDM Model.
1049: * Status STABLE means the latest attempt to parse was successful
1050: * Status BROKEN means that the latest attempt to parse was unsuccessful.
1051: * Status UNPARSED means the document has not been parsed yet.
1052: * Status PARSING means the document is being parsed.
1053: */
1054: //TODO Last Parsed status
1055: public enum Status {
1056: BROKEN, STABLE, UNPARSED, PARSING;
1057: }
1058:
1059: private void fireUndoableEditEvent(Document newDoc, Document oldDoc) {
1060: if (fireUndoEvents) {
1061: assert newDoc != oldDoc;
1062: UndoableEdit ee = new XDMModelUndoableEdit(oldDoc, newDoc,
1063: this );
1064: UndoableEditEvent ue = new UndoableEditEvent(this , ee);
1065: for (UndoableEditListener l : ues
1066: .getUndoableEditListeners()) {
1067: l.undoableEditHappened(ue);
1068: }
1069: }
1070: }
1071:
1072: private void checkNodeInTree(Node n) {
1073: if (n.isInTree()) {
1074: throw new IllegalArgumentException(
1075: "newValue must not have been added to model"); // NOI18N
1076: }
1077: }
1078:
1079: private void checkStableState() {
1080: if (getStatus() != Status.STABLE) {
1081: throw new IllegalStateException(
1082: "flush can only be called from STABLE STATE"); //NOI18N
1083: }
1084: }
1085:
1086: private void checkStableOrParsingState() {
1087: if (getStatus() != Status.STABLE
1088: && getStatus() != Status.PARSING) {
1089: throw new IllegalStateException(
1090: "The model is not initialized or is broken."); //NOI18N
1091: }
1092: }
1093:
1094: private void setStatus(Status s) {
1095: status = s;
1096: }
1097:
1098: /**
1099: * This api keeps track of the nodes created in this model.
1100: * @return the id of the next node to be created.
1101: */
1102: public int getNextNodeId() {
1103: int nodeId = nodeCount;
1104: nodeCount++;
1105: return nodeId;
1106: }
1107:
1108: /**
1109: * resets id meter
1110: */
1111: private void resetIdMeter() {
1112: nodeCount = 1;
1113: }
1114:
1115: private boolean isPretty() {
1116: return pretty;
1117: }
1118:
1119: public void setPretty(boolean print) {
1120: pretty = print;
1121: }
1122:
1123: private void doPrettyPrint(Node newParent, Node newNode,
1124: Node oldParent) {
1125: if ((getStatus() != Status.PARSING) && isPretty()) {
1126: if (isSimpleContent(newParent)) {//skip if simpleContent
1127: /*
1128: * <test name="test1">A new text node</test>
1129: */
1130: return;
1131: }
1132: if (!indentInitialized)
1133: initializeIndent(oldParent);
1134: String parentIndent = calculateNodeIndent(oldParent);
1135: if (!isPretty(newParent, newNode)) {//skip if already pretty
1136: int offset = 1;
1137: if (oldParent.getChildNodes().getLength() == 0) {//old parent did not have prettyprint before
1138: /*
1139: * before
1140: *
1141: * <test name="test1"></test>
1142: *
1143: * after
1144: *
1145: * <test name="test1">
1146: * <c name="c1">
1147: * </test>
1148: */
1149: newParent.insertBefore(
1150: createPrettyText(parentIndent
1151: + getIndentation()), newNode);
1152: offset++;
1153: }
1154: int index = ((NodeImpl) newParent)
1155: .getIndexOfChild(newNode);
1156: if (index > 0) {
1157: Node oldText = (Node) newParent.getChildNodes()
1158: .item(index - 1);
1159: if (checkPrettyText(oldText)) {
1160: /*
1161: * before
1162: *
1163: * <test name="test1">
1164: * <a name="a1">
1165: * <b name="b1">
1166: * <c name="c1">
1167: * </test>
1168: *
1169: * after
1170: *
1171: * <test name="test1">
1172: * <a name="a1">
1173: * <b name="b1">
1174: * <c name="c1">
1175: * </test>
1176: */
1177: Text newText = createPrettyText(parentIndent
1178: + getIndentation());
1179: newParent.replaceChild(newText, oldText);
1180: } else {
1181: /*
1182: * before
1183: *
1184: * <test name="test1">
1185: * <a name="a1">
1186: * <b name="b1"><c name="c1">
1187: * </test>
1188: *
1189: * after
1190: *
1191: * <test name="test1">
1192: * <a name="a1">
1193: * <b name="b1">
1194: * <c name="c1">
1195: * </test>
1196: */
1197: newParent.insertBefore(
1198: createPrettyText(parentIndent
1199: + getIndentation()), newNode);
1200: offset++;
1201: }
1202: }
1203: Node ref = null;
1204: if ((index + offset) < newParent.getChildNodes()
1205: .getLength())
1206: ref = (Node) newParent.getChildNodes().item(
1207: (index + offset));
1208: if (ref != null) {
1209: if (!checkPrettyText(ref)) {
1210: /*
1211: * before
1212: *
1213: * <test name="test1">
1214: * <a name="a1">
1215: * <b name="b1">
1216: * <c name="c1"><d name="d1">
1217: * </test>
1218: *
1219: * after
1220: *
1221: * <test name="test1">
1222: * <a name="a1">
1223: * <b name="b1">
1224: * <c name="c1">
1225: * <d name="d1">
1226: * </test>
1227: */
1228: newParent.insertBefore(
1229: createPrettyText(parentIndent
1230: + getIndentation()), ref);
1231: }
1232: } else {
1233: /*
1234: * before
1235: *
1236: * <test name="test1">
1237: * <a name="a1">
1238: * <b name="b1">
1239: * <c name="c1"></test>
1240: *
1241: * after
1242: *
1243: * <test name="test1">
1244: * <a name="a1">
1245: * <b name="b1">
1246: * <c name="c1">
1247: * </test>
1248: */
1249: newParent
1250: .appendChild(createPrettyText(parentIndent));
1251: }
1252: }
1253:
1254: //recurse pretty print
1255: doPrettyPrintRecursive(newNode, parentIndent, newParent);//for children of node
1256: }
1257: }
1258:
1259: /*
1260: * initialized only once
1261: *
1262: */
1263: private void initializeIndent(final Node n) {
1264: String parentIndent = calculateNodeIndent(n);
1265: List<Node> pathToRoot = new PathFromRootVisitor().findPath(
1266: getDocument(), n);
1267: if (parentIndent.length() > 0 && pathToRoot.size() - 2 > 0) {
1268: //exclude Document and the root from path for indent step calculation
1269: double step = Math.floor(parentIndent.length()
1270: / (double) (pathToRoot.size() - 2));
1271: StringBuffer sb = new StringBuffer();
1272: for (int i = 0; i < step; i++)
1273: sb.append(" ");
1274: String indentString = sb.toString();
1275: if (indentString.length() > 0)
1276: setIndentation(indentString);
1277: else
1278: setDefaultIndentation();
1279: } else
1280: setDefaultIndentation();
1281: }
1282:
1283: private String calculateNodeIndent(final Node n) {
1284: String indent = "";
1285: Node parent = (Node) n.getParentNode();
1286: if (parent != null) {
1287: int index = parent.getIndexOfChild(n);
1288: if (index > 0) {
1289: Node txt = (Node) parent.getChildNodes()
1290: .item(index - 1);
1291: if (checkPrettyText(txt)) {
1292: String wsValue = ((NodeImpl) txt).getTokens()
1293: .get(0).getValue();
1294: int ndx = wsValue.lastIndexOf("\n");
1295: if (ndx != -1 && (ndx + 1) < wsValue.length())
1296: indent = wsValue.substring(ndx + 1);
1297: }
1298: }
1299: }
1300: return indent;
1301: }
1302:
1303: private void doPrettyPrintRecursive(Node n, String indent,
1304: Node parent) {
1305: if ((getStatus() != Status.PARSING) && isPretty()) {
1306: if (isSimpleContent(n))
1307: return; //skip if simpleContent
1308: else if (n instanceof Element && isPretty(n)) {//adjust for pretty length difference
1309: fixPrettyForCopiedNode(n, indent, parent);
1310: } else {
1311: List<Node> childList = new ArrayList<Node>();
1312: List<Node> visitList = new ArrayList<Node>();
1313: NodeList childs = n.getChildNodes();
1314: for (int i = 0; i < childs.getLength(); i++) {
1315: childList.add((Node) childs.item(i));
1316: if (childs.item(i) instanceof Element)
1317: visitList.add((Node) childs.item(i));
1318: }
1319: String parentIndent = indent + getIndentation();
1320: if (childList.size() > 0)
1321: n.appendChild(createPrettyText(parentIndent));
1322: String childIndent = parentIndent + getIndentation();
1323: for (int i = childList.size() - 1; i >= 0; i--) {
1324: Node ref = (Node) childList.get(i);
1325: Text postText = createPrettyText(childIndent);
1326: n.insertBefore(postText, ref);
1327: }
1328: childList.clear(); //no need to keep it beyond here
1329: for (int i = 0; i < visitList.size(); i++) {
1330: doPrettyPrintRecursive((Node) visitList.get((i)),
1331: parentIndent, n);
1332: }
1333: visitList.clear(); //no need to keep it beyond here
1334: }
1335: }
1336: }
1337:
1338: /*
1339: * This function will fix the pretty text of nodes that are cut or copied and
1340: * pasted to xdm tree
1341: */
1342: private void fixPrettyForCopiedNode(Node n, String indent,
1343: Node parent) {
1344: NodeList childs = n.getChildNodes();
1345: if (childs.getLength() == 0)
1346: return;
1347: Text nlastChild = (Text) childs.item(childs.getLength() - 1);
1348: String lc = ((NodeImpl) nlastChild).getTokens().get(0)
1349: .getValue();
1350: NodeImpl pfirstChild = (NodeImpl) parent.getChildNodes()
1351: .item(0);
1352: String fc = pfirstChild.getTokens().get(0).getValue();
1353:
1354: if (fc.length() == lc.length()) {//return if already pretty
1355: return;
1356: } else {
1357: String parentIndent = indent + getIndentation();
1358: String childIndent = parentIndent + getIndentation();
1359: List<Node> childList = new ArrayList<Node>();
1360: for (int i = 0; i < childs.getLength(); i++) {
1361: childList.add((Node) childs.item(i));
1362: }
1363: for (int i = 0; i < childList.size(); i++) {
1364: Node txt = (Node) n.getChildNodes().item(i);
1365: if (checkPrettyText(txt)) {
1366: String newIndent = childIndent + getIndentation();
1367: if (i == 0) {
1368: newIndent = childIndent;
1369: } else if (i == childList.size() - 1) {
1370: newIndent = parentIndent;
1371: }
1372: n.replaceChild(createPrettyText(newIndent), txt);
1373: }
1374: }
1375: for (int i = 0; i < childList.size(); i++) {
1376: fixPrettyForCopiedNode(
1377: (Node) n.getChildNodes().item(i), childIndent,
1378: n);
1379: }
1380: childList.clear(); //no need to keep it beyond here
1381: }
1382: }
1383:
1384: private Text createPrettyText(String indent) {
1385: String textChars = "\n" + indent;
1386: Text txt = (Text) this .getDocument().createTextNode(textChars);
1387: return txt;
1388: }
1389:
1390: private void undoPrettyPrint(Node newParent, Node oldNode,
1391: Node oldParent) {
1392: if ((getStatus() != Status.PARSING) && isPretty()) {
1393: String parentIndent = calculateNodeIndent(oldParent);
1394: int piLength = parentIndent != null ? parentIndent.length()
1395: : 0;
1396: int index = ((NodeImpl) oldParent).getIndexOfChild(oldNode);
1397: Node txtBefore = null;
1398: if (index > 0) {//remove pre pretty print node
1399: txtBefore = (Node) oldParent.getChildNodes().item(
1400: index - 1);
1401: if (checkPrettyText(txtBefore)
1402: && piLength <= getLength((Text) txtBefore)) {
1403: /*
1404: * before
1405: *
1406: * <test name="test1">
1407: * <a name="a1">
1408: * <b name="b1">
1409: * <c name="c1">
1410: * </test>
1411: *
1412: * after
1413: *
1414: * <test name="test1">
1415: * <a name="a1">
1416: * <b name="b1"><c name="c1">
1417: * </test>
1418: */
1419: newParent.removeChild(txtBefore);
1420: }
1421: }
1422: if (newParent.getChildNodes().getLength() == 2
1423: && index + 1 < oldParent.getChildNodes()
1424: .getLength()) {//remove post pretty print node
1425: Node txtAfter = (Node) oldParent.getChildNodes().item(
1426: index + 1);
1427: if (checkPrettyText(txtAfter)
1428: && piLength <= getLength((Text) txtAfter)) {
1429: /*
1430: * before
1431: *
1432: * <test name="test1">
1433: * <a name="a1">
1434: * <b name="b1"><c name="c1">
1435: * </test>
1436: *
1437: * after
1438: *
1439: * <test name="test1">
1440: * <a name="a1">
1441: * <b name="b1"><c name="c1"></test>
1442: */
1443: newParent.removeChild(txtAfter);
1444: }
1445: }
1446: }
1447: }
1448:
1449: private int getLength(Text n) {
1450: int len = 0;
1451: for (Token token : ((NodeImpl) n).getTokens())
1452: len += token.getValue().length();
1453: return len;
1454: }
1455:
1456: private boolean checkPrettyText(Node txt) {
1457: if (txt instanceof Text) {
1458: if ((((NodeImpl) txt).getTokens().size() == 1)
1459: && isWhitespaceOnly(((NodeImpl) txt).getTokens()
1460: .get(0).getValue())) {
1461: return true;
1462: }
1463: }
1464: return false;
1465: }
1466:
1467: private boolean isPossibleWhiteSpace(String text) {
1468: return text.length() > 0
1469: && Character.isWhitespace(text.charAt(0))
1470: && Character.isWhitespace(text
1471: .charAt(text.length() - 1));
1472: }
1473:
1474: private boolean isWhitespaceOnly(String tn) {
1475: return isPossibleWhiteSpace(tn) && tn.trim().length() == 0;
1476: }
1477:
1478: public ElementIdentity getElementIdentity() {
1479: return eID;
1480:
1481: }
1482:
1483: public void setElementIdentity(ElementIdentity eID) {
1484: this .eID = eID;
1485: }
1486:
1487: private boolean isSimpleContent(Node newParent) {
1488: NodeList childs = newParent.getChildNodes();
1489: for (int i = 0; i < childs.getLength(); i++)
1490: if (!(childs.item(i) instanceof Text))
1491: return false;
1492: return true;
1493: }
1494:
1495: private boolean isPretty(Node newParent) {
1496: return isPretty(newParent, null);
1497: }
1498:
1499: private boolean isPretty(Node newParent, Node newNode) {
1500: boolean parentPretty = false;
1501: NodeList childs = newParent.getChildNodes();
1502: int len = childs.getLength();
1503:
1504: /*
1505: * <test name="test1"></test> parentPretty = true
1506: *
1507: * <test name="test1"> parentPretty = true
1508: * </test>
1509: *
1510: * <test name="test1"> parentPretty = true
1511: * <c name="c1">
1512: * </test>
1513: *
1514: * <test name="test1"><c name="c1"> parentPretty = false
1515: * </test>
1516: */
1517: if (len == 0)
1518: parentPretty = true;
1519: else if (len == 1 && childs.item(0) instanceof Text)
1520: parentPretty = true;
1521: else if (len > 2 && checkPrettyText((Node) childs.item(0))
1522: && checkPrettyText((Node) childs.item(len - 1)))
1523: parentPretty = true;
1524:
1525: if (!parentPretty)
1526: return false;
1527:
1528: if (newNode != null) {
1529: //now check newNode pretty
1530: Node preText = null;
1531: Node postText = null;
1532: int index = ((NodeImpl) newParent).getIndexOfChild(newNode);
1533: if (index > 0)
1534: preText = (Node) newParent.getChildNodes().item(
1535: index - 1);
1536: if ((index + 1) < newParent.getChildNodes().getLength())
1537: postText = (Node) newParent.getChildNodes().item(
1538: (index + 1));
1539:
1540: /*
1541: * <test name="test1">
1542: * <a name="a1"/>
1543: * <b name="b1"/>
1544: * <c name="c1"/> 'c' pretty = true
1545: * </test>
1546: *
1547: * <test name="test1">
1548: * <a name="a1"/>
1549: * <b name="b1"/><c name="c1"/> 'c' pretty = false
1550: * </test>
1551: *
1552: */
1553: if (checkPrettyText(preText) && checkPrettyText(postText))
1554: return true;
1555: } else
1556: return parentPretty;
1557:
1558: return false;
1559: }
1560:
1561: /**
1562: * Set/get mapping of QName-valued attributes by element.
1563: * Key of the mapping is QName of the element.
1564: * Value of the mapping is list QName's of the attributes.
1565: * If the mapping is set, it will be used to identify the
1566: * attribute values affected by namespace prefix refactoring
1567: * during namespace consolidation. If not set, namespace consolidation
1568: * would skip prefix rename refactoring case.
1569: */
1570: public void setQNameValuedAttributes(
1571: Map<QName, List<QName>> attrsByElement) {
1572: qnameValuedAttributesByElementMap = attrsByElement;
1573: }
1574:
1575: public Map<QName, List<QName>> getQNameValuedAttributes() {
1576: return qnameValuedAttributesByElementMap;
1577: }
1578:
1579: /**
1580: * The xml syntax parser
1581: */
1582: private XMLSyntaxParser parser;
1583:
1584: /**
1585: * The current stable document represented by the model
1586: */
1587: private Document currentDocument;
1588:
1589: /**
1590: * Property change support
1591: */
1592: private PropertyChangeSupport pcs;
1593:
1594: /**
1595: * The underlying model source
1596: */
1597: private ModelSource source;
1598:
1599: /**
1600: * Current status of the model
1601: */
1602: private Status status;
1603:
1604: private boolean pretty = false;
1605:
1606: /**
1607: * Undoable edit support
1608: */
1609: private UndoableEditSupport ues;
1610:
1611: /**
1612: * whether to fire undo events
1613: */
1614: private boolean fireUndoEvents = true;
1615:
1616: /**
1617: * The names of property change events fired
1618: */
1619: /**
1620: * Indicates node modified
1621: */
1622: public static final String PROP_MODIFIED = "modified";
1623: /**
1624: * Indicates node deleted
1625: */
1626: public static final String PROP_DELETED = "deleted";
1627: /**
1628: * Indicates node added
1629: */
1630: public static final String PROP_ADDED = "added";
1631:
1632: public static final String DEFAULT_INDENT = " ";
1633:
1634: /**
1635: * current node count
1636: */
1637: private int nodeCount = 0;
1638:
1639: private ElementIdentity eID;
1640:
1641: private String currentIndent = "";
1642:
1643: private boolean indentInitialized = false;
1644:
1645: private Map<QName, List<QName>> qnameValuedAttributesByElementMap;
1646: }
|