001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.xml.xam.dom;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.lang.ref.WeakReference;
046: import java.util.ArrayList;
047: import java.util.Collections;
048: import java.util.HashMap;
049: import java.util.HashSet;
050: import java.util.List;
051: import java.util.Map;
052: import java.util.Set;
053: import javax.swing.event.DocumentEvent;
054: import javax.swing.event.DocumentListener;
055: import javax.xml.namespace.QName;
056: import org.netbeans.modules.xml.xam.AbstractModel;
057: import org.netbeans.modules.xml.xam.Component;
058: import org.netbeans.modules.xml.xam.ComponentEvent;
059: import org.netbeans.modules.xml.xam.ComponentUpdater;
060: import org.netbeans.modules.xml.xam.EmbeddableRoot;
061: import org.netbeans.modules.xml.xam.Model.State;
062: import org.netbeans.modules.xml.xam.ModelSource;
063: import org.netbeans.modules.xml.xam.spi.DocumentModelAccessProvider;
064: import org.openide.util.Lookup;
065: import org.w3c.dom.Attr;
066: import org.w3c.dom.Document;
067: import org.w3c.dom.Element;
068: import org.w3c.dom.Node;
069: import org.w3c.dom.NodeList;
070:
071: /**
072: * @author Chris Webster
073: * @author Rico
074: * @author Nam Nguyen
075: */
076: public abstract class AbstractDocumentModel<T extends DocumentComponent<T>>
077: extends AbstractModel<T> implements DocumentModel<T> {
078:
079: protected DocumentModelAccess access;
080: private boolean needsSync;
081: private DocumentListener docListener;
082: private javax.swing.text.Document swingDocument;
083:
084: public AbstractDocumentModel(ModelSource source) {
085: super (source);
086: docListener = new DocumentChangeListener();
087: }
088:
089: public javax.swing.text.Document getBaseDocument() {
090: return (javax.swing.text.Document) getModelSource().getLookup()
091: .lookup(javax.swing.text.Document.class);
092: }
093:
094: public abstract T createRootComponent(Element root);
095:
096: public boolean areSameNodes(Node n1, Node n2) {
097: return getAccess().areSameNodes(n1, n2);
098: }
099:
100: /**
101: * Returns QName of elements used in model. Domain model implementation needs
102: * to override this to be able to embed elements outside of the domain such as
103: * child elements of documentation in schema model.
104: * @return full set of element QName's or null if there is no needs for distinction
105: * between domain and non-domain elements.
106: */
107: public Set<QName> getQNames() {
108: return Collections.emptySet();
109: }
110:
111: @Override
112: protected boolean needsSync() {
113: javax.swing.text.Document lastDoc = swingDocument;
114: javax.swing.text.Document currentDoc = (javax.swing.text.Document) getModelSource()
115: .getLookup().lookup(javax.swing.text.Document.class);
116: if (currentDoc == null) {
117: swingDocument = null;
118: return false;
119: }
120: if (lastDoc == null || currentDoc != lastDoc) {
121: swingDocument = currentDoc;
122: currentDoc.addDocumentListener(new WeakDocumentListener(
123: docListener, currentDoc));
124: }
125: return needsSync || !currentDoc.equals(lastDoc);
126: }
127:
128: @Override
129: protected void syncStarted() {
130: needsSync = false;
131: getAccess().unsetDirty();
132: }
133:
134: @Override
135: protected void syncCompleted() {
136: super .syncCompleted();
137: }
138:
139: private void documentChanged() {
140: if (!isIntransaction()) {
141: getAccess().setDirty();
142: needsSync = true;
143: }
144: }
145:
146: private static class WeakDocumentListener implements
147: DocumentListener {
148:
149: public WeakDocumentListener(DocumentListener delegate,
150: javax.swing.text.Document source) {
151: this .source = source;
152: this .delegate = new WeakReference<DocumentListener>(
153: delegate);
154: }
155:
156: private DocumentListener getDelegate() {
157: DocumentListener l = delegate.get();
158: if (l == null) {
159: source.removeDocumentListener(this );
160: }
161:
162: return l;
163: }
164:
165: public void removeUpdate(DocumentEvent e) {
166: DocumentListener l = getDelegate();
167: if (l != null) {
168: l.removeUpdate(e);
169: }
170: }
171:
172: public void changedUpdate(DocumentEvent e) {
173: DocumentListener l = getDelegate();
174: if (l != null) {
175: l.changedUpdate(e);
176: }
177: }
178:
179: public void insertUpdate(DocumentEvent e) {
180: DocumentListener l = getDelegate();
181: if (l != null) {
182: l.insertUpdate(e);
183: }
184: }
185:
186: private javax.swing.text.Document source;
187: private WeakReference<DocumentListener> delegate;
188: }
189:
190: private class DocumentChangeListener implements DocumentListener {
191: public void removeUpdate(DocumentEvent e) {
192: documentChanged();
193: }
194:
195: public void insertUpdate(DocumentEvent e) {
196: documentChanged();
197: }
198:
199: public void changedUpdate(DocumentEvent e) {
200: // ignore these events as these are not changes
201: // to the document text but the document itself
202: }
203: }
204:
205: protected abstract ComponentUpdater<T> getComponentUpdater();
206:
207: /**
208: * Allows match just by tag name, in case full QName is not available.
209: */
210: private Set<String> elementNames = null;
211:
212: public Set<String> getElementNames() {
213: if (elementNames == null) {
214: elementNames = new HashSet<String>();
215: Set<QName> qnames = getQNames();
216: for (QName q : qnames) {
217: elementNames.add(q.getLocalPart());
218: }
219: }
220: return elementNames;
221: }
222:
223: public ChangeInfo prepareChangeInfo(List<Node> pathToRoot) {
224: // we already handle change on root before enter here
225: if (pathToRoot.size() < 1) {
226: throw new IllegalArgumentException(
227: "pathToRoot here should be at least 1");
228: }
229: if (pathToRoot.get(pathToRoot.size() - 1) instanceof Document) {
230: pathToRoot.remove(pathToRoot.size() - 1);
231: }
232:
233: if (pathToRoot.size() < 2) {
234: throw new IllegalArgumentException(
235: "pathToRoot here should be at least 2");
236: }
237: Node current = null;
238: Element parent = null;
239: boolean changedIsDomainElement = true;
240: Set<QName> qnames = getQNames();
241: Set<String> enames = getElementNames();
242: if (qnames != null && qnames.size() > 0) {
243: for (int i = 0; i < pathToRoot.size(); i++) {
244: Node n = pathToRoot.get(i);
245: if (!(n instanceof Element)) {
246: changedIsDomainElement = false;
247: continue;
248: }
249:
250: QName q = new QName(getAccess().lookupNamespaceURI(n,
251: pathToRoot), n.getLocalName());
252: if (qnames.contains(q)) {
253: current = n;
254: if (i + 1 < pathToRoot.size()) {
255: parent = (Element) pathToRoot.get(i + 1);
256: }
257: break;
258: } else if (changedIsDomainElement == true) {
259: changedIsDomainElement = false;
260: }
261: }
262: } else {
263: Node n = pathToRoot.get(0);
264: if (n instanceof Element) {
265: current = n;
266: parent = (Element) pathToRoot.get(1);
267: } else {
268: current = pathToRoot.get(1);
269: if (pathToRoot.size() > 2) {
270: parent = (Element) pathToRoot.get(2);
271: }
272: changedIsDomainElement = false;
273: }
274: }
275:
276: if (!changedIsDomainElement) {
277: int i = pathToRoot.indexOf(current);
278: if (i < 1) {
279: throw new IllegalArgumentException(
280: "pathToRoot does not contain element");
281: }
282: parent = (Element) current;
283: current = pathToRoot.get(i - 1);
284: }
285:
286: List<Element> rootToParent = new ArrayList<Element>();
287: if (parent != null) {
288: for (int i = pathToRoot.indexOf(parent); i < pathToRoot
289: .size(); i++) {
290: rootToParent.add(0, (Element) pathToRoot.get(i));
291: }
292: }
293:
294: List<Node> otherNodes = new ArrayList<Node>();
295: if (parent != null) {
296: int iCurrent = pathToRoot.indexOf(current);
297: for (int i = 0; i < iCurrent; i++) {
298: otherNodes.add(0, pathToRoot.get(i));
299: }
300: }
301:
302: return new ChangeInfo(parent, current, changedIsDomainElement,
303: rootToParent, otherNodes);
304: }
305:
306: public SyncUnit prepareSyncUnit(ChangeInfo change, SyncUnit order) {
307: if (change.getChangedNode() == null) {
308: throw new IllegalStateException("Bad change info");
309: }
310: AbstractDocumentComponent parentComponent = (AbstractDocumentComponent) change
311: .getParentComponent();
312: if (parentComponent == null) {
313: parentComponent = (AbstractDocumentComponent) findComponent(change
314: .getRootToParentPath());
315: }
316: if (parentComponent == null) {
317: throw new IllegalArgumentException(
318: "Could not find parent component");
319: }
320:
321: DocumentComponent toRemove = null;
322: DocumentComponent toAdd = null;
323: boolean changed = false;
324:
325: if (change.isDomainElement()) {
326: if (change.isDomainElementAdded()) {
327: toAdd = createChildComponent(parentComponent, change
328: .getChangedElement());
329: } else {
330: toRemove = parentComponent.findChildComponent(change
331: .getChangedElement());
332: if (toRemove == null) {
333: parentComponent.findChildComponentByIdentity(change
334: .getChangedElement());
335: }
336: }
337: } else {
338: changed = true;
339: }
340:
341: if (order == null) {
342: order = new SyncUnit(parentComponent);
343: }
344:
345: order.addChange(change);
346: if (toRemove != null)
347: order.addToRemoveList(toRemove);
348: if (toAdd != null)
349: order.addToAddList(toAdd);
350: if (changed)
351: order.setComponentChanged(true);
352: return order;
353: }
354:
355: protected void firePropertyChangedEvents(SyncUnit unit) {
356: firePropertyChangedEvents(unit, null);
357: }
358:
359: protected void firePropertyChangedEvents(SyncUnit unit,
360: Element oldElement) {
361: Set<String> propertyNames = new HashSet(unit
362: .getRemovedAttributes().keySet());
363: propertyNames.addAll(unit.getAddedAttributes().keySet());
364: for (String name : propertyNames) {
365: Attr oldAttr = unit.getRemovedAttributes().get(name);
366: Attr newAttr = unit.getAddedAttributes().get(name);
367: super .firePropertyChangeEvent(new PropertyChangeEvent(unit
368: .getTarget(), name, oldAttr == null ? null
369: : oldAttr.getValue(), newAttr == null ? null
370: : newAttr.getValue()));
371: }
372: if (unit.hasTextContentChanges()) {
373: super
374: .firePropertyChangeEvent(new PropertyChangeEvent(
375: unit.getTarget(),
376: DocumentComponent.TEXT_CONTENT_PROPERTY,
377: oldElement == null ? "" : getAccess()
378: .getXmlFragment(oldElement),
379: getAccess().getXmlFragment(
380: unit.getTarget().getPeer())));
381: }
382:
383: for (String tagname : unit.getNonDomainedElementChanges()) {
384: List<Element> old = new ArrayList<Element>();
385: List<Element> now = new ArrayList<Element>();
386: NodeList oldNodes = oldElement
387: .getElementsByTagName(tagname);
388: for (int i = 0; i < oldNodes.getLength(); i++) {
389: Element e = (Element) oldNodes.item(i);
390: old.add((Element) e.cloneNode(true));
391: }
392: NodeList newNodes = unit.getTarget().getPeer()
393: .getElementsByTagName(tagname);
394: for (int i = 0; i < newNodes.getLength(); i++) {
395: now.add((Element) newNodes.item(i).cloneNode(true));
396: }
397: super .firePropertyChangeEvent(new PropertyChangeEvent(unit
398: .getTarget(), toLocalName(tagname), old, now));
399: }
400: }
401:
402: protected static String toLocalName(String tagName) {
403: String[] parts = tagName.split(":"); //NOI18N
404: return parts[parts.length - 1];
405: }
406:
407: public void processSyncUnit(SyncUnit syncOrder) {
408: AbstractDocumentComponent targetComponent = (AbstractDocumentComponent) syncOrder
409: .getTarget();
410: if (targetComponent == null) {
411: throw new IllegalArgumentException(
412: "sync unit should not be null");
413: }
414: // skip target component whose some ancestor removed in previous processed syncUnit
415: if (!targetComponent.isInDocumentModel()) {
416: return;
417: }
418:
419: Element oldElement = syncOrder.getTarget().getPeer();
420: syncOrder.updateTargetReference();
421: if (syncOrder.isComponentChanged()) {
422: ComponentEvent.EventType changeType = ComponentEvent.EventType.VALUE_CHANGED;
423: if (!syncOrder.hasWhitespaceChangeOnly()) {
424: fireComponentChangedEvent(new ComponentEvent(
425: targetComponent, changeType));
426: }
427: firePropertyChangedEvents(syncOrder, oldElement);
428: }
429:
430: for (DocumentComponent c : syncOrder.getToRemoveList()) {
431: removeChildComponent(c);
432: }
433:
434: for (DocumentComponent c : syncOrder.getToAddList()) {
435: Element childElement = (Element) ((AbstractDocumentComponent) c)
436: .getPeer();
437: int index = targetComponent.findDomainIndex(childElement);
438: addChildComponent(targetComponent, c, index);
439: }
440: }
441:
442: private DocumentComponent createChildComponent(
443: DocumentComponent parent, Element e) {
444: DocumentModel m = (DocumentModel) parent.getModel();
445: if (m == null) {
446: throw new IllegalArgumentException(
447: "Cannot create child component from a deleted component.");
448: }
449: return m.createComponent(parent, e);
450: }
451:
452: public void addChildComponent(Component target, Component child,
453: int index) {
454: AbstractDocumentModel m = (AbstractDocumentModel) target
455: .getModel();
456: //assert m != null : "Cannot add child to a deleted component.";
457: //Work-around xdm overlapping in firing
458: if (m == null)
459: return;
460: m.getComponentUpdater().update(target, child, index,
461: ComponentUpdater.Operation.ADD);
462: }
463:
464: public void removeChildComponent(Component child) {
465: if (child.getParent() == null)
466: return;
467: AbstractDocumentModel m = (AbstractDocumentModel) child
468: .getParent().getModel();
469: //Work-around xdm overlapping in firing
470: //assert m != null : "Cannot remove child from a deleted component.";
471: if (m == null)
472: return;
473: m.getComponentUpdater().update(child.getParent(), child,
474: ComponentUpdater.Operation.REMOVE);
475: }
476:
477: public DocumentComponent findComponent(Element e) {
478: return findComponent(
479: (AbstractDocumentComponent) getRootComponent(), e);
480: }
481:
482: private DocumentComponent findComponent(
483: DocumentComponent searchRoot, Element e) {
484: if (searchRoot.referencesSameNode(e)) {
485: return searchRoot;
486: }
487: for (Object o : searchRoot.getChildren()) {
488: DocumentComponent found = findComponent(
489: (DocumentComponent) o, e);
490: if (found != null) {
491: return found;
492: }
493: }
494: if (searchRoot instanceof EmbeddableRoot.ForeignParent) {
495: for (EmbeddableRoot child : ((EmbeddableRoot.ForeignParent) searchRoot)
496: .getAdoptedChildren()) {
497: if (child instanceof DocumentComponent) {
498: DocumentComponent found = findComponent(
499: (DocumentComponent) child, e);
500: if (found != null) {
501: return found;
502: }
503: }
504: }
505: }
506:
507: return null;
508: }
509:
510: /**
511: * Find the component given a path to its element node from root. All elements, except for
512: * the target element should be in the latest version of the xdm tree. All components on the
513: * path will be updated with latest version elements.
514: *
515: * Note that returned component could be part of an embedded model, which could be of
516: * a different type of model.
517: *
518: * @param pathFromRoot list of elements from model root to backing element of target component.
519: * @return component backed by the last element on pathFromRoot or null if not found.
520: */
521: public DocumentComponent findComponent(List<Element> pathFromRoot) {
522: return findComponent(
523: (AbstractDocumentComponent) getRootComponent(),
524: pathFromRoot, 0);
525: }
526:
527: public AbstractDocumentComponent findComponent(
528: AbstractDocumentComponent base, List<Element> pathFromRoot,
529: int current) {
530: if (pathFromRoot == null || pathFromRoot.size() <= current) {
531: return null;
532: }
533: Element e = pathFromRoot.get(current);
534: if (base.referencesSameNode(e)) {
535: if (pathFromRoot.size() == current + 1) {
536: base.getChildren(); // make sure children inited
537: return base;
538: } else {
539: for (Object child : base.getChildren()) {
540: AbstractDocumentComponent ac = (AbstractDocumentComponent) child;
541: AbstractDocumentComponent found = findComponent(ac,
542: pathFromRoot, current + 1);
543: if (found != null) {
544: return found;
545: }
546: }
547: }
548: }
549: return null;
550: }
551:
552: public DocumentComponent findComponent(int position) {
553: if (getState() != State.VALID) {
554: return getRootComponent();
555: }
556:
557: Element e = (Element) getAccess()
558: .getContainingElement(position);
559: if (e == null) {
560: return getRootComponent();
561: }
562:
563: List<Element> pathFromRoot = null;
564: try {
565: pathFromRoot = getAccess().getPathFromRoot(
566: this .getDocument(), e);
567: } catch (UnsupportedOperationException ex) {
568: // OK
569: }
570: if (pathFromRoot == null || pathFromRoot.isEmpty()) {
571: return findComponent(e);
572: } else {
573: return findComponent(pathFromRoot);
574: }
575: }
576:
577: public String getXPathExpression(DocumentComponent component) {
578: Element e = (Element) component.getPeer();
579: return getAccess().getXPath(getDocument(), e);
580: }
581:
582: public org.w3c.dom.Document getDocument() {
583: return getAccess().getDocumentRoot();
584: }
585:
586: public DocumentModelAccess getAccess() {
587: if (access == null) {
588: access = getEffectiveAccessProvider().createModelAccess(
589: this );
590: if (!(access instanceof ReadOnlyAccess)) {
591: access.addUndoableEditListener(this );
592: setIdentifyingAttributes();
593: }
594: }
595: return access;
596: }
597:
598: private DocumentModelAccessProvider getEffectiveAccessProvider() {
599: DocumentModelAccessProvider p = (DocumentModelAccessProvider) getModelSource()
600: .getLookup().lookup(DocumentModelAccessProvider.class);
601: return p == null ? getAccessProvider() : p;
602: }
603:
604: public static DocumentModelAccessProvider getAccessProvider() {
605: DocumentModelAccessProvider provider = (DocumentModelAccessProvider) Lookup
606: .getDefault().lookup(DocumentModelAccessProvider.class);
607: if (provider == null) {
608: return ReadOnlyAccess.Provider.getInstance();
609: }
610: return provider;
611: }
612:
613: /**
614: * Set the identifying attributes for underlying access to merge.
615: */
616: protected void setIdentifyingAttributes() {
617: ElementIdentity eid = getAccess().getElementIdentity();
618: eid.addIdentifier("id");
619: eid.addIdentifier("name");
620: eid.addIdentifier("ref");
621: }
622:
623: protected boolean isDomainElement(Node e) {
624: if (!(e instanceof Element)) {
625: return false;
626: }
627:
628: QName q = new QName(e.getNamespaceURI(), e.getLocalName());
629: return getQNames().contains(q)
630: || getElementNames().contains(q.getLocalPart());
631: }
632:
633: @Override
634: protected void refresh() {
635: Document lastStable = null;
636: try {
637: lastStable = getDocument();
638: } catch (Exception ex) {
639: // document is not available when underlying model is broken
640: }
641: if (lastStable != null
642: && lastStable.getDocumentElement() != null) {
643: createRootComponent(lastStable.getDocumentElement());
644: setState(State.VALID);
645: }
646: }
647:
648: /**
649: * Returns QName of all attributes with QName value, sorted by containing
650: * element QName.
651: * Note: if domain model implementation return null, namespace
652: * consolidation will not attempt namespace prefix refactoring on each
653: * mutation of the underlying XDM DOM tree.
654: */
655: public Map<QName, List<QName>> getQNameValuedAttributes() {
656: return new HashMap<QName, List<QName>>();
657: }
658: }
|